Repository navigation
Lock a task's source, owner and business unit against callers - #182
Merged
Merged
Conversation
) A duly_member or duly_manager could write source, owner and business_unit on a task through every door (data POST/PATCH, MCP create/update), and so put self-made work into the governed readings or a row into someone else's, which that person could not see (#143). - source and business_unit are readonly: the platform strips a caller's value on insert and update (source re-defaults to self). The dispatcher, the assignment fan-out and the seed write as the system and keep writing both. - The lifecycle hook stamps business_unit on a caller's insert from the writer's primary business unit, the rung the fan-out already reads. - owner stays writable, because a readonly field skips required and the #148 fan-out relies on an ownerless row being refused. A new guard, duly_task_owner_is_writer, refuses a caller's insert naming someone else and an update changing the owner. Managers follow the same rule: their door for a report's work is the assignment. Fixes #176 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbaGDrSsQpv7GZxtDyZ
) #178 measured that the console honours a permission set's field locks: a locked field renders read-only and is left out of the form's POST and PATCH. Re-measured on duly_task: that holds, and a payload naming a locked field is refused on every door (403 "Field write denied"). - TASK_PROVENANCE_LOCK on duly_member, duly_manager and duly_admin locks duly_task.source, owner and business_unit. A loud refusal on all three, as the seat ruled for owner, and owner keeps its required (a readonly field is validated shape-only). - readonly on the object and the duly_task_owner_is_writer guard are dropped; the lock does both jobs declaratively. - The lifecycle stamp now fills business_unit from the OWNER's primary unit, only when the insert carries none, so a caller the lock does not bind keeps a unit it names. - Three #174 tests sent source or owner, which now meet the lock first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbaGDrSsQpv7GZxtDyZ
Contributor
席位核对ACCEPT — PR #182 at head
Generated by Claude Code |
Contributor
LandedSeat landing at 2026-11-02T07:08Z (duly lane seat, session
State: #176 is closed Generated by Claude Code |
os-warren
pushed a commit
that referenced
this pull request
Nov 2, 2026
Each Duly set's `fields` is now the task lock (#182, editable false on all three sets) spread with the duty map (editable false for member and manager, true for duly_admin). The header note names both maps; #182's note on the most-permissive merge is unchanged and still exact for the task lock. test/duty-provenance.test.ts pins both maps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbaLRNHsfxqyrXcJtqc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Warren · project thread
Fixes #176
Step 0, measured
On main 94419c6, @objectstack 18.2.0, live
pnpm demo. The callers were member Owen and manager Marek, whose report is Sami. All 24 writes were accepted. The doors were data POST and PATCH and MCP create and update. The writes weresource: catalog,owner: Sami, and a foreignbusiness_unit. Sami reads 0 of the tasks naming him (#143). A hand-made task got no business unit at all. No conflict with #143: after this change, every caller-written task hasowner=owner_id= the writer. Full report: project fileduly-176-task-provenance/step0.md.Change
TASK_PROVENANCE_LOCK(src/security/permission-sets.ts) sits onduly_member,duly_managerandduly_admin. It setsduly_task.source,ownerandbusiness_unittoreadable: true, editable: false. The platform's field-level write gate then refuses any payload that names one of them, on every door, with403 Field write denied: not permitted to edit [<field>].ownerdefaults to the writer, so a caller creates tasks only for themselves. Per the seat's ruling, a manager follows the same rule. Their door for a report's work isduly_assignment.sourcedefaults toself.business_unitstamp (duly_task_lifecycle_stamps, alreadyrunAs: 'system'). On a non-system insert with no unit, it reads the owner'sprimary_business_unit_id. That is the same rung the fan-out reads. The FLS gate judges only the caller's payload, so the hook's write lands.readonly. Areadonlyfield is validated shape-only, sorequiredstops being checked. Measured: a system insert withowner: nullthen commits an ownerless task, which breaks the Assignment fan-out to a removed account creates a task silently: the system write skips the reference check #148 fan-out refusal. The first commit on this branch usedreadonlyplus an owner guard hook. The second commit replaces both with the lock, after A member can create a duty with source catalog, another person as owner and a foreign business unit #178 showed that the console honours FLS on the form. Seat amendment comment 6571067672.test/task-duty-access.test.tssentsourceorowner, so the lock now refuses them first. They now leave those keys out, or expect the lock's refusal.test/task-hook-body.test.tspins the second elevated read.Verify
pnpm validate,typecheck,test(44 files, 1238 tests) andbuildare all green.test/task-provenance.test.ts(61 tests) runs as a real member, a real manager and a member +duly_adminco-holder, throughSecurityPlugin,SharingServicePluginandbuildContextForUser:updateMany, and a predicate multi update.self, and in the owner's unit. An edit that names none of them saves.runDispatch(catalog, the duty's owner and unit); the fan-out run with{ isSystem, userId: manager }(assigned, the assignee's unit); and the seed context.owner: nullfrom the system is still refused with "Owner is required".pnpm demoon this branch:source,ownerorbusiness_unitisError PERMISSION_DENIED, "Field write denied", 24 of 24self, the writer's unitassigned, Sami's unitcatalogand 8assignedtasks, all with a unit{status, subject}→ 201, and its PATCH sends only the changed field → 200.Not in this PR
duly_dutyis A member can create a duty with source catalog, another person as owner and a foreign business unit #178.sys_business_unit, so a task's unit shows as a raw id on the record page and blank on the edit form. That predates this PR and is tracked in A member's task and duty pages show the business unit as a raw id, because Duly members and managers cannot read sys_business_unit #180.Generated by Claude Code