Skip to content

fix(runtime): the action and security activation doors record which admin switched an item - #26551

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

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-ugprlu

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 ruling 6434876664, letter A).

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

维护者速读

  • 改了什么: 管理员关掉一个打包的 action,或关掉安全目录里的职位、权限集、共享规则、webhook 时,激活台账行的 created_by / updated_by 和对应审计行的 user_id / actor 现在记的是这位管理员,不再是空。
  • 为什么改: 以前台账一律用不带用户的系统身份写入,审计记录说不出是谁停用了出厂功能。
  • 风险与回滚: 写入仍是提权写(权限不变),只是多带了调用者身份;没有调用者的写入(启动期等)照旧用系统身份。回滚 = revert 本 PR。flow 开关还没改,等 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。
  • 先看: 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.

Ablation (one behaviour, one mutation): the doors hand down a context with no caller

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.

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

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.
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-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

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 by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/core, @objectstack/objectql, @objectstack/runtime, touching 15 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/objectql/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/permissions/sharing-rules.mdx (via handleCatalogActivationWrite (symbol, a top-level function))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/objectql/src/index.ts) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 72 pages)
  • 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 — 51 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 f48aa45102fe34621b04f1ddd1d8defe765c797e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 46ae06fb508f7be35f2639d056dbe018f620b18c — the merge of head c5672b2ff3170b11539e6c8c562c761f7f972379 into base f48aa45102fe34621b04f1ddd1d8defe765c797e, 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 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

⚠️ 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 f48aa45102fe34621b04f1ddd1d8defe765c797e → 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: c5672b2ff3170b11539e6c8c562c761f7f972379
Local-runs: none

Inputs read: card #26474 (body and all 6 comments: triage 6434156905, claim 6434820232, ruling 6434876664, amended claim 6434884699, spec handoff 6436285999, dev report 6436600379), PR #26551 body and 16-file list, git diff origin/main...c5672b2f (394+/40-), cited code read with git show at the head, and the head's check-runs (read 2026-10-27T07:55Z and again 2026-10-27T08:01Z).

① Derived judgments

Public-surface changes the diff implies

  1. @objectstack/core — MetadataActivationStore.setActive(row, options?): an optional second parameter on a published interface. Callers may now pass it, implementers need not change. Right (widening, additive).
  2. @objectstack/core — ObjectStoreMetadataActivationStore.setActive and InMemoryMetadataActivationStore.setActive take the same optional parameter; the in-memory store ignores it and says so. Right.
  3. @objectstack/core — new exported types MetadataActivationWriteContext ({ readonly isSystem: true; readonly userId?: string } & Record<string, unknown>) and MetadataActivationWriteOptions, reachable through export * from './utils/metadata-activation-store.js' (packages/core/src/index.ts:187). The isSystem: true literal in the type is the compile-time guard; the store has no runtime isSystem read, which is what keeps check:system-context-census green. Right.
  4. @objectstack/objectql — ObjectQL.setActionActive(row, options?), ActionActivationProjection.setActive(row, options?), and the type export ActionActivationWriteOptions from index.ts. packages/spec declares no setActionActive on IObjectQLEngine (grep at the head: no hit), so no spec change is owed and the lane red line holds; runtime calls it through ql: any. Right.
  5. @objectstack/runtime — activationWriteContext is internal: packages/runtime/src/index.ts exports nothing from domains/ and package.json exposes only ".". Runtime publishes a behaviour change, no surface change. Right.

Accept-set changes. The two HTTP doors (POST /api/v1/actions/_activation/:object/:action, POST /api/v1/security/_activation/:type/:name) keep the same body reader, gates, declaration lookup and status pairs; their accept-set is unchanged. The store and engine accept-set widens by the optional { context }. Absent, the store keeps SYSTEM_CTX (pinned by the no-caller control). Right.

The write context { ...callerContext, isSystem: true } (the brief's extra question)

  • Ordering. Both doors build it only at the write step, after refuseUngrantedActivationAuthoring and refuseUngrantedActivationWrite admitted the caller: actions.ts step 1 then step 5; catalog-activation.ts:237,239 then the write at :399-405. Neither gate is touched by the diff.
  • Nothing newly admitted. The engine's isSystem bypasses RBAC, RLS and FLS (engine.ts:9704), exactly as the old constant SYSTEM_CTX did, and { ...context, isSystem: true } is the engine's own documented sudo() shape (engine.ts:9707, :12892-12916). A caller the gates refuse never reaches the write; a caller they admit was already elevated to the full system write before this PR. No permission or tenancy check that was enforced before is skipped now.
  • Tenancy. sys_metadata_activation declares systemFields: { tenant: false } (packages/platform-objects/src/system/sys-metadata-activation.object.ts:112) and the registry honours the opt-out (registry.ts:876, :1208). So the caller's organizationId neither tenant-filters the keyed read (the read-then-write stays keyed on type + name; no duplicate-row risk) nor stamps a tenant column on the ledger row (plugin.ts:1600 is gated on hasField(objectName, 'tenant_id')).
  • Actor recorded, not hand-stamped. The diff's source files assign no created_by, updated_by, user_id or actor literal. The ledger row's stamps are the engine's sys_stamp_audit_* hooks (plugin.ts:1594-1597, keyed on session?.userId, no isSystem exemption, session being hookCtx.session from the write's context). The audit row's are the audit plugin's user_id: userId ?? null and actor = userId ?? sess.actor with userId = sess.userId ?? attributedUserId (audit-writers.ts:2016, :2125, actorLabel). Both read the same context the door handed down, so the actor is the caller's. Right.
  • Residual observation, not a finding. With no executionContext the helper yields a bare { isSystem: true } without positions: [] / permissions: []; that shape is unreachable past the two gates and the engine takes it elsewhere.

