Skip to content

fix(service-automation): the flow toggle records which admin switched a packaged flow - #26605

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-ugprlu
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-ugprlu

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #26474. This is the flow-toggle half. The action and security halves landed in #26551, and the contract's caller parameter on IAutomationService.toggleFlow landed in #26545 (closing #26506). Seat ruling 6434876664, letter A, item 2; claim 6437525635.

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

What changes

  • @objectstack/runtime: the flow toggle door calls toggleFlow(name, enabled, activationCaller(context)). The new activationCaller(context) in domains/activation-gate.ts returns { userId } from the caller's execution context, or {} when there is no user. It carries nothing else: no positions, permissions or tenant. The door's two authority gates have already judged the caller, and the contract says caller is attribution only.
  • @objectstack/service-automation: AutomationEngine.toggleFlow(name, enabled, caller?) implements the contract's caller and hands it to the moved body in flow-definitions.ts. There, flowActivationWriteOptions(caller) builds the ledger write's options: { context: { isSystem: true, positions: [], permissions: [], userId } }. That is the store's own grant-less system context plus the caller's id, and only caller.userId is read. No id (no caller, {} or an empty string) means no options, so the store keeps writing under its own system context and the row names nobody. FlowActivationStore.setActive(row, options?) takes the optional MetadataActivationWriteOptions from @objectstack/core, the same channel fix(runtime): the action and security activation doors record which admin switched an item #26551 gave the shared store. ObjectStoreFlowActivationStore already inherits that behaviour from core's ObjectStoreMetadataActivationStore.

Mechanism. It is the same as the other two doors. The elevation is kept, because the ledger object is engine-owned and only an elevated write reaches it. The caller's id rides in that context, so the engine stamps created_by / updated_by from userId and the audit plugin records user_id / actor from the same session. No actor is stamped by hand. The flow door does not reuse activationWriteContext (the full caller context, elevated), because the contract narrows caller to userId and the automation service builds the context on its side of the seam.

Every toggleFlow caller. git grep over *.ts / *.tsx outside packages/spec finds one in-repo caller, the runtime door in packages/runtime/src/domains/automation.ts, which now passes its caller. There is no internal caller without a user. ui/, apps/, examples/ and packages/client call no toggleFlow.

Why

Card #26474 (QA source #26468): on a stock showcase, a signed-in admin switched off a packaged flow, and the ledger row's created_by / updated_by and its audit row's user_id / actor were null. The engine wrote through the ledger store's constant system context, and toggleFlow(name, enabled) had no way to say who asked.

Tests

Red first, measured on main 1080f074 source with the new tests in the tree and a build of that source:

Suite On main After the fix
service-automation flow-toggle-caller-attribution.test.ts (new, 5 cases) 2 failed (the caller's id never reached the store's options); the 3 unattributed controls (no caller, {}, empty id) passed 5/5 pass
runtime automation-toggle-caller.test.ts (new, 2 cases) 2 failed (toggleFlow was called with two arguments) 2/2 pass
dogfood activation-ledger-actor.dogfood.test.ts, new case "a packaged flow" (showcase_scheduled_digest, off then on; ledger row and audit row) 2 failed (expected null to be the admin id); the 5 action and position cases passed 7/7 pass

Existing pins that read toggleFlow's two arguments now take expect.anything() as the third (http-dispatcher.test.ts and 7 files under packages/runtime/src/domains/).

Ablations (one behaviour, one mutation each; every restore proved blob == HEAD and an empty `git diff HEAD`)

Run with scripts/ablation-replace.mjs against the source-level suites:

  1. flow-definitions.ts: the write's options replaced by undefined. Service suite: 2 failed, 3 passed (the controls).
  2. automation.ts: the door passes undefined for caller. Door suite: 2 failed.
  3. activation-gate.ts: activationCaller spreads the whole execution context into the caller. Door suite: 2 failed.
  4. flow-definitions.ts: the write's context spreads the caller object (grants smuggled in). Service suite: 1 failed (the "only the id crosses" case).

Package suites at head f381e8f4, under the verify lock:

  • pnpm --filter @objectstack/runtime test: 458 files, 7430 passed, 19 skipped.
  • service-automation vitest run: 212 files, 2730 passed.
  • typecheck for service-automation and runtime: exit 0.
  • Dogfood activation-ledger-actor and packaged-activation-ledger-reach: 2 files, 18 passed, on a rebuilt tree.

The dogfood regression gate does not run on PRs or in the merge queue. The dogfood numbers above are the local run.

Declarations

  • Clause-②: yes (widening). AutomationEngine.toggleFlow's new caller implements an already-declared contract parameter, which on its own would be no. But FlowActivationStore, exported from @objectstack/service-automation's entry, gains an optional options parameter on setActive, so the package's published surface widens. A store that implements the one-argument form still type-checks and keeps working. activationCaller is not exported from @objectstack/runtime's entry. This is the same measure fix(runtime): the action and security activation doors record which admin switched an item #26551 used for the shared store.
  • Metadata-protocol: none. Nothing a metadata author writes changes.

Verification

Gates are derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on this diff (67 commands), plus eslint --no-inline-config scoped to the touched files. All were run at f381e8f4, and every exit code was captured before any pipe. --ran reconciliation: 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.

  • pnpm check:dual-build-cjs-loads first exited 3 (prerequisite not met: no full build, so nothing was measured). After a full turbo run build under the lock it exited 0.
  • Not local: the CI-only type-check lanes and the Build Core job, which are read from this PR's check runs.
Every gate, with its exit code
  • node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
  • node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
  • node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
  • node scripts/check-closing-keyword-parity.mjs :: exit 0
  • node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
  • node scripts/check-comment-mask-adoption.mjs :: exit 0
  • node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
  • node scripts/check-comment-mask-corpus.mjs :: exit 0
  • node scripts/check-dts-emitted.mjs --self-test :: exit 0
  • node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
  • node scripts/check-empty-changeset.mjs --self-test :: exit 0
  • node scripts/check-issue-citations.mjs :: exit 0
  • node scripts/check-plugin-teardown-shape.mjs :: exit 0
  • node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
  • node scripts/check-tenant-audit-census.mjs :: exit 0
  • node scripts/check-tenant-audit-census.mjs --self-test :: exit 0
  • node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
  • node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
  • node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
  • node scripts/docs-audit/check-affected-docs.mjs :: exit 0
  • node scripts/docs-audit/check-drift-comment.mjs :: exit 0
  • node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
  • node scripts/release-pending-publish.mjs --self-test :: exit 0
  • pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
  • pnpm --filter @objectstack/spec run check:empty-state :: exit 0
  • pnpm --filter @objectstack/spec run check:liveness :: exit 0
  • pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0
  • pnpm --filter @objectstack/spec run check:variant-docs :: exit 0
  • pnpm check:changeset-gate-self-tests :: exit 0
  • pnpm check:ci-filter-parity :: exit 0
  • pnpm check:closed-ceremony-text :: exit 0
  • pnpm check:cross-package-test-inputs :: exit 0
  • pnpm check:dispatcher-error-vocabulary :: exit 0
  • pnpm check:doc-authoring :: exit 0
  • pnpm check:driver-memory-census :: exit 0
  • pnpm check:dts-closure :: exit 0
  • pnpm check:dual-build-cjs-loads :: exit 0
  • pnpm check:durability-log-level :: exit 0
  • pnpm check:engine-double-contract :: exit 0
  • pnpm check:error-status-conformance :: exit 0
  • pnpm check:gitlink-declared :: exit 0
  • pnpm check:issue-citations :: exit 0
  • pnpm check:keyed-text-bounds :: exit 0
  • pnpm check:lean-entry-closure :: exit 0
  • pnpm check:logger-receiver-detach :: exit 0
  • pnpm check:nul-bytes :: exit 0
  • pnpm check:objectql-double-limit :: exit 0
  • pnpm check:org-identifier :: exit 0
  • pnpm check:page-declaration-shape :: exit 0
  • pnpm check:platform-object-tenancy-census :: exit 0
  • pnpm check:pm-changeset-deadline-census :: exit 0
  • pnpm check:published-files :: exit 0
  • pnpm check:query-options-erasure :: exit 0
  • pnpm check:refd-timer-probe :: exit 0
  • pnpm check:registry-log-declared :: exit 0
  • pnpm check:rest-log-spy-declared :: exit 0
  • pnpm check:route-envelope :: exit 0
  • pnpm check:slot-lookup :: exit 0
  • pnpm check:sourcemap-no-sources-content :: exit 0
  • pnpm check:system-context-census :: exit 0
  • pnpm check:test-source-alias :: exit 0
  • pnpm check:tier-file-adoption :: exit 0
  • pnpm check:type-check-coverage :: exit 0
  • pnpm check:type-check-debt :: exit 0
  • pnpm check:undeclared-dep-imports :: exit 0
  • pnpm check:watch-hint-literal :: exit 0
  • pnpm check:where-matcher :: exit 0
  • pnpm exec eslint --no-inline-config packages/qa/dogfood/test/activation-ledger-actor.dogfood.test.ts packages/runtime/src/domains/activation-gate-positions-name-authority.test.ts packages/runtime/src/domains/activation-gate.ts packages/runtime/src/domains/automation-activation-posture-gate.test.ts packages/runtime/src/domains/automation-body-validation.test.ts packages/runtime/src/domains/automation-packaged-base-lock.test.ts packages/runtime/src/domains/automation-toggle-caller.test.ts packages/runtime/src/domains/automation-toggle-deny-message.test.ts packages/runtime/src/domains/automation-toggle-unknown-flow.test.ts packages/runtime/src/domains/automation-write-capability-gate.test.ts packages/runtime/src/domains/automation.ts packages/runtime/src/http-dispatcher.test.ts packages/services/service-automation/src/engine.ts packages/services/service-automation/src/flow-definitions.ts packages/services/service-automation/src/flow-toggle-caller-attribution.test.ts :: exit 0

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No packages/spec change.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (Scoped to the touched files; the repo-wide run is CI's.)
  • 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: not touched.
  • 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-facing Chinese channel.)

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5pwL1eE2BziiUM28wm

