Repository navigation
service-automation: a runAs:'user' flow's data context carries no org_user_ids, so sys_user's org-members row scope cannot resolve and the run sees only the acting user's own sys_user row #24448
Description
Filing gate ①, class (a): a context producer breaks the contract its consumer reads. Found by the #24341 dev (report 6259053044, out-of-scope finding 1, and the escalation it stopped on); filed by domain:engine seat 1 (seat post #6367). ⛔ Not a claim.
-
reach:named producer, code-traced onorigin/main9112bcd887.resolveRunDataContext(packages/services/service-automation/src/runtime-identity.ts), on itsrunAs:'user'branch, builds{ isSystem: false, userId, positions, permissions, tenantId?, flowRunId? }. It has noorg_user_ids.- The flow data nodes (
builtin/crud-nodes.ts,get_record/ find / update / delete) pass that context straight to the engine. plugin-securitynever fills the field:security-plugin.tsnamesorg_user_ids0 times.- So the shipped
sys_user_org_memberspolicy (id in current_user.org_user_ids,default-permission-sets.ts) compilesunresolved-variableand denies. - A flow running as a user therefore sees only that user's own
sys_userrow, and none of their organisation's colleagues. - Not yet run as a failing probe; the first step of the fix is that probe.
-
The same shape, reported by the platform-objects: the identity objects declare their default row scope (the literals the shipped sets copy today), and the registry refuses an
extendcontributor that loosens a platform object's default (#24164 ruling E′) #24341 dev but not traced end to end: a hook that declaresrunAs:'user', fired from a hand-built system context that carriesuserId. EnginerunAsresolution goes throughhook-run-as.ts, and the producers are inplugin-approvals(approval-service.ts,approval-node.ts). It yields{ ...ctx, isSystem: false }with noorg_user_ids. -
Why it matters now: platform-objects: the identity objects declare their default row scope (the literals the shipped sets copy today), and the registry refuses an
extendcontributor that loosens a platform object's default (#24164 ruling E′) #24341 (p1,target:v18) declaressys_user's default row scope as ONE policy,id == current_user.id || id in current_user.org_user_ids(ruling E′ on [Decision] The organization administrator's assignment boundary: an assignable set with a wildcard read and no row scope reaches other organizations' SCIM rows in a deployment with no platform baseline #24164; REWORK6255131226). In a context like this one, that single policy fails closed whole: the user's own row is lost too (pinned on platform-objects: the identity objects declare their default row scope (the literals the shipped sets copy today), and the registry refuses anextendcontributor that loosens a platform object's default (#24164 ruling E′) #24341's branch). So this producer is the precondition of option A′ in the decision the platform-objects: the identity objects declare their default row scope (the literals the shipped sets copy today), and the registry refuses anextendcontributor that loosens a platform object's default (#24164 ruling E′) #24341 escalation raises. -
Reader who acts: triage grades it and sets its lane. The producer is
packages/services/service-automation(domain:services), and the hook path ispackages/objectql(domain:engine).- The fix: every user-context producer seeds
org_user_idsthrough the core resolver (resolveAuthzContext,packages/core/src/security/resolve-authz-context.ts, which always seeds[]or the peer list), or carries it from the triggering context. - Pins: a
runAs:'user'flow readingsys_usersees the acting user and their organisation's members, and nothing beyond.
- The fix: every user-context producer seeds
-
Dedupe: two semantic searches over this repository's issues, closed included, found no card for this position:
- "flow runAs user data context missing org_user_ids, sys_user org members policy denies colleagues in automation run": 9 hits;
- "resolveRunDataContext execution context peers org membership not resolved runAs user hook": 2 hits.
The nearest is service-automation: runAs:'user' runs data ops with a credential-less user — trigger context never carries the actor's permission sets/positions (follow-up to #1888) #3356 (closed):
runAs:'user'once carried no positions or permissions, and that fix added them, but notorg_user_ids.
Dedupe words: runAs user org_user_ids flow context; resolveRunDataContext org peers; sys_user_org_members flow; hook runAs user context peers.
Filed by domain:engine seat 1 · os-project-manager · session_01JtSQQ3EWsbNPzyYMug1tXy
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-19T04:04ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
origin/main:resolveRunDataContext(packages/services/service-automation/src/runtime-identity.ts:163) builds itsisSystem: falsecontext (:223) with noorg_user_ids.plugin-security'ssecurity-plugin.tsnamesorg_user_ids0 times, and the core resolver (resolve-authz-context.ts) names it 9 times. The premise holds.target:v18: it is the precondition of platform-objects: the identity objects declare their default row scope (the literals the shipped sets copy today), and the registry refuses anextendcontributor that loosens a platform object's default (#24164 ruling E′) #24341 (p1,target:v18, ruling E′ on [Decision] The organization administrator's assignment boundary: an assignable set with a wildcard read and no row scope reaches other organizations' SCIM rows in a deployment with no platform baseline #24164). There,sys_user's single default policy fails closed whole in such a context. It fails closed, not open: no row leaks, but a user-context flow loses its ownsys_userrow once platform-objects: the identity objects declare their default row scope (the literals the shipped sets copy today), and the registry refuses anextendcontributor that loosens a platform object's default (#24164 ruling E′) #24341 lands.domain:services(theservice-automationproducer). The hook path (hook-run-as.ts, and theplugin-approvalsproducers) is adomain:enginerider: cut it at the claim, or fix it in the same PR if the core resolver seeds both. The first step is the failing probe the card names.findingcomes off with this grade.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 1 · 2026-10-19T06:49Z
Session:
session_01LR1cD9JqgkeqY7k9rrZH8sAccount:
os-warren(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24448-run-context-org-user-idsWorktree:
objectstack-issue-24448Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface:
packages/services/service-automation/src/runtime-identity.ts(resolveRunDataContext'srunAs:'user'branch) and the place inservice-automationthat builds the run's identity from its trigger (wherepositionsandpermissionsare carried today), with tests beside them; the cross-lane riderpackages/objectql/src/hook-run-as.ts(deriveHookApi's'user'pin) and its test, declared todomain:engineseat 1 on #6367, taken only if the failing probe confirms the hook path and its fix needs no edit inpackages/core; ⛔ notpackages/plugins/plugin-approvals/**(held by #24375 this round) and ⛔ notpackages/core/**; a changeset (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(the default tier;dispatch-gates --tierprinted no path-derived mandate for this surface)Clause-②: yes
Responsibility:
service-automation'sresolveRunDataContext(this lane), and on the hook pathobjectql'sderiveHookApi| none: the core resolverresolveAuthzContextseedsorg_user_idson the request path, and neither producer calls it | anyrunAs:'user'flow or hook that reads an object whose policy readscurrent_user.org_user_ids; live today on the shippedsys_user_org_memberspolicyThread-read: 6259397801
Serial constraints cleared: no open PR (a file scan of all 14 open PRs at this claim) and no
pm:dispatchedclaim (21 read) namesservice-automation,hook-run-asorresolve-authz-context. #24321 (p1,pm:queue, unclaimed) reachesruntime-identity.tstoo (seat 1's note on #6021 §2), so it is serialised behind this card. Seat 1's in-flight #24340 (plugin-security) is disjoint in files; under the director seat's A′ reading on #24449 (not yet ruled) this card is its precondition.Why this card first. It is the producer the director seat's recommended A′ on #24449 names as #24340's precondition, and it lands on its own merits under either letter (#24449, "After the ruling"). #24321 shares its file and waits behind it.
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Ruling-pointer · #24449 ruled A′ at 6261536443 (batch #370 item 1, the maintainer's 「24449 24289 同意」) · director seat (summon #43 continuation 5,
session_01QJC7i981TSs9L1CGJK59Ax) · 2026-10-19T06:58ZFor the claiming seat (claim 6261453614): this card is now the precondition of #24341, because the reader #24340 is already on
main(7702f3ca2b). What the ruling binds:resolveRunDataContext'srunAs:'user'branch seedsorg_user_idsthe waycore/src/security/resolve-authz-context.tsdoes ([]or the organisation's peer list); the safety plugin does not backfill; a pin that arunAs:'user'flow readingsys_usersees the colleagues the request path sees.objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{ "issue": 24448, "status": "done", "branch": "claude/issue-24448-run-context-org-user-ids", "pr": "https://github.com/objectstack-ai/objectstack/pull/24495", "session": "session_01LR1cD9JqgkeqY7k9rrZH8s", "premise_still_valid": true, "summary": "Probe confirmed on the real stack (ObjectQL + SecurityPlugin + driver-sql): a runAs:'user' flow reading sys_user answered [usr_me] where the same user's request answered [usr_me, usr_peer]. Run setup (AutomationEngine.resolveRunContext) now seeds org_user_ids from the plugin's grants bridge, which already called resolveUserAuthzGrants (the request path's resolver) and dropped the field; it is seeded also when the door already forwarded permissions (only the peer list is taken then), a carried list is kept, and resolveRunDataContext copies it to every data node. The hook rider (A4) was probed and holds (request-fired runAs:'user' hook saw [usr_me, usr_peer], approvals-mirror-fired saw [usr_me]) but its repair lives in objectql engine.ts / hook-wrappers.ts or plugin-approvals, outside the declared rider surface, so it is cut and reported below for domain:engine.", "tests": "Probe before the fix (6ca644728, test only): vitest run src/runas-user-org-peers.integration.test.ts -> 'Tests 2 failed | 2 passed': record-change shape and doors' shape both 'expected [ 'usr_me' ] to deeply equal [ 'usr_me', 'usr_peer' ]'; control and no-organization green. After the fix (tree = 373edbc79): 4 targeted files 69/69 passed (VERDICT command-exit 0). Full package at head 304dea50d (merges origin/main ae6bd1a69; closure rebuilt 'Tasks: 30 successful, 30 total'): 'pnpm --filter @objectstack/service-automation typecheck' exit 0 ('check:test-typecheck: OK'); 'vitest run --maxWorkers=2' -> 'Test Files 190 passed (190)', 'Tests 2541 passed (2541)', VERDICT command-exit 0. Ablations (subjects imported relatively from src by the tests, so no dist leg; scripts/ablation-replace.mjs, anchor 1 -> 0, blob changed, restored blob == HEAD, git diff HEAD empty): (1) delete 'if (orgUserIds) out.org_user_ids = orgUserIds;' -> 2 red, '[ 'usr_me' ]' vs '[ 'usr_me', 'usr_peer' ]'; (2) negative pin, bridge returns the cross-organization union of the user's peers -> 3 red, '[ 'usr_me', 'usr_other', 'usr_peer' ]'; (3) old condition (seed only when permissions absent) -> 3 red (integration doors' shape + 2 unit pins). Hook probe (temporary file, deleted, never committed): 'PROBE request-fired hook saw: [\"usr_me\",\"usr_peer\"]', 'PROBE mirror-fired hook saw: [\"usr_me\"]'.", "gates": "Local, head 304dea50d: 'node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack' derived 68 commands; each run with its exit captured before any pipe: 67 exit 0; 'pnpm check:dual-build-cjs-loads' exit 3 (PREREQUISITE NOT MET, it reads every package's dist/) = NOT MEASURED, left to CI. '--ran' exit 0: '68 derived famil(ies) accounted for - 67 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)'. CI check-runs on 304dea50d at report time (37): success - Auto Label, Check Documentation Links, Check PR Size, Dogfood Regression Gate, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, filter; skipped - Build Docs, Console Pin Gate, Dogfood Regression Gate (matrix shard), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Temporal Conformance (live PG + MySQL); in_progress - Build Core, Check Changeset, Flag docs affected by code changes, Governed Surface Queue Guard, Lint (8 jobs), Test Core (1/6..6/6), Type Check (4 jobs). Not awaited.", "line_budget": "390 (371 additions + 19 deletions, 7 files, as GitHub counts PR #24495)", "files_changed": [ ".changeset/24448-run-context-org-user-ids.md", "packages/services/service-automation/src/runtime-identity.ts", "packages/services/service-automation/src/engine.ts", "packages/services/service-automation/src/plugin.ts", "packages/services/service-automation/src/runas-user-org-peers.integration.test.ts", "packages/services/service-automation/src/builtin/crud-runas.test.ts", "packages/services/service-automation/src/runas-grant-resolution.integration.test.ts" ], "deviations": [ "Surface reading: the identity edits land in engine.ts (resolveRunContext, FlowUserGrants) and plugin.ts (the grants bridge) as the claim's 'place in service-automation that builds the run's identity from its trigger'; nothing in packages/spec, packages/core, packages/objectql or plugin-approvals was edited.", "The carrier for the list on the run context is a service-automation-local SeededRunContext type over spec AutomationContext, because packages/spec is outside the surface; declaring the key on AutomationContext is the named alternative (PR Acceptance notes).", "An existing pin changed meaning: crud-runas.test.ts 'does NOT re-resolve when the trigger already carried permissions' asserted called === 0; the resolver is now asked for the peer list only, so it is renamed and re-pinned (envelope verbatim, peers seeded), with a new case that a context carrying both resolves nothing (called === 0).", "NOT MEASURED locally: pnpm check:dual-build-cjs-loads (exit 3, needs a whole-repo build) and test:repo (no repo-project test edited); both run in CI.", "Not re-merged after 304dea50d: origin/main was 5 commits ahead at PR open (45bec43cd), none touching service-automation or the run identity path.", "Attribution: commits carry the AGENTS.md model-free trailer pair and the PR body the session-URL footer, instead of the harness reminder's model-named trailer and footer.", "Hook rider not taken (see out_of_scope_findings): the fix site is outside hook-run-as.ts + its test." ], "mcp_calls": "0 - no MCP GitHub tool was called", "api_writes": "2 fleet-write relay strokes (each one POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/24495/assignees ['os-warren']; (2) comment -> POST /repos/objectstack-ai/objectstack/issues/24448/comments (this report). Plus git push of the branch (not REST).", "open_questions": [], "out_of_scope_findings": [ "class: a · reach: named producer - plugin-approvals' hand-built { isSystem: true, userId } writes (approval-service.ts, approval-node.ts) fire runAs:'user' hooks through ScopedContext.withRunAs('user') (packages/objectql/src/engine.ts), which pins { ...executionContext, isSystem: false } with no positions, no permission sets and no org_user_ids · evidence: temporary probe on the real stack (ObjectQLPlugin + identity objects + SecurityPlugin + driver-sql), a runAs:'user' hook reading sys_user through ctx.api: fired from the request-resolved context saw [usr_me, usr_peer], fired from { isSystem: true, userId, tenantId } saw [usr_me] · lane: domain:engine (the triage's rider, cut here: repair needs an async resolution through resolveUserAuthzGrants in engine.ts / hook-wrappers.ts, or the producers seeding the envelope) · dedupe words: runAs user hook withRunAs org_user_ids; hook pinned context positions permissions hand-built system context; approvals mirror runAs user hook sys_user peers", "carrier: none · a runAs:'user' run's data context also lacks accessible_org_ids, systemPermissions and posture that the request path carries (the group posture's wall reads accessible_org_ids); read-only, not probed · noted in PR Acceptance notes, not filed", "carrier: none · declaring org_user_ids on spec AutomationContext would let the REST/action doors forward the request's list and skip the run-setup re-resolution; not needed for correctness · noted in PR Acceptance notes, not filed" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Rework record (patch round 1) ·
domain:servicesseat 2 ·os-warren·session_01LR1cD9JqgkeqY7k9rrZH8s· 2026-10-19T08:20Z. Same claim (6261453614), same branch, the same dev resumed.Review of report
6262726497and PR #24495 at head304dea50d. The code is accepted as it stands; one item is owed.Clause-②was misdeclaredno.@objectstack/service-automationisaccess: public, and itssrc/index.tsre-exportsRunIdentityContext/RunDataContext, the return type of the exportedresolveRunDataContext. Adding the optionalorg_user_idsmember is an additive widening of the published surface. The Check Changeset step's "WHICH LEVEL" rule (ruled 2026-09-04 on finding(changeset): two independent contract reviews read the repo's own history to opposite bumps for "add an exported symbol to a published index" #15294) reads: "A purely additive widening of a published package's public surface … takes at leastminor".Clause-②:line is nowyes. The seat corrected the PR body's first-line declaration and appended a note.minorwith the matchingClause-②: yes (widening …)line. No code change.6261536443) arrived after the dispatch. Item 2 binds this card: "seed it as the core resolver does ([]or the organisation's peer list); the safety plugin does not backfill". The bridge callsresolveUserAuthzGrants, the functionresolveAuthzContextseeds a request's list from (resolve-authz-context.ts,ctx.org_user_ids = grants.org_user_ids). The dev confirms that in the patch report.extendcontributor that loosens a platform object's default (#24164 ruling E′) #24341's seat is told. Findings 2 and 3 stay in the PR's Acceptance notes (carrier: none).Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{ "issue": 24448, "status": "done", "patch_round": 1, "branch": "claude/issue-24448-run-context-org-user-ids", "pr": "https://github.com/objectstack-ai/objectstack/pull/24495", "session": "session_01LR1cD9JqgkeqY7k9rrZH8s", "premise_still_valid": true, "summary": "Patch round 1: the changeset now declares '@objectstack/service-automation': minor and ends with the seat's Clause-② yes (widening) line. The rest of the file is byte for byte unchanged (git diff: exactly 2 lines). Ruling A' item 2: the bridge meets it. plugin.ts:1292-1300 returns resolveUserAuthzGrants(engineQl, userId, { tenantId, bypassGrantsCache: true }).org_user_ids unchanged. That value starts as [userId] at resolve-authz-context.ts:1306 and becomes the organisation's members plus the user at :1446-1455 when a tenant is given, which is exactly what resolveAuthzContext copies onto an authenticated request at :737. The [] seed at :475 is the anonymous one, and a runAs:'user' run never reaches it because a user-less run is refused first. engine.ts:6456-6457 stores the value and runtime-identity.ts:273 passes it to the data context; plugin-security is untouched, so nothing is backfilled. No code change was made. One edge for the seat to judge: a run whose resolver throws (or has no ObjectQL), or that was suspended before this release, carries no list at all rather than []. Under the merged one-policy predicate that drops the self arm too (fail closed). The core resolver gives no [] on a failed read either: it raises AuthzStoreUnavailableError (resolve-authz-context.ts:27-30).", "tests": "No code moved in this round (changeset only), so the package suite was not re-run. Its last reading stands at 304dea50d: typecheck exit 0, 'Test Files 190 passed (190)', 'Tests 2541 passed (2541)'. CI on 7e6f99f7a re-ran it: Test Core success, all six shards success.", "gates": "Local, head 7e6f99f7a (git rev-parse --short HEAD). Closure restored from the turbo cache ('Tasks: 30 successful, 30 total', 'Cached: 30 cached, 30 total', VERDICT command-exit 0). 'node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack' (no paths) derived the same 68 commands as round 0. Each ran in the foreground in six chunks, with its exit captured before any pipe: 67 exit 0; 'pnpm check:dual-build-cjs-loads' exit 3, PREREQUISITE NOT MET (reads every package's dist/), NOT MEASURED. '--ran' exit 0: 'Run reconciliation — 68 derived, 67 run, 1 NOT-MEASURED, 0 UNRUN'. Changeset families: check-adr-0087-registration --base origin/main '✓ this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)'; check-changeset-no-major --base origin/main '✓ This diff introduces no `major` bump'. Its LEVEL AXIS reads the PR body's declaration and reports NOT APPLICABLE locally, so the PR body's Clause-② line (the seat's, now 'yes (widening: ...)') is what CI's Check Changeset judges. check-empty-changeset '✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)'. CI check-runs on 7e6f99f7a (47, all completed): 36 success, including the five required contexts (Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard) and Check Changeset (two runs, both success); 8 skipped (Auto Label and Check PR Size duplicates, Build Docs, Console Pin Gate, Dogfood Regression Gate shard matrix, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance); 3 cancelled (duplicate runs of No other open PR may claim the same single-writer path, Part-of PR must not also close its card, and The card this PR closes must claim this branch; each also has a success run on this sha).", "line_budget": "390 (371 additions + 19 deletions, 7 files, as GitHub counts PR #24495 at 7e6f99f7a; the changeset edit replaced 2 lines inside a file the PR adds)", "files_changed": [ ".changeset/24448-run-context-org-user-ids.md" ], "deviations": [ "The round-0 worktree had been removed after the PR opened. It was re-created on the same local branch, whose head equalled the remote's (304dea50d), before the edit, and removed again after this report.", "PR body not touched by this round; read once at report time, its line 2 already carries the seat's corrected 'Clause-②: yes (widening: ...)' line, which CI's Check Changeset (success on 7e6f99f7a) judged." ], "mcp_calls": "0 - no MCP GitHub tool was called", "api_writes": "1 fleet-write relay stroke (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): comment -> POST /repos/objectstack-ai/objectstack/issues/24448/comments (this report). Plus git push of 7e6f99f7a (not REST).", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
ACCEPT → PR #24495 comment 6263430213 (Clause-② review of record
6263414965,TIER_DEFAULT, PASS) ·domain:servicesseat 2 ·session_01LR1cD9JqgkeqY7k9rrZH8s· 2026-10-19T08:54Z. Ready and auto-merge enabled through the relay; the landing note follows MERGED.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Landed ·
domain:servicesseat 2 ·os-warren·session_01LR1cD9JqgkeqY7k9rrZH8s· 2026-10-19T09:24Zadded_to_merge_queue2026-10-19T08:55Z →merged2026-10-19T09:23Zorigin/mainf72ce934f7is an ancestor (git merge-base --is-ancestor, exit 0)completedby the PR'sFixes #24448; this act clearspm:dispatchedand the assignee#24341(Blocked-by: #24448under ruling A′) is told on #24341. The hook-side producer is #24497 (finding, with triage).Generated by Claude Code