Skip to content

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

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24613-run-identity-email
Oct 19, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24613-run-identity-email

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24613

Clause-②: yes (widening). AutomationContext.email and the exported FlowUserGrants / RunIdentityContext types gain one optional member each. A run reaches rows its user's own request reaches, and nothing beyond.

What was wrong

A runAs: 'user' flow's data context carried no email. The RLS compiler binds current_user.email from the execution context's email and nothing else, so every row policy keyed on it (the stock showcase's task_own_rows, assignee == current_user.email) dropped out inside the run. A record-change flow triggered by a member whose only access to a row is such a policy was refused on that row, while the member's own request reads and updates it. The request path gets the email from the core resolver (resolveUserAuthzGrants, through resolveAuthzContext); the run called the same resolver and dropped the field at every step.

The change

  • The bridge passes the resolver's email on (plugin.ts). The grants resolver the plugin wires already called resolveUserAuthzGrants; it now returns grants.email beside the positions, permission sets and peer list. FlowUserGrants gains an optional email.
  • Run setup seeds it (seedRunGrants, engine.ts), by the rule the peer list uses: only what the context lacks. A runAs: 'user' context with a user and no email is resolved and takes the email; a context that already carries one keeps it verbatim (the request path keeps a session's email the same way). Forwarded positions and permission sets stay untouched.
  • The decider's segment carries the decider's email (resolveDecisionSegmentContext). It now removes the submitter's email with the submitter's other facts, on both postures, and seedRunGrants resolves the decider's. Without the removal the decider would run with the submitter's email under the decider's id.
  • resolveRunDataContext stays the single mapping (runtime-identity.ts). Its runAs: 'user' arm copies the email onto the data context (RunIdentityContext.email, optional). No email is invented when none was seeded. The runAs: 'system' arm is untouched.
  • Contract (packages/spec/src/contracts/automation-service.ts, a spec-lane rider): AutomationContext.email?: string with TSDoc naming who sets it, who reads it, and what its absence means. It regenerates no artefact: pnpm --filter @objectstack/spec check:generated reports all 14 generated artefacts up to date.
  • The [runAs] warning on a resolver failure names the email beside the peer list.
  • Docblocks that said no run context carries an email are corrected (runUserOf, the CEL scope note).

Measurement: the card's reproduction on the real stack

packages/services/service-automation/src/runas-user-email-owner.integration.test.ts, beside runas-user-hook-org-peers.integration.test.ts. It boots ObjectKernel, ObjectQLPlugin, driver-sql on better-sqlite3 :memory:, the identity objects, the real SecurityPlugin (shipped sets plus one fixture set: select and update on assignee == current_user.email) and the real AutomationEngine with its CRUD nodes. Runs start with the record-change trigger's own envelope (record, object, event, userId, tenantId, params). @objectstack/trigger-record-change depends on this package, so it is not composed here.

Before: the three producer files at c3954057f (git restore --source, each file's blob hash checked against the base blob), the new pin at head. After: head.

case before after
CONTROL: the member's own request reads and updates the row pass pass
GUARD: the same request without the email does not see the row pass pass
the card's reproduction (record-change run, read and stamp) fail: update_record(qa_owner_task) failed: Record ID not found in qa_owner_task pass, notes equals the member's id
the doors' shape (positions and permission sets forwarded, no email) fail, same message pass
nothing beyond: a row assigned to someone else's email pass (refused) pass (refused)
after a decision, the decider's row is stamped with the decider fail, same message pass
after a decision, the submitter's row is refused to the decider pass (refused) pass (refused)
CONTROL: a runAs: 'system' run pass pass

Before-leg totals: Tests 3 failed | 5 passed (8). After: Tests 8 passed (8). The card's showcase run reported the record-level 403 sentence; this stack answers the not-found that by-id writes of an unreadable row answer. Both are the refusal of a row the member can read.

Ablations (committed first, mutated through scripts/ablation-replace.mjs, each restored to the HEAD blob with git diff HEAD empty)

The subjects resolve through relative imports to src/, so no build sits between the mutation and the run.

  1. Drop the seed (if (needsEmail && grants.email) runContext.email = grants.email; deleted): 7 red across three files. The card's reproduction, the doors' shape and the decider's row on the real stack (Record ID not found), the decider envelope (after.email), and three unit pins in crud-runas.test.ts. Tests 7 failed | 59 passed (66).
  2. Keep the submitter's email in the decider's segment (delete switched.email; deleted): 6 red. On the real stack the submitter's row is written by the decider (expected true to be false) and the decider's own row is refused. In the envelope layer: expected 'rep@example.test' to be 'mgr@example.test', the failed-resolution case (expected 'rep@example.test' to be undefined), and both runAs: 'system' decision cases. Tests 6 failed | 23 passed (29).

Reverse type check: an AutomationContext literal with email: 42 planted in runtime-identity.ts gives TS2322: Type 'number' is not assignable to type 'string' under tsc --noEmit. So the type check reads the rebuilt .d.ts that declares the member. A stale one would report an unknown property. Restored to the HEAD blob.

Tests and gates (head 5318e5810, which merges origin/main at 973917c57)

  • pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2: Test Files 193 passed (193), Tests 2580 passed (2580).
  • pnpm --filter @objectstack/service-automation typecheck: exit 0 (tsc --noEmit, then check:test-typecheck: OK).
  • pnpm --filter @objectstack/spec typecheck: exit 0. vitest run --project local --maxWorkers=2 (the test script's runner): Test Files 701 passed (701), Tests 21312 passed | 1 todo. Spec run at 0663fe26c; the merge moved no file under packages/spec.
  • pnpm --filter @objectstack/spec check:generated: all 14 generated artefacts up to date.
  • Derived gates (node scripts/pm/dispatch-gates.mjs --commands, 90 commands) on 5318e5810: all 90 exit 0, each recorded with its exit code. --ran: 90 derived famil(ies) accounted for — 90 run, 0 NOT-MEASURED (0 UNRUN). An earlier run at 0663fe26c had one exit 3, check:dual-build-cjs-loads (PREREQUISITE NOT MET, it reads every package's dist/). It exited 0 once the dists were built, and on the final head it exits 0 on the first run.
  • check:dev-prereqs (local-only family; this diff touches packages/spec): exit 0.
  • test:repo is not run locally: the diff edits no repo-project test, and Test Core runs it.

Acceptance notes

  • The hook-side twin is not in this PR. ScopedContext[SEED_USER_GRANTS] (packages/objectql) seeds a runAs: 'user' hook's positions, permission sets and peer list, but not the email. It is outside this card's surface and is reported to the seat for the domain:engine lane. A hook fired by a runAs: 'user' flow's write now inherits the run's email through the write's context.
  • The flow CEL current_user still binds no email. The run context now holds one for a runAs: 'user' run. The binding follows the ruled list (id, positions, organization, the platform-admin flag), and the email serves the data operations' row policies. A pin in flow-cel-current-user.test.ts holds that. Whether the binding should widen is a question for the seat, not a change made here.
  • Persistence. A paused runAs: 'user' run's sys_automation_run.context_json now carries the user's email beside the positions and permission sets it already carried. That row's read is governed by sys_automation_run's own permissions.
  • Runs suspended before this release resume without the email (a resume does not re-run setup) and keep today's answer until they end.
  • Cost. A context that carries permissions and a peer list but no email now asks the resolver once, for the email alone. That shape is a subflow / map child of an email-less user, or an in-process caller that resolved the rest itself. Every door-started run already paid this resolution for the peer list.
  • Contract pin. No type-level pin file was added under packages/spec/src/contracts/: the claim's surface names only automation-service.ts there. The member's type is exercised by the service-automation test layer (check:test-typecheck) and by the reverse type check above.

Generated by Claude Code

…ack, and the decider's

Claude-Session: https://claude.ai/code/session_01Nk17Z26X5DB59iQztDBADH
Co-authored-by: Claude <noreply@anthropic.com>
…n-identity-email

Claude-Session: https://claude.ai/code/session_01Nk17Z26X5DB59iQztDBADH
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-automation, @objectstack/spec, touching 8 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx (via AutomationContext (symbol, a top-level interface))
  • content/docs/ui/actions.mdx (via AutomationContext (symbol, a top-level interface))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via AutomationContext (symbol, a top-level interface))
  • content/docs/releases/v16.mdx (via AutomationEngine (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via AutomationEngine (symbol, a top-level class))
  • content/docs/releases/v17/17-3.mdx (via AutomationContext (symbol, a top-level interface))
  • content/docs/releases/v17/17-5.mdx (via AutomationContext (symbol, a top-level interface))
  • content/docs/releases/v17/17-6.mdx (via AutomationEngine (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 143 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 973917c5795f7a440d46a15bc5cf8961af9d8703 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from ca573188e0ea75500eaf05a84dd5dea92cecaf24 — the merge of head 5318e581015920941957e2650d6115310041011d into base 973917c5795f7a440d46a15bc5cf8961af9d8703, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ca573188e0ea75500eaf05a84dd5dea92cecaf24 && git checkout ca573188e0ea75500eaf05a84dd5dea92cecaf24
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 973917c5795f7a440d46a15bc5cf8961af9d8703 5318e581015920941957e2650d6115310041011d && git checkout -B drift-repro 973917c5795f7a440d46a15bc5cf8961af9d8703 && git merge --no-ff 5318e581015920941957e2650d6115310041011d

node scripts/docs-audit/affected-docs.mjs --json 973917c5795f7a440d46a15bc5cf8961af9d8703

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 973917c5795f7a440d46a15bc5cf8961af9d8703 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5318e581015920941957e2650d6115310041011d
Local-runs: none
Scope: packages/spec/src/contracts/automation-service.ts + packages/services/service-automation/src/plugin.ts (the grants bridge), packages/services/service-automation/src/engine.ts (FlowUserGrants, resolveRunContext, seedRunGrants, resolveDecisionSegmentContext, runUserOf), packages/services/service-automation/src/runtime-identity.ts (RunIdentityContext, resolveRunDataContext), packages/plugins/plugin-security/src/rls-compiler.ts (the current_user.email binding, read only), packages/core/src/security/resolve-authz-context.ts (resolveUserAuthzGrants and the request path, read only), packages/runtime/src/domains/automation.ts buildAutomationContext and packages/runtime/src/action-execution.ts dispatchFlowAction (the two doors, read only), packages/triggers/trigger-record-change/src/record-change-trigger.ts (the record-change envelope, read only), packages/services/service-automation/src/builtin/subflow-node.ts and map-node.ts (child contexts, read only), packages/objectql/src/engine.ts SEED_USER_GRANTS (read only), the changeset and the four test files of the diff.

Read at 2026-10-19T19:29Z on PR #24636's head: card #24613 (its body, triage 6272563947, claim 6272823429 and the os-dev-report 6274876489), the PR body and its one comment (the advisory docs-drift check), the net diff against merge base 973917c5795f7a440d46a15bc5cf8961af9d8703 (9 files, +527 −41; the merge of main the head carries is not judged), and every check-run on the head, re-read after the last one completed. Nothing was built, run or re-run locally; the ablation and before-leg readings below are the dev's, cited as reported.

① Derived judgments

D1 — a runAs:'user' run now reaches exactly the rows its user's own request reaches, and nothing beyond: RIGHT. The email a run carries has exactly two sources. (a) The core resolver: plugin.ts bridges setUserGrantsResolver to resolveUserAuthzGrants(ql, userId, { tenantId, bypassGrantsCache: true }) and now spreads grants.email when present; core fills grants.email from the sys_user row of that same userId (the getUserRow fallback) — the stored email of the run's own user, never a different user's. (b) A context that already carries one is kept verbatim (needsEmail = !runContext.email). Who can supply (b): the REST trigger door buildAutomationContext builds the context from named fields only and routes unknown body keys into params, so no request body key can land on email; the action door dispatchFlowAction lists its fields explicitly and forwards no email; the record-change trigger hands record, previous, object, event, userId, tenantId and params only; a subflow / map child spreads its parent's context (the same user's run); a resumed leg carries the run's own persisted context. No public door lets a caller write someone else's email; the in-process execute(name, context) is a service API whose caller already controls userId, positions and permissions, a trust boundary this diff does not move. On the consuming side the RLS compiler binds current_user.email from the execution context's email and nothing else (rls-compiler.ts, the userCtx build), and resolveRunDataContext's user arm copies context.email only when present and invents nothing. Direction: with the email the run's envelope is still a subset of the request's (accessible_org_ids, systemPermissions remain absent on the run — pre-existing, outside this card, and narrowing); without it, every email-keyed policy drops out, narrowing again. The real-stack pin runas-user-email-owner.integration.test.ts holds the control, the guard, the card's reproduction, the doors' forwarded shape and the "someone else's email is still refused" case. One difference to name: the request path keeps a session email over the stored one, while no door forwards the session email and the run always takes the stored one. The two coincide for a sys_user whose session and stored email agree, which is how the auth plugin keeps them; a divergence makes the run different-or-narrower, never wider. The dev noted it unfiled; I agree.

D2 — the decider's segment (PR #24527's path): RIGHT. resolveDecisionSegmentContext now deletes switched.email beside positions, permissions and org_user_ids, and does so before the runAs === 'system' early return, so the submitter's email leaves the context on both postures. On the user posture seedRunGrants(run.flowName, switched, 'deciding') sees needsEmail true and resolves the decider's own stored email through the same resolver. Without the removal the decider would run under the submitter's email, which the dev's second ablation reports as a real-stack leak (the decider wrote the submitter's row). Pinned at the envelope layer (resume-decider-identity.integration.test.ts: the decider's email after a decision, the submitter's email before it, the failed-resolution case answering undefined, and both runAs:'system' decision cases carrying no email) and on the real stack (the decider's row stamped with the decider; the submitter's row refused to the decider as the decider's own request is).

D3 — the runAs:'system' arm is unchanged: RIGHT. The only line the diff adds to resolveRunDataContext is if (context.email) out.email = context.email; in the user arm, after the fail-closed userId check; the system arm's envelope (isSystem: true, actor, optional userId / tenantId, empty positions / permissions, optional flowRunId) is byte-identical to the base, and seedRunGrants still resolves nothing when runContext.runAs === 'system'. Pinned in crud-runas.test.ts ('email' in c.ctx is false on a system run) and by the real-stack system control. The claim's exclusion of this arm (#24614) is honoured.

D4 — the flow CEL current_user binding is unchanged: RIGHT. runUserOf still builds the EvalUser from id, positions and organizationId only; the diff edits its docblock and the scope note, not its body. The new pin in flow-cel-current-user.test.ts evaluates has(current_user.email) on a context that carries an email and answers no. This is consistent with ruling #19939 Q2 A as the test header records it (id, positions, organization, platform-admin flag; no email, no name), and content/docs/automation/flows.mdx keeps saying so truthfully.

D5 — the published surface: RIGHT, with one wording nit. AutomationContext.email?: string lands in packages/spec/src/contracts/automation-service.ts behind the contracts entry's export *, with TSDoc naming who sets it (run setup, the decider's switch), who reads it (resolveRunDataContext, the RLS compiler), what its absence means (policies drop out, narrowing) and that CEL does not bind it. The interface has no index signature, so the member is a real type widening and runContext.email is typed. RunIdentityContext.email? (and RunDataContext, which extends it) is exported by name from the service-automation entry. FlowUserGrants.email? is the return type of FlowUserGrantsResolver, the parameter of the public AutomationEngine.setUserGrantsResolver, so it is on the published face structurally, but index.ts does not re-export the name; the changeset's "Published types" sentence slightly overstates that one. All three members are optional and additive; a host resolver that returns no email still type-checks. packages/spec/api-surface/contracts.json records AutomationContext (interface) by existence only, so no generated artefact moves, as the dev's check:generated reading and the head's Type Check lanes agree.

D6 — the seeding rule and its fail-safe: RIGHT. Only a missing fact is resolved; carried positions and permission sets are never re-broadened (the ADR-0090 agent-ceiling case keeps its narrowing). A resolver that answers no email seeds none (if (needsEmail && grants.email)), pinned as "never a made-up one". A resolver failure keeps the warn level with the same fail-safe argument, and the message now names which facts were lost; the lost list is empty-safe. The one cost the rule adds — a context carrying permissions and a peer list but no email asks the resolver once for the email alone (a subflow / map child of an email-less user, or an in-process caller) — is bounded and declared in the acceptance notes. A runAs:'user' run's persisted context_json now carries its user's email beside the positions it already carried; that row's read is gated by sys_automation_run's own permissions. Accepted.

D7 — tests, pins and the head's gates: RIGHT. The real-stack pin sits beside runas-user-hook-org-peers.integration.test.ts as the card asks (its docblock names runas-user-org-peers.integration.test.ts as its twin; both files exist). The changed pins are additive except the retitled crud-runas fixture (deviation 2, by design). The check-runs on the head are the gate verdicts, read after the last of the 41 completed: the five required contexts — Lint & Repo Gates, TypeScript Type Check, Test Core (with all six shards), Build Core and Governed Surface Queue Guard — concluded success; the Type Check lanes that carry the spec artefact gates (workspace, source gates, consumer gates, debt ledger) and every Lint lane concluded success; Check Changeset, Spec property liveness, Check PR Size, Dogfood Regression Gate, Check Documentation Links, the docs-affected flag and the four claim / single-writer / part-of guards concluded success; the remaining runs (Console Pin Gate, Build Docs, the dogfood matrix shard job, Temporal Conformance, Dogfood Verify CLI, Packed-tarball smoke) are skipped, the full-battery jobs a PR run does not carry. No run is red and none is unfinished.

② Semver level

The changeset .changeset/24613-run-identity-email.md grades @objectstack/service-automation: minor and @objectstack/spec: minor, and its body carries Clause-②: yes (widening); the PR body carries the same line. That matches what the diff publishes: three optional members added, nothing removed or renamed, no authorable metadata key (AutomationContext is a runtime contract interface, not a metadata schema, so no ADR-0087 disposition, tombstone or liveness row is owed), no skip-changeset. yes takes at least minor; (widening) is the right arm. Check Changeset is green on the head. RIGHT. The one note is D5's wording nit on FlowUserGrants; it changes no grade.

③ Boundary flags

Dev deviations, each read against the diff:

  1. No narrowing, Clause-②: yes (widening) — consistent with the diff (②).
  2. The crud-runas fixture retitled and given an email — read; the old shape (permissions and peers, no email) is now pinned separately as "resolves the email alone, and keeps the rest verbatim". Accepted.
  3. Two ablations where the dispatch named one — reported with their red counts; cited, not re-run (read-only).
  4. The record-change envelope hand-built in the pin because @objectstack/trigger-record-change depends on this package — verified: the trigger package's envelope is record, previous, object, event, userId, optional tenantId, params, and the pin's recordChangeEnvelope matches that shape. Accepted.
  5. The before-leg refusal is the by-id not-found rather than the card's record-level sentence — both refuse the row the member can read with their own request. Accepted.
  6. The spec suite ran before the merge — the net diff's only packages/spec path is the contract file and the merge moved no spec file; Test Core on the head covers it. Accepted.
  7. The dev stopped its own first spec run (its own process group) — its own PID, process discipline kept. Noted.
  8. Commit trailers and the PR footer follow AGENTS.md's model-free form — correct; AGENTS.md takes precedence.

Open question (the flow CEL current_user and email): the seat's ACCEPT answers A, keep it unbound under ruling #19939 Q2 A. This PR's diff is consistent with A — runUserOf is unchanged, the new pin holds has(current_user.email) false even when the run context carries one, and the docblocks and flows.mdx still state the four-member list. Options B and C would be a re-ruling of Q2 A and are the maintainer's call, not this PR's.

Out-of-scope findings:

  • (a) The hook-side twin: ScopedContext[SEED_USER_GRANTS] in packages/objectql/src/engine.ts seeds positions, permissions and org_user_ids and not grants.email — verified by reading; packages/objectql/** is excluded by the claim and the diff does not touch it. Escalated to the seat for the domain:engine lane as the card's optional third bullet; not a condition on this PR.
  • (b) No door forwards the request's session email; the run takes the stored one — covered in D1; coincide in practice, divergence never widens. Noted, not filed, as the dev wrote.

Claim exclusions held: the net diff touches nothing under packages/objectql/**, packages/core/** or packages/plugins/plugin-security/**, and resolveRunDataContext's system arm is byte-identical (#24614 stays serialised behind this card). Governed surfaces: none in the file list; same-repo head; size/l. The docs-drift comment names flows.mdx and actions.mdx through the AutomationContext symbol; neither enumerates the interface's members, and the one sentence about the run carrying no email is about CEL current_user, which stays true — no doc edit is owed by this PR.

Implemented-by: claude/issue-24613-run-identity-email
Reviewed-by: session_01Nk17Z26X5DB59iQztDBADH

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:services seat 1 (#6021) · marchtian · session_01Nk17Z26X5DB59iQztDBADH · 2026-10-19T19:32Z · head 5318e58101.

The contract review of record is PASS 6275514585 at CONTRACT_REVIEW_TIER on this head. It covers the contracts/** widening and every file that hunk makes a claim about. This seat review records the rest. The PR lands through the merge queue.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 19, 2026 19:34
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 19, 2026
Merged via the queue into main with commit 14ccb1d Oct 19, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24613-run-identity-email branch October 19, 2026 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants