Skip to content

Commit 746637e

Browse files
fix(service-automation)!: retire RunProvenanceContext; pre-D5 principal-less readings outside packages/spec say D5 refuses it (#22357)
Fixes #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 #22302 / PR #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 #3712". None was found.** The #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 #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 #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`. - #3597 and #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 #22337 landed: "A context carrying only write PROVENANCE (`{ flowRunId }`, all an identity-less flow run has — #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 #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 #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 #3712 producer premise, false since #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 #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 #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 #22327 review says is owed. - **A possible overlap.** PR #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>
1 parent 43fc500 commit 746637e

22 files changed

Lines changed: 127 additions & 90 deletions
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/service-automation': minor
3+
'@objectstack/plugin-security': patch
4+
'@objectstack/runtime': patch
5+
'@objectstack/service-analytics': patch
6+
'@objectstack/platform-objects': patch
7+
---
8+
9+
fix(service-automation)!: retire the `RunProvenanceContext` type; `RunDataContext` names only what `resolveRunDataContext` returns
10+
11+
Clause-②: no (narrowing)
12+
13+
<!-- adr-0087: not-required (runtime-interface-only packages/services/service-automation/src/runtime-identity.ts#RunDataContext) Two TypeScript-only declarations in one runtime file move, and neither is metadata: `RunDataContext` loses its `RunProvenanceContext` arm, and the `RunProvenanceContext` interface declared beside it in the same file is removed together with its type export. No spec key, authorable spelling, Zod schema or stored shape moves, and no stored row is read or rewritten, so `objectstack migrate meta` has nothing to rewrite. The channel that reaches an affected consumer is the compiler, at the consumer's own import. -->
14+
15+
**BREAKING** for TypeScript code that imports from `@objectstack/service-automation`: one type export is removed and one type narrows. It ships as `minor` under the launch-window convention for breaking changes. No runtime behaviour changes in any package.
16+
17+
**What is removed.** The `RunProvenanceContext` type export, the `{ flowRunId }`-only "provenance" envelope, and its arm of `RunDataContext`. No code in this repository builds or reads one. Since ADR-0096 D5, the data engine's security middleware refuses a context that carries no principal and is not a system context (`403 PERMISSION_DENIED`, for every verb). So the type described a context the engine refuses, while its docstring said such a context was "indistinguishable from passing no context at all" and was let through.
18+
19+
**What `RunDataContext` is now.** Exactly what `resolveRunDataContext` returns: `isSystem: true` for a `runAs: 'system'` run, and the triggering user's principal for a `runAs: 'user'` run. A run that resolves no principal is refused before any data operation (`AUTOMATION_UNSCOPED_RUN_DATA_ACCESS`), as before.
20+
21+
**If your code named the type.** There is no replacement, because the context it described is refused. A custom data node builds its context with `resolveRunDataContext(context)`, typed `RunDataContext`.
22+
23+
**`@objectstack/plugin-security`, `@objectstack/runtime`, `@objectstack/service-analytics`, `@objectstack/platform-objects`:** comment and docstring text only. Shipped comments that said the security middleware hands through, skips or falls open for a context with no principal now say that it used to, and that ADR-0096 D5 refuses such a context. Nothing these packages do changes.

‎examples/app-showcase/src/automation/flows/index.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1581,9 +1581,11 @@ export const InboundTaskWebhookFlow = defineFlow({
15811581
type: 'api',
15821582
status: 'active',
15831583
// An inbound webhook has no authenticated user, so the create must run as the
1584-
// system principal (#1888 runAs is now enforced). Without this it relies on the
1585-
// "no identity → security-skipped" fall-through, which breaks the moment the
1586-
// target object carries row-level security.
1584+
// system principal (#1888 runAs is now enforced). Without this the run is
1585+
// user-less under the default `runAs:'user'`, so its create is refused
1586+
// (`AUTOMATION_UNSCOPED_RUN_DATA_ACCESS`); the "no identity → security-skipped"
1587+
// fall-through it once relied on is gone (ADR-0096 D5 refuses a principal-less
1588+
// context in the security middleware too).
15871589
runAs: 'system',
15881590
nodes: [
15891591
{

‎packages/metadata-protocol/src/protocol.platform-store-system-opt-in.test.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
* [#21911, ADR-0096] The metadata protocol's platform-store calls carry the
55
* EXPLICIT system opt-in, never a principal-less context.
66
*
7-
* plugin-security hands an engine operation whose context carries no user, no
8-
* position, no permission set and no `isSystem` straight to `next()` (the
9-
* ADR-0096 E1 hand-off). The protocol's own reads and writes of the
7+
* plugin-security used to hand an engine operation whose context carries no
8+
* user, no position, no permission set and no `isSystem` straight to `next()`
9+
* (the ADR-0096 E1 hand-off); since ADR-0096 D5 it refuses one. The protocol's
10+
* own reads and writes of the
1011
* `sys_metadata` family reached the engine exactly that way — a `{ where }`
1112
* with no `context`, or a repository transaction context with only its
1213
* handle — so they worked only because the hand-off let them through. They

‎packages/metadata-protocol/src/protocol.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11230,11 +11230,13 @@ export class ObjectStackProtocolImplementation implements
1123011230
// were eliminated by measurement, not by reading:
1123111231
// - the driver's tenant wall never engages — `buildDriverOptions`
1123211232
// sets `DriverOptions.tenantId` only from `execCtx.tenantId`
11233-
// (`objectql/engine.ts`), and this read passes no context;
11234-
// - plugin-security's Layer 0 never engages — the middleware takes
11235-
// its principal-less `return next()` thousands of lines before the
11236-
// `objectFields.has('organization_id')` gate that would have
11237-
// carried it;
11233+
// (`objectql/engine.ts`), and this read's context carries no
11234+
// `tenantId` (only the system opt-in below);
11235+
// - plugin-security's Layer 0 never engages — the middleware
11236+
// short-circuits on that `isSystem` opt-in (it used to take its
11237+
// principal-less `return next()`, refused since ADR-0096 D5)
11238+
// thousands of lines before the `objectFields.has('organization_id')`
11239+
// gate that would have carried it;
1123811240
// - no posture would save it anyway: `computeTenantLayer0Filter`
1123911241
// yields `null` under `single` and the deny sentinel under
1124011242
// `isolated` with no tenantId.

‎packages/platform-objects/src/system/sys-secret.object.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ export const SysSecret = ObjectSchema.create({
4646
// store (settings/datasource credentials). Not covered by the wildcard `'*'`
4747
// grant — ordinary members get 403 from the generic data layer. Platform
4848
// admins retain access via the posture-gated superuser bypass. Internal
49-
// readers are unaffected: `engine.resolveSecret` reads at DRIVER level,
50-
// SettingsService / the datasource secret-binder read with no principal
51-
// (middleware falls open for principal-less internal calls).
49+
// readers are unaffected: `engine.resolveSecret` reads at DRIVER level, and
50+
// SettingsService / the datasource secret-binder read with the explicit system
51+
// opt-in (`isSystem: true`), which the middleware short-circuits. A
52+
// principal-less read is refused (ADR-0096 D5).
5253
access: { default: 'private' },
5354
description:
5455
'Cipher store written by three privileged producers (see managedBy); each holds its handle in its own column. Never holds plaintext.',

‎packages/plugins/plugin-auth/src/auth-manager.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3383,8 +3383,8 @@ export class AuthManager {
33833383
// Both reads run as the platform (`withSystemContext`): this hook
33843384
// IS the slug guard — the organization id is the `where`, not the
33853385
// reader — so neither read reaches the engine with no principal
3386-
// and no opt-in (the security middleware's principal-less
3387-
// hand-off, ADR-0096).
3386+
// and no opt-in (the security middleware's former principal-less
3387+
// hand-off, which ADR-0096 D5 replaced with a refusal).
33883388
const dataEngine = withSystemContext(rawEngine) as any;
33893389

33903390
let currentSlug: string | undefined;

‎packages/plugins/plugin-auth/src/auth-plugin.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2478,8 +2478,8 @@ export class AuthPlugin implements Plugin {
24782478
// the write both carry the explicit system opt-in (`isSystem: true`).
24792479
// The platform-admin judge above is this route's authorization; the
24802480
// raw engine would have handed the security middleware a context with
2481-
// no principal and no opt-in — the principal-less hand-off (ADR-0096),
2482-
// which is not an authorization at all.
2481+
// no principal and no opt-in — the principal-less hand-off it no longer
2482+
// makes (ADR-0096 D5 refuses that context), and never an authorization.
24832483
const rawEngine = this.authManager!.getDataEngine();
24842484
if (!rawEngine) {
24852485
return c.json({ success: false, error: { code: 'SERVICE_UNAVAILABLE', message: 'Data engine unavailable' } }, 503);

‎packages/plugins/plugin-auth/src/principal-less-producers-system-context.test.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
* - the SCIM bearer verifier (`verifyScimBearerToken`): its credential probe
1111
* carries the opt-in in the read's trailing options.
1212
*
13-
* A context with neither a principal nor `isSystem` is the security
14-
* middleware's principal-less hand-off (ADR-0096), which is not an
15-
* authorization: each of these calls is already authorized by its own door
13+
* A context with neither a principal nor `isSystem` was the security
14+
* middleware's principal-less hand-off (ADR-0096 E1) and is refused since
15+
* ADR-0096 D5; it was never an authorization: each of these calls is already
16+
* authorized by its own door
1617
* (the platform-admin judge; the bearer digest). The opt-in names that.
1718
*
1819
* Measured on a REAL engine: a middleware registered on it records the context

‎packages/plugins/plugin-auth/src/scim-connection-service.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ interface CredentialEngine {
119119
* verifier runs BEFORE any caller is known — the bearer it is verifying is the
120120
* only identity on the request — so there is no principal to carry, and the
121121
* read must say so rather than reach the engine with no principal and no
122-
* opt-in (the security middleware's principal-less hand-off, ADR-0096). The
122+
* opt-in (the security middleware's former principal-less hand-off, which
123+
* ADR-0096 D5 replaced with a refusal). The
123124
* digest equality in the `where` is the whole of what the probe may match.
124125
*/
125126
const CREDENTIAL_PROBE_CONTEXT = { context: { isSystem: true } } as const;

‎packages/plugins/plugin-security/src/get-metadata-readable-fields.test.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
* `getReadableFields`.
66
*
77
* The two differ in exactly one place: what a caller resolving to ZERO
8-
* permission sets gets. On the DATA plane, falling open is the drift-free
9-
* answer — the engine middleware skips its whole gate for such a caller, and
10-
* reporting a narrowing the data path would not enforce is its own kind of
11-
* drift. On the METADATA plane the question is disclosure, and D7 rules that a
8+
* permission sets gets. On the DATA plane `getReadableFields` keeps its
9+
* fall-open answer, written when the engine middleware skipped its whole gate
10+
* for such a caller (the middleware now refuses it: ADR-0056 D2's deny
11+
* baseline at object admission, and ADR-0096 D5 outright for a principal-less
12+
* one). On the METADATA plane the question is disclosure, and D7 rules that a
1213
* guest/public deployment's schema exposure must be a deliberate
1314
* permission-set decision rather than an accidental everything-default. So the
1415
* zero-set caller goes through the same fallback resolution
@@ -81,7 +82,8 @@ describe('[ADR-0106 D7] SecurityPlugin.getMetadataReadableFields', () => {
8182
const dataPlane = await plugin.getReadableFields('deal', context);
8283
const metadataPlane = await plugin.getMetadataReadableFields('deal', context);
8384

84-
// The data plane keeps its middleware-mirroring fall-open — unchanged.
85+
// The data plane keeps its fall-open answer — unchanged (the middleware it
86+
// once mirrored refuses this principal-less caller since ADR-0096 D5).
8587
expect(dataPlane).toEqual(['id', 'name', 'secret']);
8688
// The metadata plane does not.
8789
expect(metadataPlane).toEqual(['id', 'name']);

0 commit comments

Comments
 (0)