Skip to content

fix(runtime): a flow started through the REST or action door carries its caller's posture rung beside their positions and permission sets - #28167

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-vyi88m
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-vyi88m

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Fixes #28099

Clause-②: yes (widening) — AutomationContext (@objectstack/spec/contracts) gains one optional member, posture, typed ExecutionContext['posture']; the REST trigger door and the action door fill it from the request's resolved execution context, and a run that carries it is served what that rung admits.
Metadata-protocol: none

维护者速读

  • 改了什么:经 REST 触发门(buildAutomationContext)和动作门(dispatchFlowAction)启动的流程,现在把调用者请求上已解析的姿态等级(ec.posture)带进运行,和 positions、permissions 一起带。AutomationContext 加一个可选成员 posture。
  • 为什么改:两个门以前只带 positions/permissions/租户/语言。运行带着自己的 grants,运行准备不再重新解析,所以运行没有 rung。平台管理员手动跑的流程只读到组织行,读不到部署级账本行(收窄,不是泄露)。automation: a runAs:'user' run carries no posture rung, so a platform administrator's flow is served organization ledger rows only #28070 修了引擎解析的那一半,这是门这一半。
  • 风险与回滚:只转发调用者自己的 rung,原样转发,不从流程作者推导,不抬高,没有就不补。请求体里的 posture 键仍是流程参数,不能伪造。门只带了 rung、没带 grants 时,运行准备自己解析 grants,并改用那次解析的 rung(没有就删掉)。回滚即 revert,无数据迁移。
  • 先看:packages/runtime/src/domains/automation.ts 与 action-execution.ts 各一行;run-resume.ts 的 seedRunGrants 一处;automation-service.ts 新成员的文档。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run src/flow-run-posture.test.ts(预期 7 passed),以及 pnpm --filter @objectstack/service-automation exec vitest run src/runas-user-posture-rung.integration.test.ts(预期 20 passed)。
  • 需要你做的:无,席位落地(欠一次契约复审,席位安排)。

