Repository navigation
Commit 8c0306f
fix(service-automation): the flow toggle records which admin switched a packaged flow (#26605)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XTXjaSjCiT9wP1UtogZiA8r)_
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
## 维护者速读
- **改了什么:** 管理员通过 `POST /api/v1/automation/:name/toggle` 关掉或打开一个打包的 flow
时,激活台账行的 `created_by` / `updated_by` 和对应审计行的 `user_id` / `actor`
现在记的是这位管理员,不再是空。至此 #26474 的三个开关门(action、安全目录、flow)都能说出是谁操作的。
- **为什么改:** flow 门经由 `IAutomationService.toggleFlow`
写台账,以前这个契约带不了调用者,只能用不带用户的系统身份写入。spec 卡 #26506 已在 #26545 补上 `caller` 参数,本
PR 把它接通。
- **风险与回滚:** 写入仍是提权写(权限不变),只多带了用户 id;`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 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 #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/`).
<details><summary>Ablations (one behaviour, one mutation each; every
restore proved blob == HEAD and an empty `git diff HEAD`)</summary>
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).
</details>
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 #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.
<details><summary>Every gate, with its exit code</summary>
- `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
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(No `packages/spec` change.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes. (Scoped to the touched
files; the repo-wide run is CI's.)
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: not touched.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English. (The 维护者速读 block is
the maintainer-facing Chinese channel.)
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01FEkQ5pwL1eE2BziiUM28wm
Co-authored-by: Claude <noreply@anthropic.com>1 parent 7c34e5d commit 8c0306f
16 files changed
Lines changed: 269 additions & 44 deletions
File tree
- .changeset
- packages
- qa/dogfood/test
- runtime/src
- domains
- services/service-automation/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | | - | |
15 | | - | |
16 | | - | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
25 | | - | |
26 | | - | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| 43 | + | |
| 44 | + | |
43 | 45 | | |
44 | 46 | | |
45 | 47 | | |
| |||
89 | 91 | | |
90 | 92 | | |
91 | 93 | | |
| 94 | + | |
92 | 95 | | |
93 | 96 | | |
94 | 97 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
256 | 256 | | |
257 | 257 | | |
258 | 258 | | |
259 | | - | |
| 259 | + | |
260 | 260 | | |
261 | 261 | | |
262 | 262 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
439 | 439 | | |
440 | 440 | | |
441 | 441 | | |
442 | | - | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
443 | 446 | | |
444 | 447 | | |
445 | 448 | | |
446 | 449 | | |
447 | 450 | | |
448 | 451 | | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
156 | 156 | | |
157 | 157 | | |
158 | 158 | | |
159 | | - | |
| 159 | + | |
160 | 160 | | |
161 | 161 | | |
162 | 162 | | |
| |||
165 | 165 | | |
166 | 166 | | |
167 | 167 | | |
168 | | - | |
| 168 | + | |
169 | 169 | | |
170 | 170 | | |
171 | 171 | | |
| |||
223 | 223 | | |
224 | 224 | | |
225 | 225 | | |
226 | | - | |
| 226 | + | |
227 | 227 | | |
228 | 228 | | |
229 | 229 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
129 | 129 | | |
130 | 130 | | |
131 | 131 | | |
132 | | - | |
| 132 | + | |
133 | 133 | | |
134 | 134 | | |
135 | 135 | | |
136 | 136 | | |
137 | 137 | | |
138 | 138 | | |
139 | | - | |
| 139 | + | |
140 | 140 | | |
141 | 141 | | |
142 | 142 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
365 | 365 | | |
366 | 366 | | |
367 | 367 | | |
368 | | - | |
| 368 | + | |
369 | 369 | | |
370 | 370 | | |
371 | 371 | | |
| |||
396 | 396 | | |
397 | 397 | | |
398 | 398 | | |
399 | | - | |
| 399 | + | |
400 | 400 | | |
401 | 401 | | |
402 | 402 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
298 | 298 | | |
299 | 299 | | |
300 | 300 | | |
301 | | - | |
| 301 | + | |
302 | 302 | | |
303 | 303 | | |
304 | 304 | | |
305 | | - | |
| 305 | + | |
306 | 306 | | |
307 | 307 | | |
308 | 308 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
112 | 112 | | |
113 | 113 | | |
114 | 114 | | |
115 | | - | |
| 115 | + | |
116 | 116 | | |
117 | 117 | | |
118 | 118 | | |
119 | 119 | | |
120 | | - | |
| 120 | + | |
121 | 121 | | |
122 | 122 | | |
123 | 123 | | |
124 | 124 | | |
125 | 125 | | |
126 | | - | |
| 126 | + | |
127 | 127 | | |
128 | 128 | | |
129 | 129 | | |
| |||
171 | 171 | | |
172 | 172 | | |
173 | 173 | | |
174 | | - | |
| 174 | + | |
175 | 175 | | |
176 | 176 | | |
0 commit comments