Repository navigation
fix(service-automation): the flow toggle records which admin switched a packaged flow - #26605
Conversation
… 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
📓 Docs Drift CheckThis PR changes 2 package(s): 26 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 31 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
Contract reviewServed-tier: 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 ① Derived judgmentsAccept set of the wire door, Public surface of Public surface of Actor provenance — from the engine's audit stamps, not a literal. Right. No Every in-repo The channel carries the id and nothing else — pinned on both sides of the seam. Right. Door side: The 18 exact-argument pins named in handoff Divergence from the other two doors — by contract, noted. The action and security doors (#26551) hand down the caller's whole execution context elevated ( ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
席位核对 · ACCEPT ·
|
| 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
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
callerparameter onIAutomationService.toggleFlowlanded in #26545 (closing #26506). Seat ruling6434876664, letter A, item 2; claim6437525635.Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
POST /api/v1/automation/:name/toggle关掉或打开一个打包的 flow 时,激活台账行的created_by/updated_by和对应审计行的user_id/actor现在记的是这位管理员,不再是空。至此 automation: switching off a packaged flow or a packaged action writes the activation-ledger row and its audit row with a null actor, so nothing records which admin did it #26474 的三个开关门(action、安全目录、flow)都能说出是谁操作的。IAutomationService.toggleFlow写台账,以前这个契约带不了调用者,只能用不带用户的系统身份写入。spec 卡 spec(automation): IAutomationService.toggleFlow has no caller channel, so a flow switch-off cannot record which admin did it (the spec half of #26474) #26506 已在 feat(spec): optional caller on IAutomationService.toggleFlow #26545 补上caller参数,本 PR 把它接通。caller只取userId,不带职位、权限、租户。没有调用者的写入照旧用系统身份。回滚 = revert 本 PR。packages/services/service-automation/src/flow-definitions.ts(flowActivationWriteOptions与写入处)、packages/runtime/src/domains/activation-gate.ts(activationCaller)、packages/runtime/src/domains/automation.ts的 toggle 门。pnpm --filter @objectstack/dogfood exec vitest run test/activation-ledger-actor.dogfood.test.ts(先按 CI 建好 dogfood 依赖闭包),预期 7/7 通过,flow 用例的台账行与审计行都写着登录管理员的 id。What changes
@objectstack/runtime: the flow toggle door callstoggleFlow(name, enabled, activationCaller(context)). The newactivationCaller(context)indomains/activation-gate.tsreturns{ 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 sayscalleris attribution only.@objectstack/service-automation:AutomationEngine.toggleFlow(name, enabled, caller?)implements the contract'scallerand hands it to the moved body inflow-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 onlycaller.userIdis 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 optionalMetadataActivationWriteOptionsfrom@objectstack/core, the same channel fix(runtime): the action and security activation doors record which admin switched an item #26551 gave the shared store.ObjectStoreFlowActivationStorealready inherits that behaviour from core'sObjectStoreMetadataActivationStore.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_byfromuserIdand the audit plugin recordsuser_id/actorfrom the same session. No actor is stamped by hand. The flow door does not reuseactivationWriteContext(the full caller context, elevated), because the contract narrowscallertouserIdand the automation service builds the context on its side of the seam.Every
toggleFlowcaller.git grepover*.ts/*.tsxoutsidepackages/specfinds one in-repo caller, the runtime door inpackages/runtime/src/domains/automation.ts, which now passes its caller. There is no internal caller without a user.ui/,apps/,examples/andpackages/clientcall notoggleFlow.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_byand its audit row'suser_id/actorwere null. The engine wrote through the ledger store's constant system context, andtoggleFlow(name, enabled)had no way to say who asked.Tests
Red first, measured on
main1080f074source with the new tests in the tree and a build of that source:mainflow-toggle-caller-attribution.test.ts(new, 5 cases){}, empty id) passedautomation-toggle-caller.test.ts(new, 2 cases)toggleFlowwas called with two arguments)activation-ledger-actor.dogfood.test.ts, new case "a packaged flow" (showcase_scheduled_digest, off then on; ledger row and audit row)expected null to bethe admin id); the 5 action and position cases passedExisting pins that read
toggleFlow's two arguments now takeexpect.anything()as the third (http-dispatcher.test.tsand 7 files underpackages/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.mjsagainst the source-level suites:flow-definitions.ts: the write's options replaced byundefined. Service suite: 2 failed, 3 passed (the controls).automation.ts: the door passesundefinedforcaller. Door suite: 2 failed.activation-gate.ts:activationCallerspreads the whole execution context into the caller. Door suite: 2 failed.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.vitest run: 212 files, 2730 passed.typecheckfor service-automation and runtime: exit 0.activation-ledger-actorandpackaged-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 newcallerimplements an already-declared contract parameter, which on its own would beno. ButFlowActivationStore, exported from@objectstack/service-automation's entry, gains an optionaloptionsparameter onsetActive, so the package's published surface widens. A store that implements the one-argument form still type-checks and keeps working.activationCalleris 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/objectstackon this diff (67 commands), pluseslint --no-inline-configscoped to the touched files. All were run atf381e8f4, and every exit code was captured before any pipe.--ranreconciliation: 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.pnpm check:dual-build-cjs-loadsfirst exited 3 (prerequisite not met: no full build, so nothing was measured). After a fullturbo run buildunder the lock it exited 0.Every gate, with its exit code
node scripts/check-adr-0087-registration.mjs --base origin/main:: exit 0node scripts/check-adr-0087-registration.mjs --self-test:: exit 0node scripts/check-changeset-no-major.mjs --base origin/main:: exit 0node scripts/check-closing-keyword-parity.mjs:: exit 0node scripts/check-closing-keyword-parity.mjs --self-test:: exit 0node scripts/check-comment-mask-adoption.mjs:: exit 0node scripts/check-comment-mask-adoption.mjs --self-test:: exit 0node scripts/check-comment-mask-corpus.mjs:: exit 0node scripts/check-dts-emitted.mjs --self-test:: exit 0node scripts/check-empty-changeset.mjs --base origin/main:: exit 0node scripts/check-empty-changeset.mjs --self-test:: exit 0node scripts/check-issue-citations.mjs:: exit 0node scripts/check-plugin-teardown-shape.mjs:: exit 0node scripts/check-plugin-teardown-shape.mjs --self-test:: exit 0node scripts/check-tenant-audit-census.mjs:: exit 0node scripts/check-tenant-audit-census.mjs --self-test:: exit 0node scripts/ci/assert-turbo-remote-cache.mjs --self-test:: exit 0node scripts/ci/plan-test-core-shards.mjs --self-test:: exit 0node scripts/ci/prune-turbo-cache.mjs --self-test:: exit 0node scripts/docs-audit/check-affected-docs.mjs:: exit 0node scripts/docs-audit/check-drift-comment.mjs:: exit 0node scripts/pm/release-rehearsal-clone.mjs --self-test:: exit 0node scripts/release-pending-publish.mjs --self-test:: exit 0pnpm --filter @objectstack/spec run check:duration-unit-keys:: exit 0pnpm --filter @objectstack/spec run check:empty-state:: exit 0pnpm --filter @objectstack/spec run check:liveness:: exit 0pnpm --filter @objectstack/spec run check:strictness-ledger:: exit 0pnpm --filter @objectstack/spec run check:variant-docs:: exit 0pnpm check:changeset-gate-self-tests:: exit 0pnpm check:ci-filter-parity:: exit 0pnpm check:closed-ceremony-text:: exit 0pnpm check:cross-package-test-inputs:: exit 0pnpm check:dispatcher-error-vocabulary:: exit 0pnpm check:doc-authoring:: exit 0pnpm check:driver-memory-census:: exit 0pnpm check:dts-closure:: exit 0pnpm check:dual-build-cjs-loads:: exit 0pnpm check:durability-log-level:: exit 0pnpm check:engine-double-contract:: exit 0pnpm check:error-status-conformance:: exit 0pnpm check:gitlink-declared:: exit 0pnpm check:issue-citations:: exit 0pnpm check:keyed-text-bounds:: exit 0pnpm check:lean-entry-closure:: exit 0pnpm check:logger-receiver-detach:: exit 0pnpm check:nul-bytes:: exit 0pnpm check:objectql-double-limit:: exit 0pnpm check:org-identifier:: exit 0pnpm check:page-declaration-shape:: exit 0pnpm check:platform-object-tenancy-census:: exit 0pnpm check:pm-changeset-deadline-census:: exit 0pnpm check:published-files:: exit 0pnpm check:query-options-erasure:: exit 0pnpm check:refd-timer-probe:: exit 0pnpm check:registry-log-declared:: exit 0pnpm check:rest-log-spy-declared:: exit 0pnpm check:route-envelope:: exit 0pnpm check:slot-lookup:: exit 0pnpm check:sourcemap-no-sources-content:: exit 0pnpm check:system-context-census:: exit 0pnpm check:test-source-alias:: exit 0pnpm check:tier-file-adoption:: exit 0pnpm check:type-check-coverage:: exit 0pnpm check:type-check-debt:: exit 0pnpm check:undeclared-dep-imports:: exit 0pnpm check:watch-hint-literal:: exit 0pnpm check:where-matcher:: exit 0pnpm 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 0Human Minimum
packages/specholds no runtime logic. (Nopackages/specchange.)pnpm lintpasses. (Scoped to the touched files; the repo-wide run is CI's.)main, one pull request.packages/spectouched: not touched.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5pwL1eE2BziiUM28wm