What changes

  • packages/spec/src/contracts/automation-service.ts. AutomationContext.posture?: ExecutionContext['posture']. It is type-only and needs no new import (ExecutionContext is already imported as a type). It is documented beside positions / permissions: who sets it (the two doors), that it is the caller's own rung as-is, how run setup treats it, and that a missing rung narrows.
  • packages/runtime/src/domains/automation.ts (buildAutomationContext, which serves POST /api/v1/automation/:name/trigger, the legacy trigger route and a declared type: 'flow' endpoint). One line copies ec.posture when it is a non-empty string, the way ec.locale is copied. It is read off the execution context only: a body key named posture stays a flow param.
  • packages/runtime/src/action-execution.ts (dispatchFlowAction, REST /actions and MCP run_action). The same copy, in the context handed to automation.execute.
  • packages/services/service-automation/src/run-resume.ts (seedRunGrants, one hunk). When run setup resolves the grants itself (the context carries no permissions), the rung of that resolution replaces a forwarded one, or the forwarded one is deleted when the resolution has none. A door forwards positions and permission sets only when they are non-empty, so a rung can arrive alone. Without this hunk it would stand beside grants it was not derived from. A context that carries its grants keeps its rung as forwarded: setup neither re-derives nor raises it. The engine already carried a context-supplied rung to the data context (resolveRunDataContext reads it through runPosture, automation: a runAs:'user' run carries no posture rung, so a platform administrator's flow is served organization ledger rows only #28070), so nothing else in the package changes.
  • content/docs/permissions/record-view-auditing.mdx. The sentence from fix(service-automation): a runAs:'user' run carries its user's posture rung, so a platform administrator's flow is served the ledger's deployment-level rows #28098 that said a door-started run "is not, yet" served the deployment-level rows now says it carries its caller's posture and is served them the same way.
  • Tests. packages/runtime/src/flow-run-posture.test.ts (new, the producer half). runas-user-posture-rung.integration.test.ts gains five envelope cells and three real-stack cells (the consumer half).
  • .changeset/28099-door-runs-carry-posture-rung.md. Patch for @objectstack/spec, @objectstack/runtime and @objectstack/service-automation.

Why

Triage 6523377748: 「The REST and action doors forward the caller's own posture rung beside their positions and permission sets. It is never derived from the flow's author, and never raised.」 The claim declares one optional AutomationContext member and owes Clause-②: yes (widening) and a contract review.

The measurement (before the fix, at origin/main 47a8b588)

The new pins were run against the unchanged source.

  • flow-run-posture.test.ts: 5 red, 2 green. buildAutomationContext given an execution context with posture: 'PLATFORM_ADMIN' returned a context with no posture key. Both trigger routes handed the engine posture: undefined, and so did dispatchFlowAction. The flat-body cell was red because the door forwarded no rung at all. The two absence cells were green, as expected.
  • runas-user-posture-rung.integration.test.ts, real stack: the door-shaped envelope for the platform administrator (their own resolved positions and permission sets, no rung, which is what both doors built) ran qa_ledger_read and was served the organization row only. The same administrator's request is served every deployment-level row. That cell is kept as a CONTROL.
  • The engine side was already able to carry a forwarded rung on main: the envelope cells for a carried rung were green before the fix. Only the cell "a forwarded rung without forwarded grants is dropped when the resolution answers none" was red, which is this PR's run-resume.ts hunk.

Clause-②

One optional member is added to an exported contract interface. No existing member changes, no producer is required to set it, and a consumer that ignores it behaves as before. So this is yes (widening). Its only readers are the run's data context (runPosture → resolveRunDataContext), which #28070 already wired.

Verification

Head 0c325aab (one commit bd3ac11c plus a plain merge of origin/main after PR #28110 landed).

  • Pins. flow-run-posture.test.ts covers both doors: the trigger door carries the rung beside the grants, both trigger routes hand it to the engine, the action door carries it, a request with no rung (or an anonymous one) adds no key, and a body cannot name a rung. That is 7 passed. runas-user-posture-rung.integration.test.ts has 20 passed. The envelope cells: a door-started run carries the forwarded PLATFORM_ADMIN beside the forwarded grants; a forwarded MEMBER is not raised to the resolver's PLATFORM_ADMIN; no forwarded rung gives no rung; a rung forwarded without grants is replaced by the resolved one, or dropped when the resolution has none. The real-stack cells: the administrator's door-started run is served every deployment-level row, as their request is; a member's door-started run is served the organization row only, as their request is; plus the CONTROL above. The automation: a runAs:'user' run carries no posture rung, so a platform administrator's flow is served organization ledger rows only #28070 cells (an engine-resolved run) are unchanged and green.
  • Ablations, each through scripts/ablation-replace.mjs, each restored to HEAD:
    1. Trigger door copy removed: 4 red.
    2. Trigger door defaults a missing rung to MEMBER: 2 red (the absence cell and the body cell).
    3. Trigger door reads ctxBody.posture first: 2 red.
    4. Action door copy removed: 1 red.
    5. Action door defaults a missing rung: 1 red.
    6. seedRunGrants's else delete removed: 1 red.
    7. seedRunGrants seeds the resolver's rung even over carried grants (re-derivation): 5 red.
    8. seedRunGrants seeds no resolved rung (automation: a runAs:'user' run carries no posture rung, so a platform administrator's flow is served organization ledger rows only #28070's case): 5 red. This shows the engine-resolved pins still bind.
  • Typecheck. pnpm --filter PKG typecheck exits 0 for @objectstack/spec, @objectstack/runtime and @objectstack/service-automation. The runtime one needs @objectstack/driver-turso's dist in a fresh worktree.
  • Suites. Before the merge: runtime 488 files / 8832 tests, service-automation 229 / 2899, spec src/contracts 50 / 469, all exit 0. After the merge: runtime 488 / 8832, service-automation 230 / 2912, and the three typechecks, all exit 0.
  • Gates. dispatch-gates --commands derived 108 families, and the list is the same after the merge. All 108 exit 0. check:skill-examples and check:dual-build-cjs-loads first exited 3 (prerequisite) and passed after a whole-tree build. dispatch-gates --ran: 108 run, 0 NOT-MEASURED. pnpm lint exit 0. pnpm check:route-envelope exit 0. check-commit-card-trailers --range origin/main..HEAD exit 0.

Acceptance notes

  • SeededRunContext.posture in service-automation/src/runtime-identity.ts now restates the member it inherits from AutomationContext (same type, AuthzPosture). It is left as is because the file is outside the claimed surface. A later touch of that file can drop it.
  • content/docs/automation/flows.mdx (the trigger route's Identity paragraph) lists the forwarded identity as "user, positions, permissions, tenant". The list is incomplete but not false. It is outside the claimed surface, so I did not edit it.
  • Skills-docs: grep -rn -i "posture" skills/objectstack-automation finds no text that says which facts a door forwards, so nothing in skills changes.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (One type-only interface member.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (None move for an interface member: the spec gates and the spec build leave the tree clean.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (The 维护者速读 block is the maintainer's summary, as on the lane's other PRs.)
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4czNtyUvgvgGC1mdxrU

…its caller's posture rung beside their positions and permission sets

The trigger door (buildAutomationContext) and the action door
(dispatchFlowAction) forwarded the request's positions, permission sets,
organization and locale, but not its posture rung. A run that carries
grants is not re-resolved at setup, so it ran with no rung, and a
platform administrator's manual run was served the organization's
ledger rows only (fail closed).

- AutomationContext gains one optional member, posture, typed
  ExecutionContext['posture'] and documented beside positions and
  permissions.
- Both doors copy ec.posture as-is, absent when the request has none;
  a request body key cannot name it.
- seedRunGrants: when run setup resolves the grants itself, the rung of
  that resolution replaces a forwarded one, or none is carried.
- record-view-auditing.mdx: the door case is no longer "not yet".

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

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/runtime, @objectstack/service-automation, @objectstack/spec, touching 4 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), dispatchFlowAction (symbol, a top-level function))

⛔ 3 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/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/ 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
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 58 of 210 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 — 173 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 a3d24c991f2d793a1600e7f25fb0d5f577ed2fff → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 80b94ca91a3b22ecd1c4c23a3b154281dbe0be20 — the merge of head b53b0297da66c321f28074e3b9d646cdba0c4301 into base a3d24c991f2d793a1600e7f25fb0d5f577ed2fff, 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 80b94ca91a3b22ecd1c4c23a3b154281dbe0be20 && git checkout 80b94ca91a3b22ecd1c4c23a3b154281dbe0be20
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a3d24c991f2d793a1600e7f25fb0d5f577ed2fff b53b0297da66c321f28074e3b9d646cdba0c4301 && git checkout -B drift-repro a3d24c991f2d793a1600e7f25fb0d5f577ed2fff && git merge --no-ff b53b0297da66c321f28074e3b9d646cdba0c4301

node scripts/docs-audit/affected-docs.mjs --json a3d24c991f2d793a1600e7f25fb0d5f577ed2fff

⚠️ 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 a3d24c991f2d793a1600e7f25fb0d5f577ed2fff → 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

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract-review-claim: session_016WV4d1Np3hU7RQnA13YZcL · head 0c325aa · 2026-10-30T15:03Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0c325aab39dd155412198d98648510eda9354ec7
Local-runs: none
Scope: packages/spec/src/contracts/automation-service.ts + packages/runtime/src/domains/automation.ts (buildAutomationContext), packages/runtime/src/action-execution.ts (dispatchFlowAction), packages/runtime/src/endpoint-executor.ts (the declared type: 'flow' endpoint, a third caller of buildAutomationContext), packages/runtime/src/http-dispatcher.ts (where executionContext is written), packages/services/service-automation/src/run-resume.ts (seedRunGrants, deciderSegmentContext), packages/services/service-automation/src/runtime-identity.ts (SeededRunContext, runPosture, resolveRunDataContext), packages/services/service-automation/src/builtin/subflow-node.ts (child context), packages/core/src/security/resolve-authz-context.ts and assemble-execution-context.ts (where a request's rung is derived and carried), packages/plugins/plugin-security/src/security-plugin-rls-filter.ts and security-plugin.ts (the first reader keyed on the carried rung), packages/spec/src/kernel/execution-context.zod.ts and packages/spec/src/security/explain.zod.ts (the rung's type), docs/adr/0095 D2/D3, and the merged precedent PR #28098. The rest of the diff (the two test files, the changeset, record-view-auditing.mdx) is the lane seat's ACCEPT at TIER_DEFAULT.

Inputs: card #28099 (body; triage 6523377748, which grants the cross-domain member; the claim 6523907984; the os-dev report 6525918906), PR #28167 (body, file list, net diff against main at the head above), and the head's check-runs. Not the dispatch order. Timestamp of this reading: 2026-10-30T15:14Z.

① Derived judgments

Each accept-set or public-surface change the diff implies, named right or wrong:

  1. AutomationContext.posture?: ExecutionContext['posture'] (@objectstack/spec/contracts). One optional member, typed off the kernel schema (AuthzPostureSchema.optional() in execution-context.zod.ts), so its accept set is exactly the four ladder literals or absent, never a fresh enum. ExecutionContext was already a type import in this file; no new import. packages/spec/api-surface/contracts.json lists AutomationContext (interface) by name only and api-surface-signatures.json has no entry for it, so no generated artifact moves; Spec property liveness and Lint & Repo Gates are green on this head. Right.
  2. The trigger door forwards the rung. buildAutomationContext (domains/automation.ts:212) copies ec.posture only when it is a non-empty string, where ec is context.executionContext, which only the dispatcher's identity step writes (http-dispatcher.ts:703; left undefined for an anonymous request at :777). A REST body cannot inject it: the function builds automationContext from params, callerParamKeys, object, event, then the identity facts off ec; a flat body key named posture is not in the reserved set, so it falls into params; a wrapped one never leaves params. All three callers of the function take it: the two trigger routes (automation.ts:2361) and the declared type: 'flow' endpoint (endpoint-executor.ts:579). Right, and pinned by flow-run-posture.test.ts (both routes, the body cells, the anonymous cell).
  3. The action door forwards the rung. dispatchFlowAction (action-execution.ts:2083) spreads the same conditional copy. Its ec is the resolved envelope at both entrances: REST /actions passes _context.executionContext (domains/actions.ts:893) and the MCP run_action bridge passes the session's ec (action-execution.ts:3555). Right.
  4. The rung is only ever the caller's own, never the flow author's, never raised. A request's ec.posture is grants.posture from resolveUserAuthzGrants, derived by derivePosture from capability grants (ADR-0095 D3; resolve-authz-context.ts:780; assemble-execution-context.ts:369). Neither door derives, defaults or widens: absent on the request means no key on the context. On the engine side seedRunGrants re-resolves only when permissions is not an array (needsGrants); a context carrying its grants keeps the forwarded rung as forwarded (the integration cell: MEMBER forwarded for usr_admin stays MEMBER). The flow's author never enters any of these paths. Right.
  5. A rung that arrives without grants. A door forwards positions and permission sets only when non-empty, so a rung can arrive alone. Then needsGrants is true, the resolver runs, and the new else delete (run-resume.ts:228) means the context ends with the resolution's own rung or none, never the forwarded one. Right, pinned by the two "without forwarded grants" cells. One arm is not covered: see ④ finding S1.
  6. runPosture safe-parses the carried value against AuthzPostureSchema, so a non-ladder string a door forwarded (the door checks only string) is dropped at the data context. Right.
  7. Where the rung is read. resolveRunDataContext (runtime-identity.ts:411) copies it onto the user arm's RunDataContext; the system arm never reads it. The first enforcement reader is the Layer 0 exemption in security-plugin-rls-filter.ts:191-208: isPlatformAdmin = superuserBypass && platformPosture, where platformPosture is the carried rung when present. The rung alone grants nothing; the superuser bit from the carried permission sets must hold too. Right, and it is what makes the member fail closed by construction.
  8. Child and resumed contexts. subflow-node.ts:105 spreads the parent's context into the child, so the rung rides beside the grants it came with, as SeededRunContext's docblock says. A suspended run persists its context; a non-decision resume continues under the trigger user's whole identity (ruling A on FlowSchema.runAs), rung included, beside that same user's grants. The decision switch deciderSegmentContext (run-resume.ts:330) deletes the submitter's rung with their positions, permission sets, peers and email before seeding the decider's. Right.
  9. Docs. record-view-auditing.mdx now states a door-started run carries its caller's posture and is served the deployment-level rows the same way; true on this tree. Right.

② Semver level

  • Changeset .changeset/28099-door-runs-carry-posture-rung.md: patch for @objectstack/spec, @objectstack/runtime, @objectstack/service-automation; all three are moved by the diff and only those three. Check Changeset green.
  • Clause-②: yes (widening) on both carriers (PR body and changeset body). Read with the fleet's one reader, readClause2Line in scripts/pm/clause2-line.mjs: both bodies parse declared / yes / widening. The arm is right: one optional member added to an exported contract interface; no existing member changes type or meaning; a producer that never sets it and a consumer that ignores it behave as on main.
  • Metadata-protocol: none on both carriers; readMetadataProtocolLine parses declared / none on both. Right: nothing a metadata author writes changes (no flow key, no accepted value, no default).
  • Per AGENTS.md Post-Task Checklist step 3, a non-protocol widening ships patch. The level matches what the diff publishes.
  • Triage 6523377748 granted the cross-domain member and asked for exactly this declaration and this review; the claim's file surface is held (no other packages/spec file, no governed path; the .mdx sentence was added to the surface by the seat at claim time).

③ Boundary flags

  • open_questions: [] in the dev report: none to answer.
  • Dev observation 1, SeededRunContext.posture restating the inherited member. Checked: SeededRunContext.posture?: AuthzPosture (z.input of the enum) and AutomationContext.posture?: ExecutionContext['posture'] (z.infer of the same enum, optional) are the same literal union; the restatement is type-identical and its docblock stays accurate on this tree. Answered: no action owed in this PR; a later touch of runtime-identity.ts may drop it.
  • Dev observation 2, content/docs/automation/flows.mdx:2518 (the trigger route's Identity paragraph: "user, positions, permissions, tenant"). Incomplete, not false; it already omitted locale and email. The docs-drift check on this PR (comment 6525918010) names flows.mdx and ui/actions.mdx as pages that name AutomationContext / dispatchFlowAction. Escalated as S2 (should-fix) below: a one-clause docs follow-up, not a reason to hold this PR.
  • Check-runs on this head at the time of this record: green Lint & Repo Gates and all Lint · legs, Type Check · source gates / consumer gates / debt ledger, Build Core, Build Docs, Build UI Deps, Test UI, Build & E2E UI, Live E2E UI, Test Core (2/8, 7/8, 8/8), Governed Surface Queue Guard, Check Changeset, Check PR Size, PR-body guards, Spec property liveness, Dogfood Regression Gate, Flag docs affected by code changes, Check Documentation Links. Still pending: Test Core (1/8, 3/8, 4/8, 5/8, 6/8), Type Check · workspace, Type Check UI. No check-run on this head has concluded red. Those conclusions are the gate verdicts; this record does not stand in for them, and the PR lands only once every required context is success.
  • Nothing in the diff touches a governed surface (Governed Surface Queue Guard green); the PR is a draft with no assignee or reviewer request, per the dispatch's shape.

④ Design judgment

packages/spec/src/contracts/automation-service.ts, the one hits file, judged against the triage ruling (forward the caller's own rung beside their positions and permission sets; never derived from the flow's author, never raised) and its first consumers.

The member holds. It is typed off the kernel's resolved rung rather than a new vocabulary; its docblock names the two setters, the as-is rule, the absent-narrows rule, the re-resolution rule, and the system arm, and every one of those sentences is true of the cited code at this head (①.2-①.8). Its first consumer chain, runPosture to resolveRunDataContext to the Layer 0 exemption, requires the superuser bit from the carried permission sets in addition to the rung, so the member can narrow and never widen on its own.

Attempts to break it:

  • A missing rung that defaults upward. Both doors add no key; runPosture answers undefined; the Layer 0 gate then falls back to the capability probe, which is a strict subset of the rung's derivation (ADR-0099 I3) and so can only narrow. The integration suite's CONTROL cell measures the pre-fix shape (grants, no rung) served the organization row only. Holds.
  • A persisted context carrying one segment's rung into a later resume under a different caller. A non-decision resume (screen, wait, SLA, engine continuation) keeps the trigger user's userId, grants and rung together, by ruling A; the rung never stands under another person's id. A decision resume deletes it before the decider is seeded. Holds.
  • An approval segment's run. deciderSegmentContext deletes positions, permissions, org_user_ids, posture, email, then seedRunGrants(..., 'deciding') with needsGrants true, so the segment carries the decider's own rung or none; a runAs: 'system' run returns before seeding and its data arm never reads the rung. The new else delete is a no-op there (already deleted) and the right thing on the trigger path. Holds.
  • An agent principal on behalf of a platform administrator with a narrowed ceiling. The request's ec.posture is the envelope's own (assemble-execution-context.ts:369); the door forwards it beside the narrowed permission sets; run setup keeps both as carried. The run reads what the same agent request reads, and the Layer 0 gate still asks the superuser bit of the narrowed sets. Holds.
  • Body injection. Refused by construction and pinned (①.2).

Findings:

  • S1 (should-fix, not blocking). seedRunGrants's fail-safe arm (run-resume.ts, the catch): when the resolver throws on a context that needed grants, the run keeps the user's unresolved identity (baseline member fallback) and, since this PR, also keeps a rung a door forwarded without grants. Before this PR no rung could exist on a context whose grants were never resolved. The first reader still refuses the deployment-level rows (no superuser bit, so isPlatformAdmin is false), and the rung is still the caller's own, never raised, so this is not a widening; but the docblock's sentence "a rung stands only beside the grants it was derived from" is not held in that arm, and the warning's "falls back to baseline member permissions (not elevated)" is then written over a context that still carries PLATFORM_ADMIN. Fix: in the catch, when needsGrants, delete (runContext as SeededRunContext).posture as the success arm does, and pin it with one cell (resolver rejects, forwarded rung alone, data context carries no posture). May land here or as a one-hunk follow-up on run-resume.ts.
  • S2 (should-fix, not blocking). content/docs/automation/flows.mdx:2518, the trigger route's Identity paragraph, lists the forwarded identity as "user, positions, permissions, tenant"; add the posture rung (and, while there, locale and email, which the same doors already forward). Docs-only; ui/actions.mdx to be re-read for the same sentence per the drift check.

No must-fix.

Implemented-by: claude/project-thread-vyi88m
Reviewed-by: session_016WV4d1Np3hU7RQnA13YZcL

VERDICT: PASS

…henever run setup resolves the grants, and the trigger route names every fact the doors forward

seedRunGrants drops a forwarded posture up front whenever the context
carries no permission sets, then re-seeds it only from a resolution
that answers grants; a resolver that rejects, answers nothing or is not
wired now leaves the run with no rung instead of the forwarded one.

flows.mdx: the trigger route's Identity paragraph lists the posture
rung and language beside user, positions, permissions and tenant.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4czNtyUvgvgGC1mdxrU
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract-review-claim: session_016WV4d1Np3hU7RQnA13YZcL · head b53b029 · 2026-10-30T15:31Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b53b0297da66c321f28074e3b9d646cdba0c4301
Local-runs: none
Scope: packages/spec/src/contracts/automation-service.ts + packages/services/service-automation/src/run-resume.ts (seedRunGrants, deciderSegmentContext), packages/services/service-automation/src/engine.ts (setUserGrantsResolver), packages/runtime/src/domains/automation.ts (buildAutomationContext) and packages/runtime/src/action-execution.ts (dispatchFlowAction) re-read for the facts the doors forward, content/docs/automation/flows.mdx (the trigger route's Identity paragraph), and everything the superseded record 6526196044 read on 0c325aa. The rest of the diff (the two test files, the changeset, record-view-auditing.mdx) is the lane seat's ACCEPT at TIER_DEFAULT.

This record is a DELTA review: it supersedes record 6526196044 (PASS on 0c325aab39dd155412198d98648510eda9354ec7) and names the new head, which is that head plus one commit, b53b0297, taking both should-fixes of that record. The delta touches five files: run-resume.ts (one hunk moved), runas-user-posture-rung.integration.test.ts (one new cell), automation-service.ts (docblock only), flows.mdx (one paragraph) and the changeset (one bullet reworded, one added). Nothing else moved between the two heads; judgments ① to ④ of 6526196044 stand where this record does not restate them. Inputs: card #28099 (body; triage 6523377748; the claim 6523907984, amended to add flows.mdx to the surface; the dev report 6525918906), PR #28167 (body, unchanged since 6526196044; file list, now nine paths; the net diff against main at the head above; the delta diff 0c325aab..b53b0297), the delta claim 6526622532, and the head's check-runs. Timestamp of this reading: 2026-10-30T15:35Z.

① Derived judgments

  1. S1 taken, and taken the stricter way. seedRunGrants (run-resume.ts:205-210) now runs if (needsGrants) delete posture before the resolver guard, where needsGrants is "permissions is not an array". The success arm is byte-identical to main's (fix(service-automation): a runAs:'user' run carries its user's posture rung, so a platform administrator's flow is served the ledger's deployment-level rows #28098's line: if (grants.posture) posture = grants.posture), and the else delete from 0c325aa is gone. So a rung stands on a run context after setup only if (a) the context arrived with its permission sets, in which case it is kept as forwarded (the carried-grants invariant of fix(service-automation): a runAs:'user' run carries its user's posture rung, so a platform administrator's flow is served the ledger's deployment-level rows #28098 is untouched), or (b) the resolver answered grants carrying one. A resolver that throws, answers undefined, answers grants with no rung, or is not wired (this.userGrantsResolver falsy; setUserGrantsResolver accepts null, engine.ts:4118) leaves none. Right: this closes the arm 6526196044 S1 named (the catch) and the unwired arm it did not name.
  2. Side effects of the up-front delete. It fires on a runAs: 'system' context with no permission sets too; harmless, the system arm of resolveRunDataContext never reads the rung, and the member's docblock already says the rung is absent on such a run's data context. The decision switch (deciderSegmentContext) had already deleted posture with the submitter's other facts before calling seedRunGrants(..., 'deciding'), so the new line is a no-op there. An agent principal's narrowed ceiling, a door envelope with grants, a subflow/map child and a resumed segment all carry permissions, so none is touched. A record-change run arrives with no rung, so nothing to delete. Right.
  3. The new test cell (integration test, envelope layer) runs a rung-alone context against three resolvers in turn: one that rejects, one answering undefined, and null (unwired), and asserts the recording node's data context has isSystem: false, the member's userId and no posture key. It covers all three arms of ①.1's "leaves none". Right. It runs in the Test Core shards, which have not reported on this head yet (③).
  4. S2 taken, with a correction to the superseded record. The Identity paragraph (flows.mdx:2518-2525) now says the caller's identity (user, positions, permissions, tenant, posture rung) and language are forwarded as the caller's request resolved them; that the rung is the caller's own, never the flow author's, never raised, and that a request without one starts a run without one; and that run setup adds the user's email and organization peers. Re-read against both doors at this head: buildAutomationContext forwards userId, positions, permissions (non-empty only), tenantId, posture, locale; dispatchFlowAction the same set. Neither forwards email; seedRunGrants seeds email and org_user_ids from the resolver (needsEmail, needsPeers). So the paragraph is true on this tree, and record 6526196044's S2 was wrong to say the doors already forward email: the dev's correction stands. Right.
  5. The hits file. The docblock of AutomationContext.posture now reads: when run setup resolves the grants itself it drops any forwarded rung and takes the rung off that same resolution; grants that stay unresolved (the resolver rejects, answers nothing, or is not wired) leave the run with none; so a rung stands only beside the grants it was derived from. Every clause is true of run-resume.ts at this head (①.1). No type, name or member changed; api-surface/contracts.json still lists the interface by name only. Right.
  6. Changeset. The "Only beside the grants it came from" bullet now matches ①.1 exactly, and a "Docs" bullet names the flows.mdx paragraph and the record-view-auditing.mdx sentence. Right.

② Semver level

Unchanged from 6526196044 and re-read on this head: patch for @objectstack/spec, @objectstack/runtime, @objectstack/service-automation, the three packages the diff moves; Clause-②: yes (widening) and Metadata-protocol: none parse declared on both carriers through readClause2Line / readMetadataProtocolLine in scripts/pm/clause2-line.mjs (changeset at b53b0297, PR body as it stands). A non-protocol widening ships patch (AGENTS.md Post-Task Checklist step 3). The delta adds no member, no key and no accepted value. Matches.

③ Boundary flags

  • The two should-fixes of 6526196044 are both taken (①.1, ①.4); no new dev flag and no open_questions entry on the delta.
  • Dev observation 1 from the previous round (SeededRunContext.posture restating the inherited member) is unchanged and still no action owed.
  • The claim surface was amended before the flows.mdx edit (comment 6523907984 names "the trigger route's Identity paragraph only"); the delta's hunk is that paragraph and nothing else on the page. Surface held.
  • PR body: unchanged since the previous record. Its "What changes" bullet for run-resume.ts describes the 0c325aa shape ("the rung of that resolution replaces a forwarded one, or the forwarded one is deleted when the resolution has none"); the tree now drops the forwarded rung up front, which that sentence does not contradict but no longer describes exactly. Not a finding; the changeset body, which is what ships, is exact. The dev may refresh the bullet when the body is next edited.
  • Check-runs on b53b0297 at the time of this record. Concluded green: Auto Label, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, PR-body guards, Plan Test Core shards, Spec property liveness, filter. Not yet reported: Build Core, Build Docs, Build UI Deps, Lint · CI, release & engine gates, Lint · ESLint, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Live E2E UI, Test Core (1/8 to 8/8), Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace; the Lint & Repo Gates, TypeScript Type Check, Lint UI, Type Check UI, Test UI and Build & E2E UI contexts had not appeared yet. No check-run on this head has concluded red. Those conclusions are the gate verdicts; this record does not stand in for them, and the PR lands only once every required context is success.

④ Design judgment

packages/spec/src/contracts/automation-service.ts, the one hits file, re-judged on the delta against the triage ruling and the first consumers.

The member is unchanged in shape; only its docblock moved, and it moved toward the tree: the sentence "a rung stands only beside the grants it was derived from" is now held in every arm of seedRunGrants, including the fail-safe and unwired arms it was not held in at 0c325aa. The break attempts of 6526196044 (a missing rung defaulting upward; a persisted rung into a resume under another caller; an approval segment; an agent principal with a narrowed ceiling; body injection) all still hold, since none of those paths reaches the moved line with a change in inputs (①.2). One new attempt: a door envelope whose permissions arrived empty-but-present ([]) with a rung. A door never builds that (it omits the key when empty), and Array.isArray([]) is true so setup neither re-resolves nor deletes, exactly as for positions and permission sets on main; a hand-built context of that shape is the caller's own doing and the Layer 0 gate still asks the superuser bit of [], which is false. Holds.

Findings: none. S1 and S2 of 6526196044 are closed on this head; S2's "and email" clause in that record was wrong and is withdrawn.

Implemented-by: claude/project-thread-vyi88m
Reviewed-by: session_016WV4d1Np3hU7RQnA13YZcL

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

维护者速读

  • 改了什么:经 REST 触发门(buildAutomationContext,含声明式 type: 'flow' 端点)和动作门(dispatchFlowAction,含 MCP run_action)启动的流程,现在把调用者请求上已解析的姿态等级(ec.posture)随 positions、permissions 一起带进运行;AutomationContext 加一个可选成员 posture。运行准备只在拿到 grants 的解析里保留 rung:上下文没带权限集、解析失败/为空/未接线时,不留 rung。flows.mdx 的 Identity 段补上 rung 与语言。
  • 为什么改:门以前不带 rung,平台管理员手动跑的流程读不到部署级账本行(收窄,非泄露);automation: a runAs:'user' run carries no posture rung, so a platform administrator's flow is served organization ledger rows only #28070 修了引擎解析那一半,这是门这一半。
  • 风险与回滚:Clause-②: yes (widening);只转发调用者自己的 rung,不推导、不抬高、没有就不补;请求体里的 posture 仍是流程参数,不能伪造。回滚即 revert,无数据迁移。
  • 先看:packages/runtime/src/domains/automation.ts、action-execution.ts 各一行;run-resume.ts 的 seedRunGrants;automation-service.ts 新成员文档。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run src/flow-run-posture.test.ts 与 pnpm --filter @objectstack/service-automation exec vitest run src/runas-user-posture-rung.integration.test.ts,全绿。
  • 需要你做的:无(非受管;契约复审 PASS;席位按本任落地授权落地)。

席位意见:ACCEPT。

席位核对

Head-sha: b53b0297da66c321f28074e3b9d646cdba0c4301

  • Shape: 9 files, +318 / −4. The code changes are one optional member on AutomationContext (@objectstack/spec/contracts), the conditional copy at both doors, and seedRunGrants. The rest is two test files, one changeset (spec, runtime and service-automation, all patch), and two docs sentences: record-view-auditing.mdx and the Identity paragraph in flows.mdx. check-governed-merges --pr 28167: NOT governed (0 of 9 paths).
  • Premise measured: flow-run-posture.test.ts (7) and the integration file (20 on 0c325aab, 21 with the new cell) are green. Eight ablations on 0c325aab each turned their pins red. On b53b0297, ablating the up-front delete turns 2 cells red: the new cell and the existing "answers none" cell. Runtime 8832 and service-automation 2912 passed on 0c325aab, and service-automation 2913 on b53b0297. Typecheck is 0 for spec, runtime and service-automation. dispatch-gates --ran derived the same 108 families, all exit 0. pnpm lint: 0.
  • Gate check-runs on the head: 50 runs, 41 success, 9 skipped, 0 failure/pending; mergeable_state clean.
  • Contract review: owed at CONTRACT_REVIEW_TIER. PASS on this head in record 6526701502, which supersedes 6526196044 (PASS on 0c325aab with two should-fixes). No must-fix and no should-fix remain:
    • S1 is taken the stricter way. seedRunGrants drops a forwarded rung whenever the context carries no permission sets, so a rejecting, empty or unwired resolver leaves none.
    • S2 is taken. The first record's "and email" was withdrawn: the doors do not forward email, and run setup seeds it.
  • Correction to the PR body: the run-resume bullet and ablation 6 describe the 0c325aab shape, an else delete in the success arm. On b53b0297 the delete moved up front, and the success arm is fix(service-automation): a runAs:'user' run carries its user's posture rung, so a platform administrator's flow is served the ledger's deployment-level rows #28098's line unchanged. The changeset, which is what ships, is exact. The claim surface was amended to add the flows.mdx Identity paragraph (6523907984).
  • Docs drift advisory: it names flows.mdx, which is updated here, and ui/actions.mdx, which was re-read: its flow row says only "with your identity forwarded", which stays true. The three release pages it names are read-only and are not edited.
  • Landing: under the maintainer's landing grant for this seat term (seat 4 thread, 2026-10-29 22:04Z): pr_ready + automerge_enable through the relay.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 15:56
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 30, 2026 15:56
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 7d846b5 Oct 30, 2026
51 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-vyi88m branch October 30, 2026 16:39
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

automation: a flow started through the REST or action door carries the caller's positions and permission sets but not their posture rung

1 participant