Skip to content

controlledByParent.ownerMayEdit admits only owner_id, but its lint arm prescribes it for any FIELD == current_user.id policy — an app whose accountable person is its own user field stays refused #26591

Closed

Description

@objectstack-fleet

Filing gate: ③ platform gap with a lint/runtime disagreement, measured on @objectstack 18.2.0 (hotclm main c53d37d, README-made personas).

What happens. hotclm's clm_obligation is controlled_by_parent under clm_contract. The person accountable for an obligation is its own user field owner (DESIGN.md §03), not the injected owner_id. 我负责的履约, the F10 reminders and the requester's update policy all key on owner: obligation_requester_own, using: owner == current_user.id.

So lint tells the author that the opt-in fixes a policy which, at runtime, the opt-in never admits.

Evidence. hotclm os-dev-report objectstack-ai/hotclm#64 (comment), and the before/after tables in hotclm PR #165.

Expected (one of).

  1. The opt-in can name the detail's owner column, for example controlledByParent: { ownerMayEdit: true, ownerField: 'owner' } (a Field.user on the detail), and the gate compares that column. This matches Salesforce's "Assigned To", which is a field distinct from the record's creator-owner.
  2. Or the platform rules that there is one owner concept (owner_id). Then the lint arm narrows to policies keyed on owner_id and says so for any other field, and apps move their accountable column onto owner_id.

The platform should rule between 1 and 2. Either way, lint and gate should agree.

