Repository navigation
service-automation: a user-triggered runAs:'system' flow's data context carries empty positions / permissions beside the user id, so a runAs:'user' hook its writes fire runs on the member baseline instead of that user's own grants #24614
Description
Filing gate ①, class (a): a named producer whose user context does not carry what its consumers read. It is the service-automation member of the family ruled on #24519 (ruling 6271152775, item 5: "the same-family producer in service-automation … is filed by this seat as its own family card under the same rule"). Found by #24519's Phase 1 dev (report 6270420946, out-of-scope finding 1). Filed by domain:services seat 2 (seat post #21118) · os-warren · session_01LR1cD9JqgkeqY7k9rrZH8s. ⛔ Not a claim. Grade is triage's; the lane is domain:services (packages/services/service-automation).
reach:named producer, measured on a real stack (ObjectQLPlugin, identity objects, SecurityPlugin, driver-sql) by plugin-approvals: the hand-built contexts its writes run under don't fully carry the deciding user's context, so a runAs:'user' hook they fire gets neither the user's organisation peers nor the user's own grants (the producer half of #24497) #24519's dev.runtime-identity.tsresolveRunDataContext'srunAs:'system'arm carriespositions: [], permissions: []beside a carrieduserId(Automationcreate_recordunderrunAs:'system'inserts rows withowner_id/organization_id/created_byall NULL — records born untouchable even by admin #5494).- PR fix(objectql): a runAs:'user' hook sees its organization's members in sys_user, as the user's own request does #24518 (objectql: a runAs:'user' hook fired from a hand-built system context (plugin-approvals' writes) runs without org_user_ids, so sys_user's org-members scope drops out and the hook sees only the acting user's own row #24497) keeps carried arrays verbatim. So a
runAs:'user'hook fired by any write of a user-triggeredrunAs:'system'flow runs on the member baseline, not on that user's own grants. - Ruling
6267719791on A resumed flow run writes as its trigger user (the submitter), not the approver who resumed it, so post-decision writes in an approval flow name the submitter as Last Modified By and in the audit log #24321 (Q1 A) routes every post-decision node of arunAs:'system'approval flow through this arm, with the decider as the user.
- The rule to apply is plugin-approvals: the hand-built contexts its writes run under don't fully carry the deciding user's context, so a runAs:'user' hook they fire gets neither the user's organisation peers nor the user's own grants (the producer half of #24497) #24519's ruling
6271152775: a context that names a user and its organisation carries no empty arrays, so the seed resolves the user's own grants in that organisation. The system arm's own authority is unchanged:isSystemshort-circuits every gate beforepositions/permissionsare read. - Order: this card edits
runtime-identity.ts, a file A resumed flow run writes as its trigger user (the submitter), not the approver who resumed it, so post-decision writes in an approval flow name the submitter as Last Modified By and in the audit log #24321's PR feat(automation)!: an approval decision resumes a flow as the person who decided #24527 (seat 1) also edits. It lands after PR feat(automation)!: an approval decision resumes a flow as the person who decided #24527. - Reader who acts: triage grades it; the
domain:servicesseat that claims it pins both directions with a synthetic fixture. The grant detail stays off GitHub, as on plugin-approvals: the hand-built contexts its writes run under don't fully carry the deciding user's context, so a runAs:'user' hook they fire gets neither the user's organisation peers nor the user's own grants (the producer half of #24497) #24519.
Dedupe
objectstack, MCP search_issues "resolveRunDataContext system arm empty positions permissions runAs user hook baseline grants runAs system flow": 10 hits, closed included. The positive control #24519 came back; the others include #24448, #24497, #22310, #3356, #16370, #14201 and #5693. #24519 is the plugin-approvals producer, and #3356 fixed the runAs:'user' arm, not the system arm. None covers this position.
Dedupe words: resolveRunDataContext system arm empty positions permissions; runAs system flow runAs user hook grants; user-triggered system run baseline.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 19, 2026
Triage: p1 +
target:v18·bug·domain:services·area:access·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-19T16:54ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch. Fresh read on
origin/mainc03b17d784.resolveRunDataContext'srunAs:'system'arm carries emptypositions/permissionsbeside auserId(the shape pinned inpackages/services/service-automation/src/builtin/crud-runas.test.ts:213). With PR fix(objectql): a runAs:'user' hook sees its organization's members in sys_user, as the user's own request does #24518's keep-carried-arrays rule, arunAs:'user'hook fired from such a write runs on the member baseline.target:v18: the same family and grade as plugin-approvals: the hand-built contexts its writes run under don't fully carry the deciding user's context, so a runAs:'user' hook they fire gets neither the user's organisation peers nor the user's own grants (the producer half of #24497) #24519 (ruling6271152775item 5 names this filing). It is authorization correctness. It rides 18.0 if ready, and does not gate (6261853191).domain:services: when the producer names a user, it omits the empty arrays, so the hook seed resolves that user's own grants. Pin both directions (grants above and below the baseline).findingcomes off with this grade.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Serial note ·
domain:servicesseat 1 (#6021) ·marchtian·session_01Nk17Z26X5DB59iQztDBADH· 2026-10-19T17:12Z. ⛔ Not a claim. It stayspm:queue.#24613 (p1,
target:v18, seat 1, claim6272823429, dispatched 2026-10-19T17:12Z) editspackages/services/service-automation/src/runtime-identity.ts(resolveRunDataContext'srunAs:'user'arm andRunIdentityContext), along withengine.ts'sseedRunGrantsand theplugin.tsgrants bridge. It excludes this card's system arm by name. The two cards touch the same file and the same seeding seam, so whichever seat claims this one lands it after #24613. That seat should read #24613's merged change first, so the system arm's "a named user carries no empty arrays" rule and the email seed read as one rule.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 16 · 2026-10-19T20:10Z
Session:
session_01Nk17Z26X5DB59iQztDBADH(the dispatching seat; the dev runsmode:subagentin this session's container and shares its session ID, so its identity is the branch)Account:
marchtian(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-24614-system-run-user-grantsWorktree:
objectstack-issue-24614Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)File surface (at
origin/main14ccb1da9or later; stop on a breach and explain it in the report):packages/services/service-automation/src/runtime-identity.ts:resolveRunDataContext'srunAs:'system'arm (the emptypositions/permissionsbeside a carrieduserId,:284on14ccb1da9), and its TSDoc.packages/services/service-automation/src/engine.ts: only where the system arm's shape is built or relied on, for example the post-decision system segment from PR feat(automation)!: an approval decision resumes a flow as the person who decided #24527.crud-runas.test.tspin of the old shape, and a real-stack pin besiderunas-user-hook-org-peers.integration.test.ts. And a changeset.Exclusions:
isSystem: trueand theactorstay as they are. Only the empty arrays leave, and only when a user is named.packages/objectql/**: the hook seed (SEED_USER_GRANTS) is the consumer, and its email twin is objectql: a runAs:'user' hook fired from a hand-built system context gets no email, so every current_user.email row policy hides rows its user can read (the email twin of #24497) #24639 (domain:engine). ⛔packages/core/**.plugin-approvals: plugin-approvals: the hand-built contexts its writes run under don't fully carry the deciding user's context, so a runAs:'user' hook they fire gets neither the user's organisation peers nor the user's own grants (the producer half of #24497) #24519 (seat 2) holds the approvals producers under the same rule.resolveRunDataContext'srunAs:'user'arm: PR fix(service-automation): a runAs:'user' flow carries its user's email, so an email-keyed row policy admits the run as it admits the user's own request #24636 (service-automation: a runAs:'user' flow run carries no email, so every current_user.email row policy denies the run on rows its triggering user can read and write #24613) just landed there.Container & model:
S–M: one arm of one producer, both-direction pins on a real stack,mode:subagent,model: opus(TIER_DEFAULT;--tierread no path-derived mandate). It is declared a narrowing, so oneCONTRACT_REVIEW_TIERreview of record is owed on the final head before the queue.Clause-②: no (narrowing). A
runAs:'user'hook fired by a user-triggeredrunAs:'system'flow's write now runs on that user's own grants instead of the member baseline. For a user whose grants sit below the baseline, as triage's "both directions" names, the hook's reads narrow. plugin-approvals: the hand-built contexts its writes run under don't fully carry the deciding user's context, so a runAs:'user' hook they fire gets neither the user's organisation peers nor the user's own grants (the producer half of #24497) #24519's ruling6271152775item 1 declares no arm for its own producer. This seat takes the stricter, narrowing arm until the dev's measurement says whether any principal's grants can sit below the baseline. If none can, the seat re-grades and drops the arm.Responsibility:
service-automation'sresolveRunDataContextsystem arm, which pins empty arrays beside a named user | the platform path is PR fix(objectql): a runAs:'user' hook sees its organization's members in sys_user, as the user's own request does #24518's hook seed, which resolves a named user's own grants when the context carries no arrays, and keeps carried arrays verbatim | reached by everyrunAs:'user'hook fired by a write of a user-triggeredrunAs:'system'flow, including the post-decision nodes of everyrunAs:'system'approval flow (ruling6267719791Q1 A); live today.Thread-read: 6272864997
Serial constraints cleared: PR fix(service-automation): a runAs:'user' flow carries its user's email, so an email-keyed row policy admits the run as it admits the user's own request #24636 (service-automation: a runAs:'user' flow run carries no email, so every current_user.email row policy denies the run on rows its triggering user can read and write #24613,
runtime-identity.ts/engine.ts) merged as14ccb1da9, the order the serial note6272864997set. A file scan of the 12 open PRs at this claim: none touchesservice-automationsource. plugin-approvals: the hand-built contexts its writes run under don't fully carry the deciding user's context, so a runAs:'user' hook they fire gets neither the user's organisation peers nor the user's own grants (the producer half of #24497) #24519 (seat 2,plugin-approvals) applies the same rule to a different producer and touches no file here.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{ "issue": 24614, "status": "blocked", "branch": "claude/issue-24614-system-run-user-grants", "pr": "https://github.com/objectstack-ai/objectstack/pull/24658", "session": "session_01Nk17Z26X5DB59iQztDBADH — the dispatching seat's id; this dev ran mode:subagent and shares it, its identity is the branch", "premise_still_valid": true, "summary": "resolveRunDataContext's runAs:'system' arm no longer carries positions: [] / permissions: [] beside a named user, so a runAs:'user' hook fired by such a run's write resolves that user's own grants through the PR #24518 seed; isSystem, actor, userId and tenantId are unchanged, and a user-less system run keeps today's envelope. Real-stack pins cover both directions, the decider after an approval decision, and the user-less control, and one ablation turned them red. BLOCKED on one scope decision: the change necessarily flips two assertions in packages/plugins/plugin-approvals/src/restored-approval-continuation.test.ts, which the claim excludes, so Test Core stays red on those two cases until the seat widens the file surface (see blocked_on). Head bf00bb57c, stable.", "blocked_on": "Widen the claim's file surface to packages/plugins/plugin-approvals/src/restored-approval-continuation.test.ts, or route the edit elsewhere. Two assertions there pin the old system-arm shape: the parked() helper's runAs:'system' expectation and the post-decision expectation of the runAs:'system' approve/reject case both expect permissions: [] beside the named user. The edit is to expect { userId, isSystem: true } with no permissions key. Measured: plugin-approvals vitest run, 2 failed / 1118 passed, the 2 being exactly those cases. Resume-from: claude/issue-24614-system-run-user-grants@bf00bb57c.", "tests": "At 9debd436b..bf00bb57c, every run through os-verify-lock.sh. service-automation vitest run (at 422c6294d; the merge brought no change to the package): 194 files / 2587 tests passed, VERDICT command-exit 0. service-automation typecheck (tsc --noEmit plus check:test-typecheck): exit 0. New file runas-system-user-hook-grants.integration.test.ts: 5/5 (GUARD, above, below, decider, user-less CONTROL). Importers built from this tree: trigger-record-change 114/114, trigger-schedule 174/174, verify six system-run files 70/70, plugin-approvals 1118 passed / 2 failed (the blocked_on cases). Ablation, one run on committed 59575b2c4 via scripts/ablation-replace.mjs: anchor 1 to 0, blob 4817a333 to bc398da1; new file plus crud-runas.test.ts 5 failed / 39 passed, the predicted direction: the three flow-driven real-stack pins and the two named-user shape pins red, GUARD, the user-less CONTROL and the user-less unit pin green. Restore proven: blob 4817a333 equals HEAD, git diff HEAD empty, porcelain empty.", "gates": "Local: dispatch-gates.mjs --commands on this diff derived 68 commands, identical to the seat's list; all 68 run with exit captured before any pipe; --ran: 68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN (exit 0) at bf00bb57c. check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET, unbuilt packages), then exit 0 after a full turbo build; the record carries the re-run. CI on PR head bf00bb57c, one read at open: 36 check-runs, none failed yet; completed success: Check PR Size, No other open PR may claim the same issue, Part-of PR must not also close its card, filter; skipped: Build Docs, Console Pin Gate, Dogfood shards, Packed-tarball smoke, Temporal Conformance; in_progress or queued: Build Core, Test Core 1-6, Check Changeset, Governed Surface Queue Guard, all Lint and Type Check lanes. Test Core is expected red on the two blocked_on cases.", "mcp_calls": "0", "api_writes": "3 REST writes in 2 fleet-relay strokes: POST /repos/objectstack-ai/objectstack/pulls and POST /repos/objectstack-ai/objectstack/issues/24658/assignees (pr_create, request fw-20261019T212754Z-3b2fc1, read back identical, assignee marchtian); POST /repos/objectstack-ai/objectstack/issues/24614/comments (this report). Plus 6 git pushes, not REST. Zero label writes: no label named, a changeset is present.", "open_questions": [], "out_of_scope_findings": [ "carrier: #24639 · noted, not filed — the objectql hook seed resolves positions, permission sets and peers but no email, so a row policy on current_user.email in the user's own sets drops out in a runAs:'user' hook this change now seeds (code-read)." ], "clause2_premise": "Held, measured on the real stack: a principal whose own grants read fewer rows than the member baseline exists, and its hook read narrows. The principal's grant detail is in the seat copy of this report only.", "measurement": "Above: the hook now reads what the user's own request reads (old envelope: PERMISSION_DENIED 403). Below: pinned, detail in the seat copy. Decider: the submit write's hook runs on the submitter's grants, the post-decision write's hook on the decider's. CONTROL user-less: envelope keeps positions: [] and permissions: [] with no userId, and the hook is refused HOOK_UNSCOPED_DATA_ACCESS 403.", "ablation": "One run, restore proven by blob and git diff HEAD; see tests.", "files_changed": [ ".changeset/24614-system-run-user-grants.md", "packages/services/service-automation/src/runtime-identity.ts", "packages/services/service-automation/src/engine.ts", "packages/services/service-automation/src/builtin/crud-runas.test.ts", "packages/services/service-automation/src/runas-grant-resolution.integration.test.ts", "packages/services/service-automation/src/resume-decider-identity.integration.test.ts", "packages/services/service-automation/src/runas-system-user-hook-grants.integration.test.ts" ], "line_budget": "+408 / -23 over 7 files (431 changed lines, under the 3000 human-merge threshold): source +50 / -16, tests +336 / -7, changeset +22. No skills/** or governed surface touched.", "deviations": [ "Breach not taken: the plugin-approvals test edit is required for a green Test Core and is left to the seat (blocked_on).", "This GitHub copy omits the below-baseline grant detail the dispatch keeps off GitHub; the seat copy carries it in measurement and clause2_premise. Otherwise the two copies are the same.", "The above-baseline ablation red came from its envelope assertion (the write carried permissions) before its read assertion; not re-ablated, to keep one ablation.", "The service-automation full suite ran at 422c6294d, before the merge of origin/main 7c7741dcb, which touched core datetime, objectql aggregation, plugin-audit and service-storage only; per AGENTS.md section 10 it was not re-run. Merge-touched packages were rebuilt and every gate ran at bf00bb57c.", "cli, client and connector-* tests not run locally: none of their tests runs a system flow with a user (cli's three runAs:'system' hits are lint and format tests); CI Test Core runs them.", "The PR body's Clause-② line is the claim's line verbatim, as the contract asks; the measurement upholds its narrowing arm." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{ "issue": 24614, "status": "done", "round": "patch round 1, answering the seat's rework record 6277856952 on PR #24658", "branch": "claude/issue-24614-system-run-user-grants", "head": "0f452e832", "pr": "https://github.com/objectstack-ai/objectstack/pull/24658", "session": "session_01Nk17Z26X5DB59iQztDBADH — the dispatching seat's id; this dev ran mode:subagent and shares it, its identity is the branch", "premise_still_valid": true, "summary": "Resumed from claude/issue-24614-system-run-user-grants@bf00bb57c (the remote head was unchanged) in a re-created worktree. In packages/plugins/plugin-approvals/src/restored-approval-continuation.test.ts I changed exactly the two expectations the rework record names, the parked() helper's runAs:'system' one and the post-decision one of the runAs:'system' approve/reject it.each, to { userId, isSystem: true } with no permissions key (+2 / -2, commit 678acbb8c); nothing else in plugin-approvals changed. origin/main had moved to f89d29e38 (formula and cli only), so I merged it with scripts/pm/os-regen-merge.sh (merge 0f452e832, no regen deferral owed, no rebase, amend or force-push). The plugin-approvals suite now passes in full, the 70 re-derived gate families are reconciled, and CI on 0f452e832 concluded green. The PR stays draft; the full replacement body is in the seat's scratchpad for the seat to write.", "tests": "At 0f452e832, every run through os-verify-lock.sh after a full turbo build (76/76). plugin-approvals vitest run against this tree's built service-automation (dist carries the new arm, grep count 1): 83 files / 1132 tests passed, 0 failed. Of those, 1120 are the tests my first measurement counted (1118 passed plus the 2 cases), and 12 are the two mirror files that could not load then (unbuilt trigger-record-change); every file loads now. plugin-approvals typecheck (tsc, tsc -p tsconfig.scripts.json, check:test-typecheck) exit 0, its debt ledger holding. service-automation vitest run: 194 files / 2587 tests passed. The targeted runAs files (the new real-stack file, crud-runas, runas-grant-resolution, resume-decider-identity, runas-user-hook-org-peers): 75/75. The round-0 ablation stands (no tested behaviour changed this round, so no new ablation).", "gates": "Local: dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on the final diff (8 paths vs merge base f89d29e38, at 0f452e832) derived 70 commands: round 0's 68 plus check:i18n and check:i18n-stale-fill, which the plugin-approvals path brings. All 70 exited 0 with exit captured before any pipe. --ran: 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. CI on PR head 0f452e832, one read, already concluded: 40 check-runs, 34 success and 6 skipped, 0 failed. The five required contexts all succeeded: Lint & Repo Gates, TypeScript Type Check, Test Core (1-6), Build Core and Governed Surface Queue Guard, plus Check Changeset. Skipped: Build Docs, Console Pin Gate, Dogfood shards, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance.", "mcp_calls": "0", "api_writes": "1 REST write in 1 fleet-relay stroke: POST /repos/objectstack-ai/objectstack/issues/24614/comments (this report). Plus 2 git pushes (678acbb8c, then the merge 0f452e832), not REST. The PR body was not patched: the full replacement is written to the seat's scratchpad file pr-body-r1.md for the seat to send. Zero label writes.", "open_questions": [], "out_of_scope_findings": [ "carrier: #24639 · noted, not filed — unchanged from round 0: the objectql hook seed resolves no email, so a row policy on current_user.email in the user's own sets drops out in a runAs:'user' hook this change now seeds (code-read)." ], "clause2_premise": "Held, unchanged from round 0: a principal whose own grants read fewer rows than the member baseline exists, and its hook read narrows; Clause-②: no (narrowing) stands. The grant detail is in the seat copy only.", "measurement": "Unchanged from round 0, and re-run green on 0f452e832 (the new real-stack file is in the 75/75): above, below, decider, and the user-less control. Below-baseline detail in the seat copy only.", "ablation": "Round 0's single ablation stands (commit 59575b2c4, restore proven by blob and git diff HEAD). This round changed only two test expectations and merged main, so no tested behaviour moved and no ablation is owed.", "files_changed": [ ".changeset/24614-system-run-user-grants.md", "packages/plugins/plugin-approvals/src/restored-approval-continuation.test.ts", "packages/services/service-automation/src/runtime-identity.ts", "packages/services/service-automation/src/engine.ts", "packages/services/service-automation/src/builtin/crud-runas.test.ts", "packages/services/service-automation/src/runas-grant-resolution.integration.test.ts", "packages/services/service-automation/src/resume-decider-identity.integration.test.ts", "packages/services/service-automation/src/runas-system-user-hook-grants.integration.test.ts" ], "line_budget": "+410 / -25 over 8 files (435 changed lines, under the 3000 human-merge threshold). This round: +2 / -2 in the one plugin-approvals test file. No skills/** or governed surface touched.", "deviations": [ "The plugin-approvals total is 1132, not 1120; the difference is the 12 tests in the two mirror files that could not load in my first measurement. None failed.", "AGENTS.md section 10 did not require it, but I re-ran service-automation's full suite on the final head, since main's merge moved formula, which flows evaluate through.", "The PR carries labels documentation, size/m, tests and tooling, set by the labeler workflows, not by me; I left them untouched.", "pr-body-r1.md keeps the session-URL footer the stored body already carries (one footer, written by me at create, not platform-appended). The seat should send it as is, without adding another." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
ACCEPT → PR #24658 comment
6278876565·domain:servicesseat 1 (#6021) ·session_01Nk17Z26X5DB59iQztDBADH· 2026-10-19T22:36Z. Head0f452e832b. Contract review PASS6278843703atCONTRACT_REVIEW_TIER, on the narrowing face. Every check on the head is green or a roster skip. It lands through the merge queue, andFixes #24614closes this card at the merge.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Landed ·
domain:servicesseat 1 (#6021) ·marchtian·session_01Nk17Z26X5DB59iQztDBADH· 2026-10-19T23:09Z. PR #24658 merged through the queue as8fd3944a2at 2026-10-19T23:08Z.Fixes #24614closed this card ascompleted.main:git merge-base --is-ancestor 8fd3944a2 origin/mainexits 0, and the controlf89d29e38exits 0. The PR head0f452e832bexits 1, as a squash merge should. All 8 paths are blob-equal to the PR head.pm:dispatchedis removed and the card is unassigned (marchtian). The read-back matched.runAs:'system'flow's writes name the user without empty grant arrays, so arunAs:'user'hook they fire resolves that user's own grants, as ruling6271152775sets for the family.6278843703, on the narrowing face; ACCEPT6278876565.6277888374names is now open: until objectql: a runAs:'user' hook fired from a hand-built system context gets no email, so every current_user.email row policy hides rows its user can read (the email twin of #24497) #24639 (PR fix(objectql): a runAs:'user' hook carries its user's email, so a current_user.email row policy admits it as it admits the user's own request #24674,domain:engine) seeds the email in the hook path, an email-keyed row policy in the user's own sets fails closed in such a hook. It does not leak.Generated by Claude Code