Tests and pins. The new pins are the right ones: the core store's find/insert/update contexts and the no-caller control; objectql pass-through; both runtime doors handing down exactly { ...executionContext, isSystem: true } (tenant admin under single, operator under isolated; the catalog door on insert and the later update); dogfood on a real engine plus AuditPlugin for both rows, off then on. The dogfood harness surface it uses exists (BootOptions.extraPlugins harness.ts:511, signIn :170, apiAs :176). The five existing toHaveBeenCalledWith pins relaxed to expect.anything() pin the row, not the context, and the new describes pin the context. Right. Red-first is claimed with commit order (6eed608a, 2e0d5df6 before 773a999a) and per-suite failure text; read as claimed, not re-run.

Gate verdicts on the head (2026-10-27T08:01Z): 46 check-runs: 28 success, 8 skipped, 0 failure, 10 in_progress. Still running: Build Core, Test Core (1/8) through (7/8), Type Check UI, Type Check · workspace. Completed green include Check Changeset, Governed Surface Queue Guard, Part-of PR must not also close its card, The card this PR closes must claim this branch, No other open PR may claim the same issue, Lint · PM dispatch-gates, all Lint · lanes, Type Check · consumer gates / debt ledger / source gates, Test Core (8/8), Dogfood Regression Gate (its PR-side no-op).

② Semver level

  • Changeset .changeset/26474-activation-ledger-actor.md: patch for @objectstack/core, @objectstack/objectql, @objectstack/runtime. @objectstack/dogfood is private: true, so no changeset is owed for the new dogfood file. The three packages are exactly the ones the diff publishes from. Matches.
  • Level rule (AGENTS.md Post-Task Checklist step 3): the level follows the Metadata-protocol kind alone; none takes patch; a non-protocol widening ships patch. The diff changes no metadata key, value or type. patch right.
  • Declarations: PR body and changeset body both carry, line-initial, Clause-②: yes (widening) and Metadata-protocol: none. yes (widening) is a well-formed arm of the closed pair; yes is right because the published store/engine accept-set widens and nothing narrows or breaks. Check Changeset is success.
  • Clause-②: yes (widening)

③ Boundary flags

  • open_questions: [] in report 6436600379. None to answer.
  • Dev flag — flow door still null-actor (out_of_scope_findings[0]): answered. 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 behind spec card 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 per ruling 6434876664; the PR says Part of #26474 and the Part-of PR must not also close its card run is green. The spec handoff 6436285999 names 18 pins in 8 runtime test files and three stale docblocks for the flow half; none of those files is in this PR's list, so nothing there is owed here. The card takes Blocked-by: #26506 / pm:blocked once this lands, per the ruling's item 2.
  • Dev flag — audit row tenant_id now carries the caller's organization (out_of_scope_findings[1], carrier none): answered, accepted, no card owed. Confirmed in code: audit-writers.ts sets tenantId = recordOrgId ?? sess.organizationId, and the ledger row has no organization column, so the activation audit row's tenant_id becomes the acting admin's organization. Effect: under the sys_audit_log_org row policy that row is readable to organization readers of the admin's own organization instead of platform admins only. Reachable only under single, since under wall-enforcing postures refuseUngrantedActivationWrite admits PLATFORM_ADMIN alone (activation-gate.ts). This is ADR-0131 D7's own meaning of the column (the organization the row is about) and is recorded in the PR's Acceptance notes; it is not escalated.
  • Dev flag — clone-as-system side note (out_of_scope_findings[2]): answered. Ruling 6434876664 item 3 carries it on the registerAndSaveFlow / PutOptions.actor path; not this PR.
  • Dev flag — the first sweep's check:system-context-census red: answered. Resolved in c5672b2f by dropping the store's runtime isSystem read; the head's store has none, and the census lane is green on the head.
  • Dev flag — CI in progress at report time: recorded above; ten runs were still in progress at review time, none had failed. The seat's adoption follows their completion.
  • The dev report's earlier copy on Scheduled flows: record a run whose time passed while the hosted kernel was not ticking as missed, visible in the run history (no replay) #25235 is ignored per the brief.

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

Rendered by the seat's fable subagent; the seat adopts and posts.

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

Head c5672b2ff3, report 6436600379 (on #26474), amended claim 6434884699, contract review 6436776614 (TIER_DEFAULT, declaration face, PASS on this head).

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

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

Development

Successfully merging this pull request may close these issues.

2 participants