Repository navigation
fix(runtime): the action and security activation doors record which admin switched an item - #26551
Conversation
…e action and security doors (red on main) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5pwL1eE2BziiUM28wm
… who switched an action or a position (red on main) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5pwL1eE2BziiUM28wm
…edger row for the caller, elevated
The activation store wrote every row under a constant system context that
names nobody, so the ledger row's created_by/updated_by and its audit row's
user_id/actor came out null when an admin switched a packaged action or a
security catalog item off. The doors now hand down the caller's own context
elevated ({ ...callerContext, isSystem: true }) and the store writes with it;
a write no caller asked for keeps the system context.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pwL1eE2BziiUM28wm
…s measured; add the changeset Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5pwL1eE2BziiUM28wm
…rite options too Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5pwL1eE2BziiUM28wm
…type carries the elevation A runtime read of the handed context's isSystem is an elevation read site the system-context census requires a page row for. The type already requires isSystem: true and both doors build the context through one helper. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5pwL1eE2BziiUM28wm
📓 Docs Drift CheckThis PR changes 3 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 51 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 46ae06fb508f7be35f2639d056dbe018f620b18c && git checkout 46ae06fb508f7be35f2639d056dbe018f620b18c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f48aa45102fe34621b04f1ddd1d8defe765c797e c5672b2ff3170b11539e6c8c562c761f7f972379 && git checkout -B drift-repro f48aa45102fe34621b04f1ddd1d8defe765c797e && git merge --no-ff c5672b2ff3170b11539e6c8c562c761f7f972379
node scripts/docs-audit/affected-docs.mjs --json f48aa45102fe34621b04f1ddd1d8defe765c797e
|
Contract reviewServed-tier: Inputs read: card #26474 (body and all 6 comments: triage ① Derived judgmentsPublic-surface changes the diff implies
Accept-set changes. The two HTTP doors ( The write context
Tests and pins. The new pins are the right ones: the core store's Gate verdicts on the head (2026-10-27T08:01Z): 46 check-runs: 28 ② Semver level
③ Boundary flags
Implemented-by: Rendered by the seat's fable subagent; the seat adopts and posts. VERDICT: PASS |
席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, Part of #26474: the action door and catalog-activation half under ruling 6434876664 letter A. 16 files across core, objectql and runtime, plus a dogfood test. One patch changeset for the three published packages. Clause-②: yes (widening) and Metadata-protocol: none appear in both the body and the changeset. |
| Fix | Triage 6434156905. setActive(row, { context }) writes under the caller's context. Both doors build { ...callerContext, isSystem: true }, so the elevation is kept and the caller rides as the acting principal. The ledger row and the audit row now name the admin through the engine's own audit stamps, and no actor is stamped by hand. |
| Contract | Review 6436776614. The elevated context is built only after both authority gates, so nobody new can write. The ledger is tenant: false, so the caller's org neither filters the keyed read nor stamps a tenant column. The new optional parameters and exported types are the widening, declared right. |
| Clause-② | The dev measured yes (widening) where the claim guessed no. The seat accepts the measured line. |
| Evidence | CI on head c5672b2ff3: 40 succeeded, 8 expected skips, none failed or pending; mergeable clean. Red first on main at all four layers. The dogfood case (showcase plus AuditPlugin) went from 4 red (actor null) to 5 of 5; the dogfood gate does not run on PRs, so this local reading is in the body. The ablation turns it red again. 71 of 71 derived gates ran, with 0 NOT-MEASURED. |
| Card | The card stays open. The flow-toggle half waits on #26506, so the card moves to pm:blocked with Blocked-by: #26506 after this lands. |
| 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
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 ruling6434876664, letter A).Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
created_by/updated_by和对应审计行的user_id/actor现在记的是这位管理员,不再是空。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 typeMetadataActivationWriteContextrequiresisSystem: true, so a door cannot pass the caller's context down without the elevation. There is deliberately no runtime read ofisSystemin the store: that would be a new elevation read site forcheck: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.MetadataActivationWriteOptionsis exported, and theMetadataActivationStoreinterface carries the optional parameter.@objectstack/objectql:ObjectQL.setActionActive(row, options?)andActionActivationProjection.setActive(row, options?)pass the options to the store unchanged.ActionActivationWriteOptionsis exported.@objectstack/runtime: a newactivationWriteContext(context)indomains/activation-gate.tsreturns{ ...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_byfrom itsuserId, and the audit plugin readsuser_id/actorfrom the same session. No actor is stamped by hand on any row. This is thesudo()-shaped elevation thatObjectRepository.executedocuments. It is deliberately notbuildActionExecutionContext, 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 commits6eed608aand2e0d5df6precede the fix commit773a999a. The red was measured with the test commits on top of unchangedmainsource and a build of that source.mainmetadata-activation-store.test.ts, new describe "the write names the caller who asked for it" (3 cases)expected { isSystem: true, positions: [], … } to deeply equal { userId: 'usr_admin', … }); the no-caller case is a control and passes on bothaction-activation.test.ts, "setActionActive hands the caller's write context through to the durable store"action-activation-posture-gate.test.ts, "the activation write names the caller who asked for it" (tenant admin undersingle, operator underisolated)expected undefined to deeply equal { userId: ... })catalog-activation-door.test.ts, "the write names the caller who asked for it" (position,permission: insert then update)activation-ledger-actor.dogfood.test.ts(showcase boot plusAuditPlugin, signed-in admin; action and position, off then on; ledger row and audit row)expected null to bethe admin id, oncreated_byandupdated_by)Existing pins that read
setActionActive's single argument now read the second one withexpect.anything()(action-activation-posture-gate.test.ts,tenancy-posture-outage-gates.test.ts,scoped-service-miss-attribution.test.ts). Theaction-activation-unpackaged.test.tsdouble now forwards the options to the real projection and store.Ablation (one behaviour, one mutation): the doors hand down a context with no caller
scripts/ablation-replace.mjsonpackages/runtime/src/domains/activation-gate.ts. The anchor wasreturn { ...caller, isSystem: true };, replaced by a marker-carrying{ isSystem: true }. Thenpnpm --filter @objectstack/runtime build, andablation-dist-preflight.mjs @objectstack/runtime ABLATE_26474(exit 0, marker in dist).expected null to bethe admin id.651aa0c0),git diff HEADis empty, rebuilt,--absentpreflight exit 0. Dogfood 5/5 pass.Package suites, run under the verify lock. The final head is
c5672b2f; its last commit changes only@objectstack/core:c5672b2f:pnpm --filter @objectstack/core testpassed 122 files and 2791 tests.typecheckfor core, objectql and runtime exited 0. Runtimesrc/domainspassed 148 files and 4156 tests, and objectqlaction-activation.test.tspassed 19 tests. Dogfood passed 3 files and 24 tests:activation-ledger-actor,catalog-activation-doorandpackaged-activation-ledger-reach, on a rebuilt core.57a1b5e9(the same runtime and objectql source):pnpm --filter @objectstack/objectql testpassed 472 files and 8790 tests.pnpm --filter @objectstack/runtime testpassed 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
POST /api/v1/automation/:name/togglereaches the ledger only throughIAutomationService.toggleFlow(name, enabled)inpackages/spec, which has no caller channel, and the server lane does not edit spec. It waits on 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 and stays the open half of 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. Its ledger row and audit row are still null today.tenant_idfrom the session'sorganizationId, so an activation audit row written for a caller who has an active organization now carries that organization instead of null. Undergroup/isolatedonly the platform operator can write this ledger, and the operator's context has no organization.system): a different code path. The clone arm saves throughregisterAndSaveFlow(metadata protocol save, thePutOptions.actorfamily), not through the activation ledger. It is not carried here; see seat ruling6434876664item 3.Verification
Gates are derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon this diff (71 commands), pluseslint --no-inline-configscoped to the touched files. All were run atc5672b2f, and every exit code was captured before any pipe.--ranreconciliation: 71 derived, 71 run, 0 NOT-MEASURED, 0 UNRUN.pnpm check:system-context-census([file-without-a-row]): a runtimeisSystemread in the store counted as a new elevation read site. Commitc5672b2fdrops that read, because the type carries the elevation, and the re-run is green.check-engine-split-ratio --days 90andcheck-plugin-teardown-shape --self-testfirst refused on the shallow clone (exit 2 and 3: a prerequisite, so nothing was measured). Aftergit fetch --shallow-since=2026-07-22 origin mainand a fetch of the self-test's control commit621a4876, both exit 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-dev-prereqs.mjs --self-test:: 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-engine-split-ratio.mjs --days 90:: exit 0node scripts/check-engine-split-ratio.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/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:kernel-hook-pairs:: 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:stack-collection-maps:: exit 0pnpm check:swallow-census-controls:: 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/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 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 by Claude Code