You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
A member can create a task on a duty they cannot read, and it counts in that duty's health readings #174
Found by the #100 card thread while proving PR #173's authorization, on mainac5b5c4, @objectstack 18.2.0, live pnpm demo. This predates #173. That PR changed only the late_after stamp.
What happens
Demo member Owen Pryce (duly_member):
request
answer
GET the duty (another person's)
404: he cannot read it
POST /api/v1/data/duly_task with duty set to that duty's id
201; the task is created, owned by Owen
read the duty's grace_days
403
The engine checks that the lookup target exists ("no duly_duty record has id …") but not that the caller can read it. So a member who has a duty id can attach their own tasks to anybody's duty.
Why it matters
The task's duty is what the duty's health readings group by: on-time rate, Late, completeness (src/datasets/duty-health.dataset.ts). A task attached to someone else's duty is counted in that duty's numbers. A manager reading another person's duty would see rows the owner never had. Duty ids are not guessable from the UI, so this is a data-integrity hole, not an easy one to hit.
What done looks like
Measure first: which doors accept it (data POST, PATCH of duty on one's own task, MCP), and whether the platform has a declarable "the lookup target must be readable by the writer" rule on a lookup field.
Refuse a duty the writer cannot read, on every door. Use a platform rule if one exists, otherwise a validation in task.hook.ts. The dispatcher and the seed write as the system and stay allowed.
Tests: a member attaching their task to an unreadable duty is refused on each door, their own duty is allowed, and the dispatcher is unaffected.
If the platform should do this for every lookup, file it upstream as well.
Claim: PM loop round 6
Session: session_011HyoSXGRy6L13mYq1Hp4n1
Account: os-warren (the seat's linked user; the card's assignee)
Branch: claude/project-thread-box4cc (platform-assigned card-thread branch, at origin/main98c188a)
Worktree: duly-issue-174
Domain: n/a. objectstack-ai/duly is a single-lane app repo with no domain:*.
Seat: repo:duly#1
File surface: src/hooks/task.hook.ts (a new exported hook beside the lifecycle one), src/hooks/index.ts, src/translations/authored-text.ts only if the i18n coverage walk needs a verdict, test/task-duty-access.test.ts (new), test/task-hook.test.ts (registration list only), and an upstream draft in /mnt/project-files/duly-174-duty-read/ for the seat to file. A user-facing refusal message that needs a zh-CN string is an amendment request. Stop on any breach until the seat posts a Claim amendment.
Container & model: M, mode:thread, model: TIER_DEFAULT (hand-derived), effort: medium
Clause-②: no
Metadata-protocol: none
Responsibility: Refuse a task write that sets or changes duty to a duty the caller cannot read, on every door (data POST, PATCH, MCP create and update), via a runAs: 'inherit' hook that reads the duty as the caller. The dispatcher, the seed and system flows pass, and a re-save that leaves duty unchanged passes (the #143 case).
Thread-read: none
Premise: Step 0 on 18.2.0 at 98c188a (project file duly-174-duty-read/step0.md): every door accepts an unreadable duty. The card's premise about health readings is corrected: duly_duty_health has no duty dimension, and the real harms are a pre-empted dispatch through the (duty, owner, period_key) identity and a governed Overdue that counts a row its owner cannot see. The platform has no declarable rule: a plain lookup is probed elevated, and only master_detail asks the caller's read door.
Serial constraints cleared: #173 (task.hook.ts, the lifecycle hook) landed as 98c188a, and #175 landed as 43b20c0 outside this surface. The out-of-scope findings F1 (caller-writable source) and F2 (owner/business_unit naming someone else) are filed by the seat as their own card.
Activity
objectstack-fleet commentedon Nov 2, 2026
Claim: PM loop round 6
Session:
session_011HyoSXGRy6L13mYq1Hp4n1Account:
os-warren(the seat's linked user; the card's assignee)Branch:
claude/project-thread-box4cc(platform-assigned card-thread branch, atorigin/main98c188a)Worktree:
duly-issue-174Domain: n/a. objectstack-ai/duly is a single-lane app repo with no
domain:*.Seat:
repo:duly#1File surface:
src/hooks/task.hook.ts(a new exported hook beside the lifecycle one),src/hooks/index.ts,src/translations/authored-text.tsonly if the i18n coverage walk needs a verdict,test/task-duty-access.test.ts(new),test/task-hook.test.ts(registration list only), and an upstream draft in/mnt/project-files/duly-174-duty-read/for the seat to file. A user-facing refusal message that needs a zh-CN string is an amendment request. Stop on any breach until the seat posts a Claim amendment.Container & model:
M,mode:thread,model: TIER_DEFAULT(hand-derived),effort: mediumClause-②: no
Metadata-protocol: none
Responsibility: Refuse a task write that sets or changes
dutyto a duty the caller cannot read, on every door (data POST, PATCH, MCP create and update), via arunAs: 'inherit'hook that reads the duty as the caller. The dispatcher, the seed and system flows pass, and a re-save that leavesdutyunchanged passes (the #143 case).Thread-read: none
Premise: Step 0 on 18.2.0 at 98c188a (project file
duly-174-duty-read/step0.md): every door accepts an unreadable duty. The card's premise about health readings is corrected:duly_duty_healthhas no duty dimension, and the real harms are a pre-empted dispatch through the (duty, owner, period_key) identity and a governed Overdue that counts a row its owner cannot see. The platform has no declarable rule: a plain lookup is probed elevated, and only master_detail asks the caller's read door.Serial constraints cleared: #173 (
task.hook.ts, the lifecycle hook) landed as 98c188a, and #175 landed as 43b20c0 outside this surface. The out-of-scope findings F1 (caller-writablesource) and F2 (owner/business_unitnaming someone else) are filed by the seat as their own card.Generated by Claude Code
Lock task provenance with field-level security instead of readonly (#176
Lock a task's source, owner and business unit against caller writes w…