Repository navigation
feat(plugin-security)!: refuse a principal-less, non-system data-engine context (ADR-0096 D5 strict mode) - #22297
Conversation
…t the engine (ADR-0096 D5 strict mode) The engine middleware refuses a context that carries no principal and is not a system one with PermissionDeniedError (403 PERMISSION_DENIED), before anything resolves. The object-admission probes answer false and the row scope is the deny sentinel, read off the same predicate. The field projections no longer settle such a context early. Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ Co-authored-by: Claude <noreply@anthropic.com>
…xplicit system opt-in
The publish path's draft read and org-scope probes, and the store reads and
writes behind the history, audit, diff, commit, migration and code-only
delete doors, reached the data engine with no context. Each now passes
context: { isSystem: true }, as the protocol's other store calls do. The pin
drives each producer and asserts every engine call it issues is a system one.
Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ
Co-authored-by: Claude <noreply@anthropic.com>
…carry one A fixture passed the system context as the options argument, a fixture inserted a store row and read seeded rows with no context, and an engine double dropped the trailing read options that carry the permission-set loader's system opt-in. Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ Co-authored-by: Claude <noreply@anthropic.com>
…refusal pins A principal-less, non-system context is refused on read and write in every spelling, at the middleware and the object-admission probes, with the deny row scope. A system context is unchanged, and a context carrying a principal without a user id is decided by what it carries. The runtime pin composes the dispatcher's identity-resolution fault class with the real middleware; the dogfood pin resolves an owner_of audience on a private object end to end. Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ Co-authored-by: Claude <noreply@anthropic.com>
…etadata-protocol opt-ins (patch) Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 24 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 b49796f36f5fa21598680b59709d13f830252d96 && git checkout b49796f36f5fa21598680b59709d13f830252d96
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4578c56e65c1f50f04da9259579091272d319558 454d0e89b9397900c6d4b01bd30e8e505d886bbe && git checkout -B drift-repro 4578c56e65c1f50f04da9259579091272d319558 && git merge --no-ff 454d0e89b9397900c6d4b01bd30e8e505d886bbe
node scripts/docs-audit/affected-docs.mjs --json 4578c56e65c1f50f04da9259579091272d319558
|
…and the telemetry gate (objectstack-ai#22298) Refs objectstack-ai#21908 **Tier H — governed surface (`docs/adr/**`).** This PR lands only on the maintainer's approval: an authorized APPROVED review, or the maintainer's own merge. No seat readies, queues or arms it. ## Summary A dated note under ADR-0096 D5 records the maintainer's ruling Q3 (甲) on objectstack-ai#21908 ([comment 6019001864](objectstack-ai#21908 (comment)), 2026-10-06). The note says: - strict mode lands ON at the security plugin's hand-off sites, ahead of the D2 `systemContext(reason)` door and the telemetry gate; - the explicit opt-in is the wire-level `isSystem: true` flag, which is E1's own prescription; - no `security.identityStrict` switch is built, so there is no OFF mode. The note cites what licenses that order: the published contract sentence on `ChatWithToolsOptions.toolExecutionContext` ("unauthenticated (RLS-on, sees-nothing)") and the maintainer's ruling 2B on objectstack-ai/cloud#2006. The ADR's own text is not rewritten, and its status stays `Proposed`. The behaviour the note records ships in objectstack-ai#22297, the deny round, which is a separate PR and not governed. ## The diff One blockquote note, 2 added lines, placed at the end of D5 and before D6. Nothing else in ADR-0096 changes. ## 维护者速读(草稿) ### 改了什么 在 ADR-0096 的 D5(严格模式)末尾加一段带日期的注记,不改动 ADR 原文与状态。注记记录:严格模式现在就默认开启,先于 D2 的 `systemContext(reason)` 入口和遥测门;显式提权沿用线上已有的 `isSystem: true`;不提供关闭开关。 ### 为什么改 你在 objectstack-ai#21908 的 Q3 选了「甲」:只加注记,不重写 ADR。代码那一半(拒绝没有主体的数据引擎调用)在 objectstack-ai#22297。ADR 是受管面,所以单独成 PR,由你批准。 ### 风险与代价(含回滚) 纯文档,对运行时零影响。回滚就是撤掉这两行。真正的行为风险在 objectstack-ai#22297(进程内未带上下文的调用会被 403 拒绝),那边有迁移说明和合同级复核。 ### 席位意见 ### 你要做的 读一遍注记的措辞,同意就批准(APPROVED);要改措辞就在 PR 上留言。 ## Evidence - `dispatch-gates --commands` derived 19 families at `2becf9015f`. All 19 ran and exited 0. `--ran`: "19 derived famil(ies) accounted for — 19 run, 0 NOT-MEASURED (a DERIVED zero — all 19 recorded an exit code and none of them is 3)". The first `check:doc-formula-expressions` run exited 3 (prerequisite not met: `formula` and `lint` were unbuilt in this worktree). They were built, and it then exited 0. - `check-governed-merges --branch claude/issue-21908-adr-0096-d5-note`: GOVERNED, landing tier H, `docs/adr/**` ×1 (`docs/adr/0096-execution-surface-identity-admission.md`), 2 changed lines. --- _Generated by [Claude Code](https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ)_ Co-authored-by: Claude <noreply@anthropic.com>
…al-less readings outside packages/spec say D5 refuses it (objectstack-ai#22357) Fixes objectstack-ai#22345 Clause-②: no (narrowing) `domain:services` seat 1, branch `claude/issue-22345-pre-d5-reading-pass`, dispatched under the claim `6068302217`, executing triage `6068146867`. The pre-D5 family's `packages/spec` half is objectstack-ai#22302 / PR objectstack-ai#22327 and is not touched here. ## What this does - **Retires `RunProvenanceContext`** from `@objectstack/service-automation`: the interface, its arm of `RunDataContext`, the type export in `src/index.ts` and the README export list. `RunDataContext` is now `interface RunDataContext extends RunIdentityContext {}`, which is exactly the set of shapes `resolveRunDataContext` returns. Only type declarations change. - **Closes the pre-D5 reading outside `packages/spec`.** 24 comment or docstring sites in 11 packages said, in the present tense, that a principal-less or `{ flowRunId }`-only context falls open, is handed through, or is skipped by the data security middleware. Each now says that the middleware used to do this, and that ADR-0096 D5 refuses such a context. These edits are comment text only. - **No runtime behaviour changes.** Comments were stripped with `scripts/js-comment-mask.mjs` `stripComments` and whitespace was collapsed. After that, 18 of the 20 changed `.ts` files are byte-identical to base `35afb15878`. The other two are `runtime-identity.ts` and `index.ts`. They differ only by the removed `interface RunProvenanceContext`, the `RunDataContext` declaration, and the dropped name in the type export. The class body between those two declarations is byte-identical. ## H1: producers and readers (base `35afb15878`) - `git grep -n RunProvenanceContext` finds 4 hits: `runtime-identity.ts:75` (the declaration), `:124` (the union), `index.ts:195` (the type export) and `README.md:451` (the export list). There is no other reference in `packages/**`, `apps/**`, `examples/**` or `packages/qa/**`. The pinned sibling `objectui` at `a58626c88d` has none either: `git grep -E 'RunProvenanceContext|RunDataContext'` exits 1, and in the same shallow fetch `service-automation` matches 14 files as the control. - `RunDataContext` by name: `runtime-identity.ts:124` (the declaration), `:178` (the return type of `resolveRunDataContext`), `:275` (a parameter of `stampSystemInsertOwner`), `index.ts:195` and `README.md:450`. It has one reader outside the package: `plugin-approvals/test-typecheck-debt.json:43`. That is a ledgered TS2352 on `record-lock-schedule-run.integration.test.ts:150`, which casts `resolveRunDataContext(...)` to a record because the union's provenance arm had no `isSystem`. - **No production code builds an engine context that carries `flowRunId` and no principal.** Every data node resolves its context through `resolveRunDataContext` (`crud-nodes.ts:484/572/720/812`, `plugin.ts:1248`). `engine.ts:6079` stamps `flowRunId` on the run's `AutomationContext`, which is not an engine context. Only tests build such a context: - `objectql/src/engine.test.ts:601` (a bare engine; it pins hook provenance); - `plugin-security/src/delegated-admin-gate.test.ts:137` and `system-write-guard.test.ts:94` (gate units); - `principal-less-strict-mode.test.ts:135` (refused by D5). The `provenance: { flowRunId }` fixtures with no session in the plugin-approvals, plugin-audit and service-storage tests are `HookContext` shapes. None of these names the type. - **The docstring said the type "survives for the non-data provenance uses that motivated objectstack-ai#3712". None was found.** The objectstack-ai#3712 use is the approvals record lock. It reads `HookContext.provenance.flowRunId` (`plugin-approvals/src/lifecycle-hooks.ts:489`), which objectql's `buildProvenance` builds from any `ExecutionContext`, and `RunIdentityContext` already carries `flowRunId`. - Result: the retirement condition holds, and the type is retired. ## H2: what D5 does today (on `main`) - `plugin-security/src/security-plugin.ts:2451`: `if (opCtx.context?.isSystem) return next()`. This system short-circuit runs first. - `:2654`-`:2656`: `if (isPrincipalLessContext(opCtx.context)) throw principalLessDenial(...)` throws `PermissionDeniedError`, `403 PERMISSION_DENIED`, for every verb. It runs after the package-managed, system-row, curated-capability, audience-anchor, engine-owned-write and delegated-admin gates. The predicate is at `:383`-`:387`. The probes agree with it: `getReadFilter` returns the deny filter (`:5947`), and `canReadObject` (`:6384`) and `canExport` (`:6778`) answer `false`. Landed in `a3bcbcf3ca` (PR objectstack-ai#22297), and `git merge-base --is-ancestor a3bcbcf origin/main` exits 0. - `resolveRunDataContext` (`service-automation/src/runtime-identity.ts`) returns, by `runAs`: - `runAs: 'system'`: `{ isSystem: true, actor: 'svc:flow:NAME', userId?, tenantId?, positions: [], permissions: [], flowRunId? }`; - `runAs: 'user'` with a user: `{ isSystem: false, userId, positions, permissions, tenantId?, flowRunId? }`; - no user: it throws `UnscopedRunDataAccessError` (`AUTOMATION_UNSCOPED_RUN_DATA_ACCESS`). ## H3: the enumeration (the pin) **Why there is no test file.** The repo's closest precedents pin a relation or a structure, never wording. `rest-server-docblock-position.test.ts` says so: "Wording is not pinned here on purpose: nothing parses these sentences". Nothing parses these comments either. So the pin is this command and its output, re-runnable on any tree: ``` git grep -n -i -E "middleware('s)? (skips (when|every|its)|skipped (a|when|every|its)|would skip|waves|waved|takes its principal-less|SKIPS)|(skips|skipped) when there is no (principal|identity)|wave[sd]? (it |them )?straight through|principal-less (fall-open|hand-off|.return next)|empty-principal (fall-open|skip)|(falls|fell|fall|falling) open for principal-less|plugin-security('s)? (principal-less )?(falls|fell) open|indistinguishable from passing no context|security[- ]skipped|ADR-0096 E1|straight to .next\(\)|(be|been) handed straight through|middleware handed it" -- . ':!packages/spec/**' ':!**/CHANGELOG.md' ``` It gives 82 lines at base `35afb15878` and 76 at head `f3a9675f4b`. The grep is line-based, so the sweep also paired subject and claim terms across 8-line windows to catch sentences split over lines. That window sweep found the fixed sites below that the grep alone misses. ### Fixed in this PR: current tense and false now (base positions) | # | Site | The false sentence | |:-|:-|:-| | 1 | `service-automation/src/runtime-identity.ts:56-74` | `RunProvenanceContext`: "the empty-principal fall-open … indistinguishable from passing no context at all" (retired with the type) | | 2 | `service-automation/src/runtime-identity.ts:85-86` | `UnscopedRunDataAccessError`: "the data security middleware skips when there is no principal" | | 3 | `service-automation/src/runtime-identity.ts:220-223` | "presenting none means the data security middleware skips every principal gate" | | 4 | `service-automation/src/runtime-identity.ts:333-336` | `runIsUnscopedUserMode`: "a principal-less context that the security middleware would wave straight through" | | 5 | `service-automation/src/engine.ts:6057-6058` | `resolveRunContext`: "the data security middleware skips when there is no identity" | | 6 | `service-automation/src/builtin/crud-runas.test.ts:219-220` | "which the data security middleware waves straight through" | | 7 | `service-automation/src/builtin/crud-runas.test.ts:258-259` | "the data security middleware skips" | | 8 | `service-analytics/src/strategies/objectql-strategy.ts:410-411` | "reaches the engine principal-less and plugin-security falls open" | | 9 | `plugin-security/src/security-plugin.ts:6061-6065` | `getMetadataReadableFields`: "mirrors the engine middleware, which skips its grant-based gates for a caller with no permission sets" | | 10 | `plugin-security/src/get-metadata-readable-fields.test.ts:8-10, :84` | "the engine middleware skips its whole gate for such a caller"; "middleware-mirroring fall-open" | | 11 | `platform-objects/src/system/sys-secret.object.ts:50-51` | "read with no principal (middleware falls open for principal-less internal calls)" | | 12 | `metadata-protocol/src/protocol.ts:11233-11237` | "this read passes no context"; "the middleware takes its principal-less `return next()`" | | 13 | `metadata-protocol/src/protocol.platform-store-system-opt-in.test.ts:7-9` | "plugin-security hands … straight to `next()`" | | 14 | `plugin-auth/src/auth-plugin.ts:2480-2482` | "— the principal-less hand-off (ADR-0096)" | | 15 | `plugin-auth/src/auth-manager.ts:3384-3386` | "(the security middleware's principal-less hand-off, ADR-0096)" | | 16 | `plugin-auth/src/scim-connection-service.ts:121-122` | same | | 17 | `plugin-auth/src/principal-less-producers-system-context.test.ts:13-15` | "A context with neither … is the security middleware's principal-less hand-off" | | 18 | `runtime/src/http-dispatcher.ts:1244-1246` | "these calls carry NO ExecutionContext, so the data engine's security middleware skips RLS / FLS / CRUD / tenant scoping entirely" (the facade has carried `{ ...caller, isSystem: true }` since objectstack-ai#3914) | | 19 | `runtime/src/http-dispatcher.ts:1510-1511` | "(the security middleware's principal-less hand-off, ADR-0096)" | | 20 | `runtime/src/dispatcher-plugin.endpoint-fallback.integration.test.ts:551-556` | "which the security middleware hands straight through"; "once a principal-less context is denied too" | | 21 | `trigger-record-change/src/record-change-integration.test.ts:395-396` | "the data security middleware skips when there is no principal" | | 22 | `qa/dogfood/test/flow-runas-schedule.dogfood.test.ts:10-12` | "the security middleware SKIPS (it delegates auth to the auth layer)" | | 23 | `qa/dogfood/test/declarative-endpoint-anonymous-guest.dogfood.test.ts:15-17` | "once the engine-level deny for the principal-less hand-off lands" | | 24 | `examples/app-showcase/src/automation/flows/index.ts:1584-1586` | "Without this it relies on the 'no identity → security-skipped' fall-through" | ### The pin's 76 lines at head `f3a9675f4b`, each with its class - **Fixed here (15):** the lines of rows 1-24 that still match, now in their corrected form: `examples/…/flows/index.ts:1586`, `protocol.platform-store-system-opt-in.test.ts:8, :9`, `protocol.ts:11237`, `auth-plugin.ts:2481`, `principal-less-producers-system-context.test.ts:14`, `scim-connection-service.ts:122`, `get-metadata-readable-fields.test.ts:9`, `security-plugin.ts:6062`, `declarative-endpoint-anonymous-guest.dogfood.test.ts:17`, `dispatcher-plugin.endpoint-fallback.integration.test.ts:552`, `http-dispatcher.ts:1514`, `crud-runas.test.ts:220`, `engine.ts:6058`, `runtime-identity.ts:206`. - **Historical, left (36):** - Release text, not edited in a code PR: `.changeset/21908-by-id-producers-opt-in.md:6`, `.changeset/21908-principal-less-producers-final.md:8`, `.changeset/21908-principal-less-strict-mode.md:20` and `content/docs/releases/v17/17-0.mdx:302`. - Decision records (Tier H): `docs/adr/0096-…:11, :38, :156` and `docs/adr/0138-…:111, :635`. - "Before …" / "used to …" / "which D5 closes": `auth-manager.org-slug-guard-system-context.test.ts:10`, `http-dispatcher.membership-system-context.test.ts:10`, `principal-less-strict-mode.test.ts:9`, `security-plugin.ts:359, :2650, :6212`, `zero-set-capability-fold.test.ts:40`, `zero-set-masking.test.ts:37`, `webhook-system-context.pin.test.ts:11`, `datasource-system-context.pin.test.ts:16`, `inbox-system-context.ts:16`, `sql-http-outbox.ts:473`, `system-context.pin.test.ts:18` (service-messaging), `settings-system-context.pin.test.ts:11`, `metadata-store.ts:143, :174, :196, :197, :198` and `owner-of-private-object-under-strict-mode.dogfood.test.ts:8`. - objectstack-ai#3597 and objectstack-ai#1888 history: `analytics-rls.dogfood.test.ts:9, :163`, `flow-runas-fixture.ts:27`, `flow-runas.dogfood.test.ts:30`, `execution-context-bridge.test.ts:16`, `service-analytics/src/plugin.ts:427` and `objectql-strategy.ts:852`. - **True, left (15):** - Names the hand-off as what ADR-0096 D5 closes: `auto-enqueuer.ts:30`, `redeliver-guard.ts:70`, `datasource-admin-plugin.ts:104`, `datasource-secret-binder.ts:40`, `fan-out-system-context.ts:25`, `outbox-dispatcher-scope.ts:94, :118, :135`, `settings-service-plugin.ts:414, :538` and `settings-service.ts:104`. - Negation: `public-form-grant-masking.test.ts:33, :233` ("never the principal-less hand-off"). - Describes the replacement: `tenant-audit-update-delete-half-repairs.test.ts:798`. - This PR's changeset quoting the removed sentence: `.changeset/22345-run-provenance-context-retired.md:17`. - **A string literal, not a comment, left (2):** both are test assertion messages that name the failure shape: `dispatcher-plugin.endpoint-fallback.integration.test.ts:571` and `runas-grant-resolution.integration.test.ts:102`. - **The sibling ADR-0056 D2 family, left (3):** `export-permission-axis.test.ts:143` (a test title), `rest/src/rest-server.ts:2640` and `runtime/src/domains/automation.ts:295`. These describe an AUTHENTICATED caller with zero permission sets, not a principal-less one. See the Acceptance notes. - **Another subject (5):** `docs/adr/0111-…:126` (the sharing middleware skipping `sys_record_share`), `better-auth-schema-parity.test.ts:13`, `can-write-object-admission.test.ts:576` and `security-plugin.ts:6521` (step 2.5 with no payload), and `text-match-sql.ts:237`. ## H4: `objectql/src/engine.ts` (`domain:engine`), not changed The sentence is now at `:5466`-`:5469`, after PR objectstack-ai#22337 landed: "A context carrying only write PROVENANCE (`{ flowRunId }`, all an identity-less flow run has — objectstack-ai#3712) is such a case: it says what produced the write, not who is calling, and surfaces through {@link buildProvenance} instead." **Reading:** the sentence does not assert the pre-D5 behaviour. It says nothing about the security middleware admitting or skipping that context. It describes `buildSession` returning no session for it, and that is still what the code does. So it is not changed, and this diff contains no objectql file. Its parenthetical producer claim, "all an identity-less flow run has", is a different staleness. It has been false since objectstack-ai#3760: a user-less `runAs: 'user'` run never reaches the engine, and a `runAs: 'system'` one carries `isSystem` and `actor`. It is listed in the Acceptance notes with its family. ## H5: retirement, dependents and reverse verification - **Why an interface rather than a type alias.** This was probed with tsc 6.0.3. The alias `type RunDataContext = RunIdentityContext` prints as `RunIdentityContext | undefined` in diagnostics. That re-spells plugin-approvals' ledgered TS2352 signature (`'RunDataContext | undefined'`) and turns its `check:test-typecheck` red (one ARRIVED, one VANISHED). The interface keeps the name, so no consumer ledger moves. - **Dependents typecheck.** `turbo run typecheck --filter="...^@objectstack/service-automation"` covers all 18 dependents. The six other published packages this diff touches were added with explicit `--filter`s. That is 24 packages, and all 24 declare a `typecheck` script. Result: `Tasks: 89 successful, 89 total`, 36 cached, at `c19dde3b5c`, before the merge of `main`. - **Reverse verification.** A probe file went into `plugin-approvals/src`. That package resolves `@objectstack/service-automation` through `exports` to `dist/index.d.ts`, rebuilt from this branch. tsc reported: - `TS2305: Module '"@objectstack/service-automation"' has no exported member 'RunProvenanceContext'`; - `TS2739: Type '{ flowRunId: string; }' is missing the following properties from type 'RunDataContext': isSystem, positions, permissions`. The probe was removed by a trap, and `git status --porcelain` printed nothing afterwards. ## Changeset `.changeset/22345-run-provenance-context-retired.md` grades `@objectstack/service-automation` as `minor`: BREAKING, an accept-set narrowing of one type and one removed type export, with the `!` banner. It also grades four packages as `patch` for comment text only. Their edited comment text ships, as measured in the built `dist`: | Package | Edited text found in | |:-|:-| | `plugin-security` | `dist/index.js` and `dist/index.d.ts` | | `runtime` | `dist/index.js` and `dist/index.d.ts` | | `service-analytics` | `dist/index.js` | | `platform-objects` | `dist/index.js` | The edited comments of `plugin-auth` and `metadata-protocol` do not ship. As a control, the code tokens next to them are present in the bundle (`withSystemContext(rawEngine)` 2, `CREDENTIAL_PROBE_CONTEXT` 3, `sys_metadata_audit` 8). ADR-0087 disposition: `not-required (runtime-interface-only packages/services/service-automation/src/runtime-identity.ts#RunDataContext)`. The gate verified it: "verified: …#RunDataContext (interface)". ## Cross-lane paths (comment-only, declared) - `plugin-security`, `plugin-auth`, `runtime`, `metadata-protocol`, `platform-objects`, `service-analytics` and `trigger-record-change`: rows 8-21 of the table. - `packages/qa/dogfood` and `examples/app-showcase`, both private: rows 22-24. - `objectql`: read under H4 and not changed. ## Verification (head `f3a9675f4b` = this branch merged with `main` at `54c3ce10ce`; PR objectstack-ai#22337 landed meanwhile; no conflict) - `pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2`: `Test Files 177 passed (177)`, `Tests 2171 passed (2171)`. Before the merge, at `c19dde3b5c`, it was 176 / 2163. - `pnpm --filter @objectstack/service-automation typecheck`: `check:test-typecheck: OK … 0 file(s) / 0 error(s)`. - The edited test files, one run each, all passed: | Test file | Tests passed | |:-|-:| | plugin-security `get-metadata-readable-fields.test.ts` | 7 | | metadata-protocol `protocol.platform-store-system-opt-in.test.ts` | 7 | | plugin-auth `principal-less-producers-system-context.test.ts` | 4 | | runtime `dispatcher-plugin.endpoint-fallback.integration.test.ts` | 21 | | trigger-record-change `record-change-integration.test.ts` | 9 | - After the merge, `turbo run build --filter=!@objectstack/docs` gave `72 successful, 72 total`. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 79 commands from 22 paths at `f3a9675f4b`, and all 79 exited 0. `--ran` with recorded exit codes: "79 derived famil(ies) accounted for — 79 run, 0 NOT-MEASURED (a DERIVED zero …)". Some of the gates' own verdict lines: - `check-adr-0087-registration`: "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition"; - `check-system-context-census`: "OK — 118 elevation read sites in 20 packages"; - `check:nul-bytes`: "OK (scanned 10334 text file(s) … no raw ASCII control bytes)"; - `check:dual-build-cjs-loads`: "106 published require entry point(s) across 66 package(s) load"; - `check:published-files`, `check:dts-closure`, `check:test-source-alias`, `check:cross-package-test-inputs` and `check-issue-citations`: green. - ESLint on the 20 changed `.ts` files: `errors=0 warnings=0`, with 20 files counted from `--format json`. That run was narrowed to the changed files, which holds only because no file's lint result depends on another file: the population is the config's `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` block, and `eslint.config.mjs:327-328` states that the config "never enables type-aware linting (no `parserOptions.project` …)". The full lint is CI's. - Declared narrowings: - The dependents typecheck ran before the merge. The commits the merge brought in move no `service-automation` export, and CI's TypeScript Type Check runs them all. - The full test suites of the comment-only packages, and the Dogfood Regression Gate, are left to CI. Each of their changed files is comment-identical to base, as measured above. ## Acceptance notes - **Sibling family, outside this card's definition (ADR-0056 D2, the deny baseline).** These sites say that the middleware skips its CRUD gate for an AUTHENTICATED caller with zero permission sets. The step-2 CRUD gate has not been guarded on a resolved set since that change. The sites are `rest/src/rest-server.ts:2639-2642`, `runtime/src/domains/automation.ts:295-303` ("this surface refuses where `/data` falls open"), `plugin-security/src/export-permission-axis.test.ts:143-146` (a title and a comment) and `plugin-security/src/baseline-composition.test.ts:157-159`. Not a principal-less reading, so left as they are. Taker: none. - **The objectstack-ai#3712 producer premise, false since objectstack-ai#3760, with no admission claim.** These sites say that a schedule-triggered run reaches the data layer as `{ flowRunId }` with no session: `objectql/src/engine.ts:5467` and `:5524`, `plugin-security/src/delegated-admin-gate.test.ts:133-135`, `system-write-guard.test.ts:89-91`, `plugin-audit/src/comment-access-hooks.test.ts:186`, `service-storage/src/attachment-access-hooks.test.ts:147-151`, `plugin-approvals/src/approval-service.test.ts:2239-2242` and `lifecycle-hooks.ts:484-488`. The units they introduce still test real hook and gate behaviour for that shape. Taker: none. - **Runtime strings, outside "comments only".** The `UnscopedRunDataAccessError` message (`runtime-identity.ts:109-113`) and the run-setup warning (`engine.ts:6133-6135`) say a principal-less run "would execute UNSCOPED (elevated, RLS-bypassing)". On a kernel with plugin-security, that run is now a 403. Both are pinned: `crud-runas.test.ts:238` and `schedule-runas-e2e.test.ts:122` assert `/UNSCOPED/`. Not changed, because no runtime behaviour moves in this PR. The prescription they give (declare `runAs: 'system'`) is still right. - **Test titles are strings, so they are left.** `plugin-security/src/security-plugin.test.ts:2317, :2571, :2674` read "(gate is before the fall-open)", and `can-write-object-admission.test.ts:640` reads "before the fall-open". The gates still run before the D5 refusal, so the DENIES assertions hold. `trigger-schedule/src/schedule-runas-e2e.test.ts:95-96` reads "runs the flow UNSCOPED". - **Left on purpose:** - `service-automation/src/engine.ts:6292`, "Surfaces the user-less fail-open (see helper)", names the objectstack-ai#1888 case. It is not a claim about the middleware. - `objectql-strategy.ts:852` and `analytics-rls.dogfood.test.ts:8` say "the bridge passes no ExecutionContext". That is a tense slip inside a past-tense paragraph; the bridge has passed the caller's context since objectstack-ai#3602. - **An orphaned docblock.** `runtime/src/http-dispatcher.ts:1241-1254` is bound to no declaration, the shape `rest-server-docblock-position.test.ts` records for this file. Its text is corrected here; its position is not moved. - **A plugin-security design question, not answered here.** `getReadableFields` still answers the full field set (minus posture fields) for a caller with no permission sets, a caller the middleware now refuses. The docstring now says that, instead of calling it "mirroring". No consumer was measured reaching it with such a context. - **Not changed:** - `resolveRunDataContext` keeps its `| undefined` return type although no path returns `undefined`. - `skills/objectstack-query/SKILL.md:66` ("`{ flowRunId }` for provenance alone") is Tier H and stays with the card the PR objectstack-ai#22327 review says is owed. - **A possible overlap.** PR objectstack-ai#22315 (`domain:spec` seat 2) edits `service-automation/src/engine.ts` and `README.md` in other regions. --- _Generated by [Claude Code](https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…ince ADR-0096 D5 (objectstack-ai#22382) Fixes objectstack-ai#22372 Clause-②: no The published `objectstack-query` skill taught `{ flowRunId }` "for provenance alone" as a valid execution context. Since ADR-0096 D5 strict mode (PR objectstack-ai#22297) the security plugin refuses a non-system context that carries no principal with `403 PERMISSION_DENIED`, so an AI author following that line wrote a context the engine refuses. This PR rewrites that one paragraph in place so it names the two shapes the engine admits and the refusal otherwise, and records the `skills/**` enumeration the card asked for. ## The paragraph Before (`skills/objectstack-query/SKILL.md:65-69` on `main` at `16096e8d7`): ``` Pass any SUBSET of the execution envelope (identity, tenant, transaction): `{ isSystem: true }` for a system read, `{ flowRunId }` for provenance alone. On the READ methods it may sit in the query bag (above) OR in the trailing options argument, `engine.find(obj, query, { context })`; the trailing one wins when both are given. Writes take only the trailing argument. ``` After (`:65-70`): ``` Pass any SUBSET of the execution envelope (identity, tenant, transaction): `{ isSystem: true }` for a system read, otherwise the caller's own context (user, position or permission set), or `403 PERMISSION_DENIED` (ADR-0096 D5). On the READ methods it may sit in the query bag (above) OR in the trailing options argument, `engine.find(obj, query, { context })`; the trailing one wins when both are given. Writes take only the trailing argument. ``` Lines 68-70 are the original 67-69 re-wrapped, text unchanged. ## The contract the sentence follows — `main` at `16096e8d7`, verbatim at each site - `packages/plugins/plugin-security/src/security-plugin.ts:383-387`, `isPrincipalLessContext`: `positions.length === 0 && explicitPermissionSets.length === 0 && !context?.userId`. That is the "(user, position or permission set)" gloss. - `:398-404`, `principalLessDenial`, the refusal text: "was called with a context that carries no principal (no user, no position, no permission set) and is not a system context. Pass the caller's execution context, or, for platform plumbing whose own door already authorized the caller, the explicit system opt-in (isSystem: true)." — `PermissionDeniedError`, `403 PERMISSION_DENIED`. - `:349-367`, the predicate's docblock: "A non-system context of this class is REFUSED, at every layer that used to hand it through: the engine middleware throws principalLessDenial before it resolves anything, the object-admission probes (`canReadObject` / `canWriteObject` / `canExport`) answer `false`, and its row scope is the deny sentinel (`getReadFilter`)." and "The two ways to reach the engine are explicit, never a missing field: carry the caller's principal, or … the explicit system opt-in (`isSystem: true`)." - `docs/adr/0096-execution-surface-identity-admission.md`, the D5 note dated 2026-10-08: "An engine context that carries no principal (no user, no position, no permission set) and is not a system context is refused with `PermissionDeniedError` (`403 PERMISSION_DENIED`) wherever the security plugin used to hand it through". **Wording and PR objectstack-ai#22327.** PR objectstack-ai#22327 (card objectstack-ai#22302) is still an open draft as of this PR (read via REST: `state: open`, `draft: true`; it edits `packages/spec/src/contracts/security-service.ts`, `packages/spec/src/kernel/execution-context.zod.ts`, `packages/spec/src/data/data-engine.zod.ts`, `content/docs/kernel/contracts/data-engine.mdx` and `content/docs/permissions/access-recipes.mdx`). So the sentence here follows the D5 contract as it stands on `main` — the plugin's refusal text and predicate above — not that PR's draft text; the two agree on substance (principal or `isSystem: true`, else `403 PERMISSION_DENIED`, ADR-0096 D5). On `main` the old sentence still stands at `execution-context.zod.ts:335-336` and `:501`, `data-engine.zod.ts:67-70` and `data-engine.mdx:127-128`; those are objectstack-ai#22327's files and are not touched here. ## Enumeration pin — `git grep -n -i` over `skills/**` on `main` at `16096e8d7` **Class 1, a `{ flowRunId }`-only context.** `flowRunId`: 1 hit, `objectstack-query/SKILL.md:66`, fixed here. `provenance`: 1 hit, the same line. `runId` / `run id`: 2 hits, the same line plus `objectstack-automation/references/state-machines-and-approvals.md:208`, a `:runId` URL path parameter, not a context. **Class 2, a principal-less context that is admitted, keeps its scope or falls open.** Zero hits for each of: `no principal`, `without a principal`, `principal-less`, `principalless`, `anonymous context`, `context: {}` (fixed-string, and the regex `context:\s*\{\s*\}`), `empty context`, `fall open`, `falls open`, `fall-open`, `fail open`, `fail-open`, `hand(ed|s)? (it )?through`, `keeps its scope`, `skips? (the )?(permission |security )?checks`, `no identity`, `without identity`, `resolves no identity`, `without (a )?context`, `contextless`, `no context`, `RLS-on`, `sees-nothing`, `SYSTEM_CTX`, `passes only`. Non-zero query words, each hit read and dispositioned: - `unauthenticated` (4): `objectstack-api/SKILL.md:162` — `authRequired: false` opens an anonymous HTTP entry point (ADR-0121 D6 pairing); that is the door's authentication, not an engine context. The public-form endpoints at `:87-88` run under a synthetic `{ permissions: ['guest_portal'], anonymous: true }` context, which carries a named permission set and so is not principal-less under the predicate. `objectstack-data/references/data-hooks.md:361`, `:602`, `:629` — `ctx.user` is `undefined` for system / unauthenticated writes: the ctx shape, no admission claim. - `no user` (2): `objectstack-automation/SKILL.md:192-194` — a `'user'` hook whose trigger resolved no user has its `ctx.api` refused (`HOOK_UNSCOPED_DATA_ACCESS`, 403) "rather than run unscoped": fail-closed, consistent with D5. `data-hooks.md:930` — "system operations carry no user", a system context. - `context-less` (1): `objectstack-ui/rules/actions.md:127` — `ctx.user` is `undefined` for a context-less / self-invoked call (the `ScopedRepo.execute()` path, `packages/runtime/src/sandbox/body-runner.ts:1188-1198` says that path carries no caller identity). It describes `ctx.user`; it does not say the engine admits such a context. Left alone. - `carries no` (5), `sees nothing` (1), `anonymous` (11), `bypass` (7), `elevat` (12), `runAs` (20), `system context` (3), `resolve[sd] no` (3), `no resolvable` (1), `unscoped` (4): every hit is either an explicit elevation the engine admits (`isSystem`, `runAs: 'system'`: `objectstack-automation/SKILL.md:183`, `references/examples-flows.md:23`, `:88` teach `runAs: 'system'` for a run with no trigger user, which is the D5-correct prescription) or a different subject (anonymous records, sharing `bypass`, public forms, a search axis, a time dimension). Control: `isSystem` hits 3 files (`data-hooks.md`, `objectstack-query/SKILL.md`, `objectstack-query/evals/filters-pagination-search.json`), matching the seat's reading. No hit lands in `skills/objectstack-formula/SKILL.md` (PR objectstack-ai#22347) or `skills/objectstack-ui/references/react-blocks.md` (PR objectstack-ai#22322); neither file is touched. ## Evals `skills/objectstack-query/evals/filters-pagination-search.json`: the 2 `isSystem` hits are both in case `id: 5`, whose `expected_output` prescribes `context: { isSystem: true }` and whose `must_contain` is `["context", "isSystem: true", "limit: 1"]`. `flowRunId` / `provenance` / `envelope`: 0 hits across `evals/`. The old line is not asserted; the eval is unchanged and stays true. ## Budget — net-line budget 0, cap +1: spent +1 | reading | before (`16096e8d7`) | after (`f4e5170b`) | |---|---|---| | `skills/objectstack-query/SKILL.md`, lines | 400 | 401 (+1) | | whole package, all `skills/*/SKILL.md`, lines | 4409 | 4410 (+1) | | `SKILL.md` tokens, `ceil(bytes/4)` (ceiling 5552) | 4109 | 4128 (headroom 1424) | Why not 0: the replaced clause (`{ flowRunId }` for provenance alone) was 37 characters; the replacement that states the admitted shape, the refusal code and the ADR is 112. A 0-net fit required deleting content — the envelope's "(identity, tenant, transaction)" or the principal gloss — and re-wrap is not a currency, so the one line the cap allows was spent instead. `scripts/pm/check-skill-line-ratchet.mjs` does not cover the published root (its header says so); the token ratchet is the binding one and it is green with headroom. ## Changeset `skills/**` is in no released package's `files[]`: no `package.json` under `packages/` names a `skills` path and none carries an entry that escapes its own directory (both measured over every `packages/**/package.json`); the catalog reaches customers through `npx skills add objectstack-ai/objectstack/skills` from this repository, which `packages/create-objectstack` invokes at scaffold time rather than bundling (`src/created-summary.ts:31`). Positive control: the same old sentence in `packages/spec/src/kernel/execution-context.zod.ts:501` IS inside spec's `files[]` (`src/**/*.zod.ts`), which is why objectstack-ai#22327 carries a changeset and this PR does not. No `.changeset/*.md`; `skip-changeset` is the repo's skip form. ## Gates — merge base `16096e8d7`, final commit `f4e5170b` `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 24 families from the worktree change set (1 path); each ran with its exit code captured before any pipe; `--ran` reconciliation: "24 derived, 24 run, 0 NOT-MEASURED, 0 UNRUN", with every line carrying its exit code ("a DERIVED zero — all 24 recorded an exit code and none of them is 3"). All 24 exit 0: - `node scripts/check-skills-token-ratchet.mjs` (+ `--self-test`): "skills/objectstack-query/SKILL.md is 4128 tokens (ceiling 5552; headroom 1424)". - `pnpm --filter @objectstack/lint run check:doc-formula-expressions`, after building its prerequisite under the verify lock (`pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint`, `VERDICT command-exit 0`, held 105s, waited 0s). - `pnpm --filter @objectstack/spec run check:skill-docs` (reads frontmatter only; unchanged), `pnpm check:doc-authoring`, `pnpm check:skill-identifier-liveness`, `pnpm check:skill-frame-sync`, `pnpm check:skill-compatibility`, `pnpm check:corpus-claim-drift`, `pnpm check:cross-package-test-inputs`, `pnpm check:agent-test-spelling`, `pnpm check:role-word`, `pnpm check:gitlink-declared`, `pnpm check:driver-memory-census`, `pnpm check:refd-timer-probe`, `pnpm check:watch-hint-literal`, `pnpm check:pm-governed-merges`, `pnpm check:nul-bytes`, `node scripts/check-ci-filter-parity.mjs`, `node scripts/check-closing-keyword-parity.mjs` (+ `--self-test`), `node scripts/check-comment-mask-corpus.mjs`, `node scripts/check-doc-route-spelling.mjs --advisory` (+ `--self-test`). Beyond the derivation: `pnpm check:pm-skill-ratchet` exit 0 (the published root is outside its map, as its header states); `pnpm --filter @objectstack/spec run check:skill-refs` exit 0 ("9 generated files in sync", nothing to regenerate); a control-byte scan over the edited file finds none. The 52 artifact-roster families, the 11 declared wide-population families and the type-check lanes the derivation lists outside the derived total are CI's runs on this PR; `pnpm lint` (repo-level eslint) was not run locally — the diff is one Markdown file. ## Acceptance notes - Governed surface, Tier H (`skills/**`): this PR stays draft; landing waits for an authorized approval, and the dispatch names the contract-review tier as mandatory on this path. - Commit identity: this cloud container cannot mint the fleet identity (`OS_FLEET_APP_ID` / `OS_FLEET_PRIVATE_KEY` are unset and the relay hands out no token for `git`), so the one commit carries the worktree's harness identity; every later commit on this branch keeps that same identity. - Observed, not filed (code comments are objectstack-ai#22345's lane, PR objectstack-ai#22357): `packages/runtime/src/sandbox/body-runner.ts:979-984` still says "A caller that has no context to give gets the same identity-less behavior as before", a pre-D5 reading in a code comment. - Historical `CHANGELOG.md` entries ("A run with no principal now passes provenance alone.") are release-owned records of what shipped and are not edited. ## 维护者速读(草稿) - **改了什么:** 已发布的 `objectstack-query` 技能里,"Execution Context" 一节的一段话。原来教"只传 `{ flowRunId }` 做溯源"也是合法的执行上下文;现在改为:系统读传 `{ isSystem: true }`,否则传调用者自己的上下文(带用户、岗位或权限集),两者都没有则引擎拒绝(`403 PERMISSION_DENIED`,ADR-0096 D5)。只改这一段,净增 1 行(预算 0、上限 +1)。 - **为什么改:** ADR-0096 D5 严格模式(PR objectstack-ai#22297)落地后,`plugin-security` 在全部站点拒收"无 principal 且非 system"的上下文。按旧句写出来的上下文会被引擎直接拒绝,而这份技能是通过 `npx skills add` 装进客户项目的,AI 作者先读到它、再撞上 403。同时按卡片要求对全部 `skills/**` 做了枚举:只有这一行教旧读法,其余命中都是 `isSystem`/`runAs:'system'` 这类显式提权或别的主题;评测文件没有断言旧句。 - **风险与代价(含回滚):** 纯文本改动,不碰代码、不发包、无 changeset(`skills/**` 不在任何已发布包的 `files[]` 内)。措辞按 `main` 上的 D5 契约原文写;PR objectstack-ai#22327 仍是 draft,它落地后两边说法一致。回滚即还原这一个文件的一次提交。 - **席位意见:** - **你要做的:** 看一眼第 65-70 行这一段表述是否认可,认可就给一个批准。 --- _Generated by [Claude Code](https://claude.ai/code/session_01JmWtcHfGbC4ncw4GFKWuRA)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21908
Clause-②: no (narrowing)
Summary
ADR-0096 D5 strict mode, the deny round of #21908. A data-engine context that carries no principal (no user id, no position, no permission set) and is not a system context is now refused at every place
plugin-securityused to hand it through. The published contract sentence onChatWithToolsOptions.toolExecutionContext("unauthenticated (RLS-on, sees-nothing)") now holds.Before the deny, every producer of such a context that a run or a static read found was closed inside this claim: it carries the caller's principal or the explicit system opt-in (
isSystem: true) inside its owning code. Breaking for in-process code (the!banner); the changeset carries the migration and an ADR-0087 disposition. The at-tier contract review is owed before the queue.The ADR-0096 D5 dated note is its own Tier H draft PR, on
claude/issue-21908-adr-0096-d5-note, for the maintainer's approval.What changes, by class
The predicate is
isPrincipalLessContext(context) && !isSystem, the one the hand-off already used. These are the six sites, atorigin/main0e9371f0positions:security-plugin.ts):2616)return next(): no CRUD gate, no RLS, no field mask, no tenant wallPermissionDeniedError(403 PERMISSION_DENIED) for every verb, before the operation runsgetReadFilter(near:5875)resolveProjectionFieldMask(near:6126)canReadObject(near:6299)truefalsecanWriteObject(near:6585)truefalsecanExport(near:6688)truefalseWhy the five service sites return rather than throw. The ruling's error class is the engine-level refusal, and the middleware throws it. The published
ISecurityServicecontract hascanReadObject,canWriteObjectandcanExportreturnfalse, and hasgetReadFilterreturn the deny filter. Each of them now gives a principal-less context the deny it already gives a caller that carries a principal and resolves no set (ADR-0056 D2). The field projections are field answers, not admission answers. The contract text forgetReadableFieldsnames the answer for "a non-system caller who resolves no permission set", and that answer now applies to this context too. Object admission is refused by the middleware and the three probes.Producer census (H1), by class, position and function
Method.
sys_*engine call in production source that passes no context.metadata-protocolprotocol.tspromoteDraftForPublish(the draft read)metadata-protocolprotocol.tsresolveDraftOrgScopeForPublish(both probes)metadata-protocolprotocol.tsresolveMetaItemOrgScope(both history probes)metadata-protocolprotocol.tsauditMetaItemmetadata-protocolprotocol.tsdiffMetaItem(the history read and the current-row read)metadata-protocolprotocol.tslistCommits,revertCommit,rollbackToPackageCommitmetadata-protocolprotocol.tsmetaTypeNamespaceExists,migrateStoredMetadatametadata-protocolprotocol.tsdeleteMetaItemlegacy path (read and delete)qa/dogfood{ context }qa/dogfoodpackage-first-authoring(a direct store insert)runtimestandalone-stack-seeder-declaration-copy(context-less reads)runtimeshare-links-enforcement-contextengine double (dropped the trailing read options)mergeReadContextdoesNot producers today.
loadMetaFromDbandreportUnhydratableOrgScopedRowsread context-less at boot. They run inside ObjectQL'sstart(), and plugin-security depends on ObjectQL, so they run before the security middleware is registered. Noted, not edited.No producer was found outside
plugin-security,protocol.tsand the test harnesses. The static sweep's other 25 hits all go through a system-context wrapper or a context-carrying options object, or are comments. Every edit uses only the declared opt-in; nothing guesses a boundary. Each moved call is asys_metadata*store read, or the legacy delete of asys_metadatarow. None of the six gates the middleware runs before the refusal keys on those objects, so the opt-in changes nothing they decide.The dispatcher's identity-resolution fault class (the
domain:cliseat's note) is closed by this deny by construction. It is pinned below, andhttp-dispatcher.tsis not edited.Pins
The ruling's three pins:
plugin-securityprincipal-less-strict-mode.test.ts. It covers every spelling: no context, an empty one, empty arrays,isSystem: false, a tenant id alone, and provenance only. The middleware answersPERMISSION_DENIED/ 403 for all seven verbs before the operation runs, the probes answerfalse, and the row scope is the deny filter.Further pins:
zero-set-deny-baseline,zero-set-masking,zero-set-capability-fold,metadata-unresolvable-posture,predicate-related-read-tenant-scopeandsecurity-plugin.test.ts's row-scope case.resolveRequestScopefault class is now denied:runtimedispatcher-plugin.endpoint-principal-less-deny.integration.test.ts, over the real dispatcher plugin and the real security middleware. The engine is handed no principal, the store is never reached, and the caller gets 403 with nothing of the rows. Its two controls: an anonymous request with no fault runs as the guest (the posture of security(rest, runtime): an anonymous request at an app-declaredauthRequired: falseendpoint executes principal-less — execute it as the guest principal (ruled C), never principal-less, never system #22147, unchanged), and a granted signed-in caller is served.declarative-endpoint-anonymous-guestpins stay green under the deny.qa/dogfoodowner-of-private-object-under-strict-mode. Anowner_of:audience on aprivateobject reaches its owner's inbox, and nobody else's, end to end. In the same composition, a principal-less read of that object is refused.metadata-protocolprotocol.platform-store-system-opt-in.test.ts, deny-round block. Each producer is driven, and every engine call it issues must carryisSystem: true.Ablations
Each ablation ran through
scripts/ablation-replace.mjs(wrap mode, plus a shell trap). Every mutation landed (anchor 1 → 0, blob changed), and every restore was proven (blob == HEAD,git diff HEADempty). The dist legs were proven withablation-dist-preflight(marker present while live,--absentafter the restore rebuild, tree clean).return next()(dist leg)getReadFilteranswers no scope for the classcanReadObjectadmits the classresolveOwnerOfloses its opt-in (service-messaging, dist leg)Evidence
Final head
454d0e89b9:origin/main8cbe255ef6merged at6681b216e1, then a docblock-only test commit. All runs were made under the shared verify lock, so wall times are shared-box readings.On the merged tree (
6681b216e1):maintouched (rest, cli, lint, platform-objects, plugin-approvals, service-analytics, organizations, spec): all green.tsc --listFilesshows metadata-protocol's program compiles the edited pin.Before the merge (
8248ceb97f): objectql 381 files / 7525 passed, and core 81 files / 2240 passed. Neither package is touched bymain's incoming commits or by this diff.Real boot, showcase
--fresh, on the merged tree:Gates:
dispatch-gates --commandsderived 73 families at454d0e89b9. All 73 ran and exited 0.--ran: "73 derived famil(ies) accounted for — 73 run, 0 NOT-MEASURED (a DERIVED zero — all 73 recorded an exit code and none of them is 3)". Selected verdict lines:✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.✓ This diff introduces no \major` bump.`check-nul-bytes: OKcheck-engine-double-contract: OKcheck-test-source-alias OKcheck:cross-package-test-inputs OKLint, a proven narrowing (CI runs the full
pnpm lint):eslint.config.mjsis**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}plus thepackages/**blocks.eslint --no-inline-config --format jsonover the 16 changed.tsfiles at454d0e89b9gives 16 files, 0 errors, 0 warnings.parserOptions.project, no typed rules), so no untouched file's verdict can move.check-governed-merges --branch: 0 of 17 paths on the register; 1083 changed lines.Acceptance notes
ISecurityServiceprose still describes the hand-off. Four TSDoc sentences ongetReadFilter,canExportandcanReadObjectinpackages/spec/src/contracts/security-service.tssay a context with no principal "is admitted" or "keeps the scope".packages/specis outside this claim, so the sentences are not edited here. The report asks the seat how to route them.domain:enginedeclaration on [PM seat] domain:engine — ⏳ vacant #6367 namespromoteDraftForPublish. The census found the otherprotocol.tsproducers listed above in the same file: the same class, with the same mechanical opt-in. They are closed here and named for the seat, so the declaration can be amended.ExecutionContextvariable as the options argument of an engine verb compiles, and silently drops the context. One fixture did this. Under strict mode it is now a loud 403, no longer a silent hand-through.sys_*object names spelled as literals; a call whose object name is a variable is covered only by the runs.Generated by Claude Code