Business reason. In hotclm, 我负责的履约 is read-only for the 200 requesters it is built for (hotclm #64, ruled D: platform). HotCRM tasks assigned to a rep who only reads the account are the same shape.

Consumer. objectstack-ai/hotclm #64 (Blocked-by this card).

Duplicate search. objectstack issues, all states: "ownerMayEdit owner field", "controlledByParent owner column", "owner-policy-without-opt-in". The only related item is #25760 (closed, the original gap, answered by #26005). No duplicate.


Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Oct 27, 2026
  2. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: audit line · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-27T09:55Z

    ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 4 · card thread · 18.2.0 wave-2 split with server seat 1
    Session: session_0186vfjHHMsRXC9EUT9hTFMo (the seat); the card thread is session_01FEkQ5oX5j1RjbyiKQpdeiF
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-j9djvl (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#2
    Ruling-ref: triage 6438797534 (default arm: narrow the lint so it prescribes ownerMayEdit only where the runtime honours it, owner_id; widening the runtime to a named owner column moves a security boundary (#25972) and is the maintainer's decision, put to him on a decision card)
    File surface: packages/lint/src/validate-security-posture.ts (the security-controlled-by-parent-owner-policy-without-opt-in arm), packages/lint/src/rule-explanations.ts, their tests, the path readers that describe the arm's behaviour (packages/spec/liveness/object.json evidence string, the object.zod.ts docblock near :3089) if their wording becomes false, and a changeset on @objectstack/lint. ⛔ No packages/plugins/plugin-security/** runtime change, ⛔ no new packages/spec/** key, ⛔ no docs/adr/**
    Container & model: S, mode:thread, default model and effort (set at thread open)
    Clause-②: no (lint advice is not a contract face; no accepted metadata shape moves)
    Metadata-protocol: none
    Responsibility: @objectstack/lint security-posture rule vs plugin-security assertControlledByParentWrite (#26005)
    Thread-read: 6438797534
    Premise: holds (read 2026-10-27T10:32Z at origin/main 7c34e5dc21: card open, unassigned, pm:queue with its triage line; unclaimed by either seat; the lane reads 9 dispatched, under its cap); the dev re-measures it first, and a refutation is a premise_still_valid: false hand-back
    Resume-from: none — fresh from origin/main
    Serial constraints cleared: yes — the dev re-measures open PRs on the file surface before the first edit and before pr_create

    State: pm:queue → pm:dispatched, assigned to os-project-manager, in this act.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 26591,
    "status": "done",
    "branch": "claude/project-thread-j9djvl",
    "pr": "#26658",
    "session": "session_01FEkQ5oX5j1RjbyiKQpdeiF (card thread; harness-stamped Claude-Session line)",
    "premise_still_valid": true,
    "summary": "Premise held at origin/main 7c34e5d. assertControlledByParentWrite compares only the stored owner_id, and resolveCbpRelation resolves ownerMayEdit false on a schema with no owner_id. The lint arm prescribed the opt-in for any FIELD == current_user.id update policy. The arm now keeps { optedIn, ownerColumn } per resolving detail. ownerColumn is read the runtime's way: the spec injection plan (injectedColumnsFor) or an author field named owner_id. A predicate keying owner_id on a detail that carries it keeps today's behaviour: the opt-in is prescribed when absent, and the arm is silent when present. Any other caller-keyed predicate, such as hotclm's owner == current_user.id or an owner_id on a detail with no owner_id column, is flagged under the same rule id and warning tier whether or not the detail opted in. That verdict says the opt-in admits only the stored owner_id, and its hint names the owner_id remedy and never the opt-in. callerIdentityKeyedFields keeps its shape: it already returns field names, and this arm is its only caller. The runtime gate, packages/spec keys and docs/adr are untouched. rule-explanations.ts covers both cases. The liveness ownerMayEdit evidence string described the arm as flagging an owner-keyed policy on a detail that has not declared the opt-in, which no longer holds, so it now describes the narrowed arm (verifiedAt 2026-10-27). The object.zod.ts docblock near :3089 stays true (gate and lint read the opt-in through one reader) and is unchanged. Changeset: @objectstack/lint patch, plus @objectstack/spec patch because liveness/ ships in spec files[]. Deviations: the first closure build (pnpm --filter @objectstack/lint^... build) ran outside os-verify-lock. Every later build and test ran under the lock. The full lint suite ran before the final two edits (a corpus row and a typecheck fix); the two touched suites and typecheck re-ran on the final tree.",
    "tests": "HEAD 8165012. pnpm --filter @objectstack/lint test: Test Files 166 passed (166), Tests 7395 passed | 5 skipped (7400), on the tree before the reachability-corpus row and the detailName typecheck fix. On the final tree: the two touched suites gave Test Files 2 passed (2), Tests 210 passed (210), and pnpm --filter @objectstack/lint typecheck exited 0. eslint --no-inline-config on the 4 touched lint files exited 0. pnpm --filter @objectstack/spec exec vitest run gave Test Files 810 passed (810), Tests 23353 passed | 1 todo, VERDICT command-exit 0. Ablation via scripts/ablation-replace.mjs: the anchor 'const admittable = detail.ownerColumn && fields.includes(CBP_OWNER_COLUMN);' became 'const admittable = true || (...); // ABL26591' (the old behaviour). The anchor went x1 to x0 and the replacement x0 to x1, with blob f52007d93faf to 3b0a2d4cd9cb. The two suites read Tests 5 failed | 205 passed (210): both other-field cases and the three no-owner_id cases (org/none/business_unit), with every owner_id case green. The restore was proven: blob equal to HEAD and git diff HEAD empty. The suite imports src, so there was no dist leg.",
    "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack derived 65 commands. All 65 ran, with every exit captured before any pipe, and all 65 exited 0. The --ran reading: '65 derived famil(ies) accounted for — 65 run, 0 NOT-MEASURED'. Four commands first exited 3 (PREREQUISITE NOT MET) and their re-runs are the readings counted. check-plugin-teardown-shape --self-test re-ran after git fetch origin 621a487 (48 cases pass). check:docs-transcript-drift, check:lean-entry-closure and check:dual-build-cjs-loads re-ran after building the lint and objectql closures. CI check-runs on PR #26658: in_progress at report time, not awaited per os-dev.md.",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm (Clause-②: no, copied from the claim)",
    "mcp_calls": "0 — no MCP GitHub calls; reads went through gh api REST (issue, comments, open-PR file lists)",
    "api_writes": "2 relay strokes + 1 git push = write budget 3 of 3. Push: git push -u origin claude/project-thread-j9djvl. pr_create through fleet-write dispatch (run 40498760583: POST pulls, then the assignees leg POST issues/26658/assignees); read-back 9866 bytes sent, 9866 stored, identical. This os-dev-report comment through post-stamped.mjs (POST issues/26591/comments).",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: docs drift (none of a/b/c) · reach: docs/adr/0055-master-detail-controlled-by-parent.md:176 says an update/all policy that keys a field on current_user.id on a detail that has not opted in "is flagged with the opt-in as the fix". After #26658 that holds only for a policy keyed on owner_id on a detail that carries it. Another field, or a detail with no owner_id, is flagged with the owner_id remedy instead, and an opted-in detail is flagged for such a policy too. · the order keeps docs/adr out of this PR · carrier: server seat 2 (order-26591 names it)",
    "class: observation · reach: none measured · on an opted-in detail, a mixed predicate (owner == current_user.id || owner_id == current_user.id) stays silent because its owner_id disjunct is admitted, while its owner disjunct can only narrow master editors. Left silent on purpose: the order covers single-field shapes, and flagging a partly working policy would widen the arm · carrier: none · noted, not filed",
    "class: decision (runtime) · reach: maintainer · widening assertControlledByParentWrite to a named owner column (card option 1, e.g. ownerField) moves a security boundary (#25972) and stays out of this PR · carrier: server seat 2 (putting it to the maintainer per the order)"
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed

    2026-10-27T12:50Z · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo. PR #26658 merged at 2026-10-27T12:46:01Z by the merge queue as d434481503 on main (Fixes #26591; ACCEPT 6441703535; Clause-②: no, so no contract review was owed). The merge closed this card as completed.

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-26658-*' 0 refs (left the queue)
    origin/main by content git grep -c "pushCbpCallerPolicyOffOwnerColumn" origin/main -- packages/lint/src/validate-security-posture.ts 2
    Ancestry git merge-base --is-ancestor d434481503 origin/main holds
    Card state GET /repos/objectstack-ai/objectstack/issues/26591 closed, completed
    • Landed: the triage's default arm, shipped as an @objectstack/lint patch plus an @objectstack/spec patch for the liveness evidence string.
      • security-controlled-by-parent-owner-policy-without-opt-in prescribes ownerMayEdit only for an update policy keyed on owner_id, on a detail that carries that column.
      • A policy keyed only on another user field (hotclm's owner == current_user.id), or on owner_id where the detail has no such column, is flagged whether or not the detail opted in. Its hint says to key the accountable person on owner_id, and never names the opt-in.
      • The runtime gate is unchanged.
    • Remains:
    • State: closed by the merge. The card thread is done.

    Generated by Claude Code

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

Metadata

Metadata

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpm:dispatchedpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions