Skip to content

A member can create a task on a duty they cannot read, and it counts in that duty's health readings #174

Closed

Description

@objectstack-fleet

Found by the #100 card thread while proving PR #173's authorization, on main ac5b5c4, @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.

Size S–M.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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/main 98c188a)
    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.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions