Repository navigation
Commit a98693d
fix(runtime): the action and security activation doors record which admin switched an item (#26551)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XTXjaSjCiT9wP1UtogZiA8r)_
Part of #26474. This PR covers the action door and the security catalog
door. The flow toggle half stays open on the card and waits on spec card
#26506, which declares a caller channel on
`IAutomationService.toggleFlow` (seat ruling `6434876664`, letter A).
Clause-②: yes (widening)
Metadata-protocol: none
## 维护者速读
- **改了什么:** 管理员关掉一个打包的 action,或关掉安全目录里的职位、权限集、共享规则、webhook 时,激活台账行的
`created_by` / `updated_by` 和对应审计行的 `user_id` / `actor` 现在记的是这位管理员,不再是空。
- **为什么改:** 以前台账一律用不带用户的系统身份写入,审计记录说不出是谁停用了出厂功能。
- **风险与回滚:** 写入仍是提权写(权限不变),只是多带了调用者身份;没有调用者的写入(启动期等)照旧用系统身份。回滚 = revert
本 PR。flow 开关还没改,等 spec 卡 #26506。
- **先看:**
`packages/core/src/utils/metadata-activation-store.ts`(`setActive` 的
context 与 `MetadataActivationWriteContext`
类型)、`packages/runtime/src/domains/activation-gate.ts`(`activationWriteContext`)、`packages/runtime/src/domains/catalog-activation.ts`
写入处。
- **两分钟验证:** `pnpm --filter @objectstack/dogfood exec vitest run
test/activation-ledger-actor.dogfood.test.ts`(先按 CI 建好 dogfood 依赖闭包),预期
5/5 通过,台账行与审计行都写着登录管理员的 id。
- **需要你做的:** 无,席位落地。
## What changes
- **`@objectstack/core`:**
`ObjectStoreMetadataActivationStore.setActive(row, options?)` takes `{
context }`, which is the caller the write is made for. The keyed read
and the insert or update all run under that context. The new type
`MetadataActivationWriteContext` requires `isSystem: true`, so a door
cannot pass the caller's context down without the elevation. There is
deliberately no runtime read of `isSystem` in the store: that would be a
new elevation read site for `check:system-context-census`, and the type
plus the one shared runtime helper already carry it. Without a context
the store keeps its system context, so a write that no caller requested
(boot time, in-process) still names nobody.
`MetadataActivationWriteOptions` is exported, and the
`MetadataActivationStore` interface carries the optional parameter.
- **`@objectstack/objectql`:** `ObjectQL.setActionActive(row, options?)`
and `ActionActivationProjection.setActive(row, options?)` pass the
options to the store unchanged. `ActionActivationWriteOptions` is
exported.
- **`@objectstack/runtime`:** a new `activationWriteContext(context)` in
`domains/activation-gate.ts` returns `{ ...callerContext, isSystem: true
}`, the caller's own execution context, elevated. Both doors call it
after their two authority gates admit the caller:
- `POST /api/v1/actions/_activation/:object/:action`
(`domains/actions.ts`)
- `POST /api/v1/security/_activation/:type/:name`
(`domains/catalog-activation.ts`), which covers positions, permission
sets, sharing rules and webhooks.
**Mechanism.** The elevation is kept, because the ledger object is
engine-owned and only an elevated write reaches it. The caller is
carried as the acting principal in the same context. The engine stamps
`created_by` / `updated_by` from its `userId`, and the audit plugin
reads `user_id` / `actor` from the same session. No actor is stamped by
hand on any row. This is the `sudo()`-shaped elevation that
`ObjectRepository.execute` documents. It is deliberately not
`buildActionExecutionContext`, which also marks the context as an
app-automation elevation.
## Why
Card #26474 (QA source #26468): on a stock showcase, a signed-in admin
switched off a packaged flow and a packaged action, and all four actor
fields came out null. The cause was that every door ended in
`ObjectStoreMetadataActivationStore.setActive`, which wrote with a
constant `{ isSystem: true, positions: [], permissions: [] }` carrying
no user.
## Tests
Red first on `main` (`98c02577`): the test commits `6eed608a` and
`2e0d5df6` precede the fix commit `773a999a`. The red was measured with
the test commits on top of unchanged `main` source and a build of that
source.
| Suite | On `main` | After the fix |
|:--|:--|:--|
| core `metadata-activation-store.test.ts`, new describe "the write
names the caller who asked for it" (3 cases) | the insert and update
cases failed (`expected { isSystem: true, positions: [], … } to deeply
equal { userId: 'usr_admin', … }`); the no-caller case is a control and
passes on both | pass |
| objectql `action-activation.test.ts`, "setActionActive hands the
caller's write context through to the durable store" | failed | pass |
| runtime `action-activation-posture-gate.test.ts`, "the activation
write names the caller who asked for it" (tenant admin under `single`,
operator under `isolated`) | 2 failed (`expected undefined to deeply
equal { userId: ... }`) | pass |
| runtime `catalog-activation-door.test.ts`, "the write names the caller
who asked for it" (`position`, `permission`: insert then update) | 2
failed (both writes ran under the bare system context) | pass |
| dogfood `activation-ledger-actor.dogfood.test.ts` (showcase boot plus
`AuditPlugin`, signed-in admin; action and position, off then on; ledger
row and audit row) | 4 failed (`expected null to be` the admin id, on
`created_by` and `updated_by`) | 5/5 pass |
Existing pins that read `setActionActive`'s single argument now read the
second one with `expect.anything()`
(`action-activation-posture-gate.test.ts`,
`tenancy-posture-outage-gates.test.ts`,
`scoped-service-miss-attribution.test.ts`). The
`action-activation-unpackaged.test.ts` double now forwards the options
to the real projection and store.
<details><summary>Ablation (one behaviour, one mutation): the doors hand
down a context with no caller</summary>
`scripts/ablation-replace.mjs` on
`packages/runtime/src/domains/activation-gate.ts`. The anchor was
`return { ...caller, isSystem: true };`, replaced by a marker-carrying
`{ isSystem: true }`. Then `pnpm --filter @objectstack/runtime build`,
and `ablation-dist-preflight.mjs @objectstack/runtime ABLATE_26474`
(exit 0, marker in dist).
- Ablated: runtime door suites, 5 failed (the 4 new caller pins, plus
the legacy empty-body pin that this run exposed as still reading one
argument; that pin is fixed in a later commit). Dogfood: 4 failed,
`expected null to be` the admin id.
- Restored: the blob equals HEAD (`651aa0c0`), `git diff HEAD` is empty,
rebuilt, `--absent` preflight exit 0. Dogfood 5/5 pass.
</details>
Package suites, run under the verify lock. The final head is `c5672b2f`;
its last commit changes only `@objectstack/core`:
- At `c5672b2f`: `pnpm --filter @objectstack/core test` passed 122 files
and 2791 tests. `typecheck` for core, objectql and runtime exited 0.
Runtime `src/domains` passed 148 files and 4156 tests, and objectql
`action-activation.test.ts` passed 19 tests. Dogfood passed 3 files and
24 tests: `activation-ledger-actor`, `catalog-activation-door` and
`packaged-activation-ledger-reach`, on a rebuilt core.
- At `57a1b5e9` (the same runtime and objectql source): `pnpm --filter
@objectstack/objectql test` passed 472 files and 8790 tests. `pnpm
--filter @objectstack/runtime test` passed 457 files and 7426 tests,
with 19 skipped.
The dogfood regression gate does not run on PRs or in the merge queue.
The dogfood numbers above are the local run.
## Acceptance notes
- **Flow door: not in this PR.** `POST /api/v1/automation/:name/toggle`
reaches the ledger only through `IAutomationService.toggleFlow(name,
enabled)` in `packages/spec`, which has no caller channel, and the
server lane does not edit spec. It waits on #26506 and stays the open
half of #26474. Its ledger row and audit row are still null today.
- **Audit row organization.** This is read from code and not asserted.
The audit writer stamps `tenant_id` from the session's `organizationId`,
so an activation audit row written for a caller who has an active
organization now carries that organization instead of null. Under
`group` / `isolated` only the platform operator can write this ledger,
and the operator's context has no organization.
- **Side note from the card (clone audited as `system`):** a different
code path. The clone arm saves through `registerAndSaveFlow` (metadata
protocol save, the `PutOptions.actor` family), not through the
activation ledger. It is not carried here; see seat ruling `6434876664`
item 3.
## Verification
Gates are derived with `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` on this diff (71 commands), plus
`eslint --no-inline-config` scoped to the touched files. All were run at
`c5672b2f`, and every exit code was captured before any pipe. `--ran`
reconciliation: **71 derived, 71 run, 0 NOT-MEASURED, 0 UNRUN**.
- The first sweep reddened `pnpm check:system-context-census`
(`[file-without-a-row]`): a runtime `isSystem` read in the store counted
as a new elevation read site. Commit `c5672b2f` drops that read, because
the type carries the elevation, and the re-run is green.
- `check-engine-split-ratio --days 90` and `check-plugin-teardown-shape
--self-test` first refused on the shallow clone (exit 2 and 3: a
prerequisite, so nothing was measured). After `git fetch
--shallow-since=2026-07-22 origin main` and a fetch of the self-test's
control commit `621a4876`, both exit 0.
- Not local: the CI-only type-check lanes and the Build Core job, which
are read from this PR's check runs. The dogfood regression gate does not
run on PRs.
<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-dev-prereqs.mjs --self-test` :: 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-engine-split-ratio.mjs --days 90` :: exit 0
- `node scripts/check-engine-split-ratio.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/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:kernel-hook-pairs` :: 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:stack-collection-maps` :: exit 0
- `pnpm check:swallow-census-controls` :: 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/core/src/utils
packages/objectql/src/action-activation.ts
packages/objectql/src/action-activation.test.ts
packages/objectql/src/engine.ts packages/objectql/src/index.ts
packages/runtime/src/domains
packages/qa/dogfood/test/activation-ledger-actor.dogfood.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 by [Claude
Code](https://claude.ai/code/session_01FEkQ5pwL1eE2BziiUM28wm)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent b53d9f3 commit a98693d
16 files changed
Lines changed: 394 additions & 40 deletions
File tree
- .changeset
- packages
- core/src/utils
- objectql/src
- qa/dogfood/test
- runtime/src/domains
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
346 | 346 | | |
347 | 347 | | |
348 | 348 | | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
349 | 392 | | |
350 | 393 | | |
351 | 394 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
82 | 82 | | |
83 | 83 | | |
84 | 84 | | |
85 | | - | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
86 | 90 | | |
87 | 91 | | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
88 | 125 | | |
89 | 126 | | |
90 | 127 | | |
| |||
120 | 157 | | |
121 | 158 | | |
122 | 159 | | |
123 | | - | |
124 | | - | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
125 | 165 | | |
126 | 166 | | |
127 | 167 | | |
| |||
176 | 216 | | |
177 | 217 | | |
178 | 218 | | |
179 | | - | |
| 219 | + | |
| 220 | + | |
180 | 221 | | |
181 | 222 | | |
182 | 223 | | |
| |||
187 | 228 | | |
188 | 229 | | |
189 | 230 | | |
190 | | - | |
191 | | - | |
| 231 | + | |
| 232 | + | |
192 | 233 | | |
193 | | - | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
194 | 238 | | |
195 | 239 | | |
196 | 240 | | |
| |||
256 | 300 | | |
257 | 301 | | |
258 | 302 | | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
259 | 308 | | |
260 | | - | |
| 309 | + | |
261 | 310 | | |
| 311 | + | |
262 | 312 | | |
263 | 313 | | |
264 | 314 | | |
265 | | - | |
| 315 | + | |
266 | 316 | | |
267 | 317 | | |
268 | 318 | | |
269 | 319 | | |
270 | 320 | | |
271 | 321 | | |
272 | 322 | | |
273 | | - | |
| 323 | + | |
274 | 324 | | |
275 | 325 | | |
276 | 326 | | |
| |||
283 | 333 | | |
284 | 334 | | |
285 | 335 | | |
286 | | - | |
| 336 | + | |
287 | 337 | | |
288 | 338 | | |
289 | 339 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
366 | 366 | | |
367 | 367 | | |
368 | 368 | | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
369 | 386 | | |
370 | 387 | | |
371 | 388 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
| 97 | + | |
97 | 98 | | |
98 | 99 | | |
99 | 100 | | |
| |||
142 | 143 | | |
143 | 144 | | |
144 | 145 | | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
145 | 153 | | |
146 | 154 | | |
147 | 155 | | |
| |||
253 | 261 | | |
254 | 262 | | |
255 | 263 | | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
256 | 267 | | |
257 | | - | |
| 268 | + | |
258 | 269 | | |
259 | 270 | | |
260 | 271 | | |
| |||
269 | 280 | | |
270 | 281 | | |
271 | 282 | | |
272 | | - | |
| 283 | + | |
273 | 284 | | |
274 | 285 | | |
275 | 286 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
52 | | - | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
53 | 58 | | |
54 | 59 | | |
55 | 60 | | |
| |||
5755 | 5760 | | |
5756 | 5761 | | |
5757 | 5762 | | |
5758 | | - | |
| 5763 | + | |
| 5764 | + | |
| 5765 | + | |
5759 | 5766 | | |
5760 | | - | |
5761 | | - | |
| 5767 | + | |
| 5768 | + | |
5762 | 5769 | | |
5763 | 5770 | | |
5764 | 5771 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
| 33 | + | |
33 | 34 | | |
34 | 35 | | |
35 | 36 | | |
| |||
0 commit comments