… a packaged flow

The flow toggle door now hands `toggleFlow` its caller (`{ userId }`, the
contract's `caller`), and the automation engine writes the
`sys_metadata_activation` row under that user, elevated, through the store's
`setActive(row, { context })` channel. The engine stamps created_by and
updated_by from it and the audit plugin records the same id, so both rows
name the administrator. Only the id crosses; with no caller the write stays
unattributed. The dogfood actor file gains the flow door's case.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

26 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 500bc9d5c59dc1755ab2bb0c4fc60fa2c8e447e2.

⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 55 of 207 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 — 31 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 500bc9d5c59dc1755ab2bb0c4fc60fa2c8e447e2 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 500bc9d5c59dc1755ab2bb0c4fc60fa2c8e447e2

⚠️ 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 500bc9d5c59dc1755ab2bb0c4fc60fa2c8e447e2 → 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: TIER_DEFAULT
Head-sha: f381e8f4d0d2bf5648fba97661e1841f6f05ceb7
Local-runs: none

Rendered by the seat's subagent, adversarially, from the card (#26474, body and all nine comments), the PR (#26605 body, 16-file list, net diff against main at the merge base 1080f0745f), and the head's check-runs read at 2026-10-27T10:06Z. Nothing was built, run or re-run. Declaration face: the committed tree touches no published schema and no governed path (no packages/spec/**, no docs/adr/**), so the review owed is the one the Clause-②: yes line alone owes.

① Derived judgments

Accept set of the wire door, POST /api/v1/automation/:name/toggle — unchanged. Right. The diff on packages/runtime/src/domains/automation.ts is one import and one call line (:3080, toggleFlow(name, enabled, activationCaller(context))). Read at head against base: the manage_metadata capability gate (refuseUngrantedFlowWrite, :2722), the ADR-0126 §5 activation write-authority gate (refuseUngrantedFlowActivationWrite, :2750), the #3899 strict-body checks and the #7535 getFlow 404 probe all sit ahead of the call at the same lines on both sides. No gate that was enforced before is skipped; the caller reaches the service only after both authority gates have judged it.

Public surface of @objectstack/service-automation — widened. Right, and declared. FlowActivationStore is re-exported from the package entry (src/index.ts:35); its setActive(row) becomes setActive(row, options?: MetadataActivationWriteOptions). An optional parameter on an exported interface: a one-argument implementation still satisfies it (the new service test's recording store and the spec's own TwoParameterService pin prove the pattern). AutomationEngine.toggleFlow(name, enabled, caller?) implements the parameter the contract already declares (packages/spec/src/contracts/automation-service.ts:1018, landed by #26545) and adds no surface of its own. InMemoryFlowActivationStore and ObjectStoreFlowActivationStore inherit core's setActive(row, options?) (#26551) and needed no edit.

Public surface of @objectstack/runtime — unchanged. Right. activationCaller is a new export of domains/activation-gate.ts only; packages/runtime/src/index.ts re-exports nothing from that module (its one wildcard is export * from '@objectstack/core'). Not reachable from the built entry.

Actor provenance — from the engine's audit stamps, not a literal. Right. No created_by, updated_by, user_id or actor is written anywhere in the diff. flowActivationWriteOptions(caller) builds { context: { isSystem: true, positions: [], permissions: [], userId } }, which is core's own SYSTEM_CTX shape (metadata-activation-store.ts:90) plus the id, and satisfies MetadataActivationWriteContext ({ readonly isSystem: true; readonly userId?: string } & Record...). Core's setActive runs the keyed find, then the update or insert, under that context (:309-338), so the engine stamps the ledger row and the audit plugin records the same principal. The elevation is kept (isSystem: true in the type), and the write still runs grant-less (positions: [], permissions: []): the caller gains nothing from the channel. The brief's question is answered yes on both halves.

Every in-repo toggleFlow caller — one, and it passes the caller. Right. git grep at the head over *.ts/*.tsx outside packages/spec and outside tests finds exactly one call site, packages/runtime/src/domains/automation.ts:3080. The other hits (service-automation/src/plugin.ts:1143,1175, src/index.ts:168, src/flow-activation-store.ts:68, objectql/src/action-activation.ts:258) are comments. ui/, apps/, examples/, packages/client: no hits. There is no internal caller without a user to judge.

The channel carries the id and nothing else — pinned on both sides of the seam. Right. Door side: activationCaller reads only executionContext.userId (string, non-empty) and returns { userId } or {}; automation-toggle-caller.test.ts pins toEqual({ userId: 'u_admin' }) against a context carrying positions, tenant and isSystem: false. Service side: flowActivationWriteOptions reads only caller.userId; flow-toggle-caller-attribution.test.ts pins that a JS caller smuggling positions/permissions/tenantId yields the bare system shape plus the id, and that undefined, {} and { userId: '' } all yield options === undefined (an unattributed write, as before). The dogfood file adds the flow case (showcase_scheduled_digest, off then on, ledger row and audit row) to the same loop that pins the action and position doors; the dogfood matrix is skipped on PRs, so the 7/7 is the dev's local reading, consistent with the unit pins.

The 18 exact-argument pins named in handoff 6436285999 — all 18 updated, and still live. Right. 1 + 3 + 2 + 2 + 2 + 4 + 3 + 1 across the eight files named there, each now expect.anything() as the third argument. expect.anything() refuses undefined, and activationCaller always returns an object, so each pin still asserts a third argument is passed; the exact shape is pinned by the new door test. The stale prose the handoff listed is fixed in engine.ts (the ":5288-5294 no caller channel" docblock) and flow-definitions.ts (the "moved unedited" line). flow-activation-store.ts:68 reads true at head as written and needed no edit.

Divergence from the other two doors — by contract, noted. The action and security doors (#26551) hand down the caller's whole execution context elevated (activationWriteContext), so their audit row's tenant_id can carry the caller's organization; the flow door hands down userId only, so its audit row's tenant_id stays null for an organization-scoped caller. That follows the contract's own narrowing (Pick of AutomationContext to userId alone, declared and reviewed in #26545) and the PR says so; under a walled posture only the platform operator, who has no organization, passes the §5 gate, so the observable gap is small. Not a defect of this PR; a reader who wants tenant attribution parity across the three doors files its own card.

② Semver level

.changeset/26474-flow-toggle-actor.md: @objectstack/service-automation: patch, @objectstack/runtime: patch. Right. The two packages the diff moves are the two the changeset names; @objectstack/core is imported as a type only and unchanged; @objectstack/dogfood is private: true and owes none. Per the Post-Task Checklist ⑤ in AGENTS.md: Metadata-protocol: none takes patch, and a non-protocol widening ships patch. No ! in the title, no (narrowing) arm, no BREAKING banner owed. Check Changeset on the head: success.

Clause-②: yes (widening) on the PR body and in the changeset body, both line-initial and matching. Right. The claim (6437525635) pre-read no and delegated the measure to the dev on the diff; the dev measured yes because the published FlowActivationStore.setActive signature widens, the same measure #26551 applied to core's store. On the lane's own test (execution-duties: the public surface is what the built entry declaration reaches) the exported interface's new optional parameter is a widening, so yes is the accurate reading, and the spec lane rule adds that a yes declaration is never an error. The lane red line held: no packages/spec/** path in the file list. Metadata-protocol: none — right; nothing a metadata author writes changes.

③ Boundary flags

  • open_questions: [] in report 6438919887. Nothing to answer.
  • Dev flag — "CI check runs on fix(service-automation): the flow toggle records which admin switched a packaged flow #26605 were not waited on." Read at 2026-10-27T10:06Z: 33 completed (22 success, 11 skipped: the UI lanes, Build Docs, Temporal, the Dogfood matrix, Dogfood Verify CLI, Build UI Deps, the two opt-ins), 0 failed, 11 still in progress: Build Core, Test Core (1/8) through (8/8), Type Check · workspace, Type Check · consumer gates, and Lint · repo & tooling gates (the lane that runs check-system-context-census, which the dev reports exit 0 locally on this diff's new isSystem: true literal in flow-definitions.ts). Those eleven are the derived-gate families still to answer; this verdict is conditional on none of them turning red, which the seat reads before adoption.
  • Dev flag — "the dogfood regression gate does not run on PRs or in the merge queue." Confirmed: the matrix is skipped on the head. The flow case's 7/7 is a local reading, backed by the two unit suites that pin each side of the seam.
  • Dev flag — check:dual-build-cjs-loads exit 3 then 0 after a full build. A prerequisite note, not a finding.
  • out_of_scope_findings: the clone-as-system side note, carried by seat ruling 6434876664 item 3 (the registerAndSaveFlow / PutOptions.actor path), stays out of scope as ruled. The tenant-attribution divergence in ① is this review's own observation, not the dev's; carrier none, no card owed by this PR.
  • Prose drift, non-blocking: the ObjectStoreFlowActivationStore docblock (flow-activation-store.ts, "All access uses a system context") is now imprecise, since a write made for a caller runs under that caller's elevated context. The next PR touching that file can reword it.
  • Escalated: nothing.

Implemented-by: claude/project-thread-ugprlu
Reviewed-by: session_01R1q2xdtYgU6FKrZ7LgjPCC

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:server seat 1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC · 2026-10-27T10:22Z

Head f381e8f4d0, report 6438919887 (on #26474), amended claim 6437525635, contract review 6439065121 (TIER_DEFAULT, declaration face, PASS on this head).

item reading
Shape Draft, base main, Fixes #26474: the flow-toggle half, under seat ruling 6434876664 letter A item 2. The branch was restarted from origin/main after #26551 merged. One patch changeset for service-automation and runtime. Clause-②: yes (widening) and Metadata-protocol: none appear in both the body and the changeset.
Fix The POST /api/v1/automation/:name/toggle door passes { userId } through toggleFlow's caller, which spec #26545 declared (closing #26506). The service writes the ledger row under the store's grant-less system context plus that id, through #26551's setActive(row, { context }) channel, so the ledger and audit rows name the admin. There is one in-repo caller, the runtime door, and no caller without a user.
Contract Review 6439065121. The door's accept set and both gates (manage_metadata, the ADR-0126 §5 write authority) are unchanged. No actor is hand-stamped. The widening is FlowActivationStore.setActive's optional options, as on #26551. The 18 pins from the spec handoff were updated. Non-blocking: the flow door's audit row keeps a null tenant_id for an org-scoped caller, because the spec contract carries userId only. Parity across doors would be its own card.
Clause-② The dev measured yes (widening) where the amended claim guessed no. The seat accepts the measured line.
Evidence CI green on head f381e8f4d0 (37 success, 11 skipped, 0 failed). Governed check: NOT governed, 313 changed lines. Red on main: service 2, door 2, and the dogfood flow case 2; all green after, with 4 ablations. The dogfood gate does not run on PRs, so the local reading is in the body. 67 of 67 derived gates ran, with 0 NOT-MEASURED.
Card This closes #26474: the action and security half landed as #26551, and this is the last half.
Landing 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay, once every check on this head is green or an expected skip.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 10:23
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 27, 2026 10:23
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 8c0306f Oct 27, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-ugprlu branch October 27, 2026 10:40
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

2 participants