Skip to content

Commit 76fec88

Browse files
fix(services): settings, datasource, webhook and messaging plumbing passes the explicit system opt-in instead of no principal (#21940)
Fixes #21913 Clause-②: yes (widening) This is a slice of #21908: the services-lane producers. #21908 stays open, because it builds the deny itself, last. ## What changes Every engine call in the card's named functions now passes the explicit system opt-in that exists today: `{ isSystem: true }` on the call's context. These calls used to reach the data engine with no context at all, so they had no principal and no opt-in. They got past the security middleware only through its principal-less hand-off (ADR-0096 E1), which #21908 retires. This PR adds no new elevation API, changes nothing any door authorizes, and does not build the deny. | Row | Package | Function | Engine calls that now carry the opt-in | | :-- | :-- | :-- | :-- | | 7 | service-settings | `SettingsService.loadRows` | `find` on `sys_setting` | | 8 | service-settings | `SettingsService.upsertRow` | existence-probe `find` and `insert` on `sys_setting` (its `update` already had the opt-in) | | 8 | service-settings | `buildSettingAuditWriter` `write` | `insert` on `sys_setting_audit` | | 11 | service-datasource | `loadDatasourceRows`, `loadDatasourceRow` | `find` / `findOne` on `sys_metadata` | | 11 | service-datasource | `persistDatasourceRow`, `deleteDatasourceRow` | `findOne` + `insert` / `update` / `delete` on `sys_metadata` | | 11 | service-datasource | secret binder `bind` / `unbind` / `resolve` | `insert` / `delete` / `find` on `sys_secret` | | 12 | plugin-webhooks | `AutoEnqueuer.doRefresh` | `find` on `sys_webhook` | | 12 | plugin-webhooks | `createWebhookRedeliverGuard` | `findOne` on `sys_webhook` | | 13 | service-messaging | `SqlNotificationOutbox.claim` / `claimDigest` / `reapExpired` | candidate `find`, claiming `update`, read-back `find`; the reap `update` | | 13 | service-messaging | `SqlHttpOutbox.claim` / `reapExpired` | candidate `find`, claiming `update`, read-back `find`; the reap `update` | | 14 | service-messaging | `MessagingService.writeEvent` | `insert` on `sys_notification` | | 14 | service-messaging | inbox channel `send` + `writeDeliveredReceipt` | `insert` on `sys_inbox_message`, the recipient-locale `findOne` on `sys_user` (a helper only `send` calls), `insert` on `sys_notification_receipt` | | 14 | service-messaging | `PreferenceResolver.loadRows` | both `find`s on `sys_notification_preference` | | 14 | service-messaging | `RecipientResolver.resolveEmail` | `findOne` on `sys_user` | IDataEngine reads pass the opt-in in the trailing options argument, which is where the contract puts a read's context. Two package-local surfaces have a single options bag, and the opt-in goes there: `SettingsEngine`, and the `sys_secret` binder's engine slice. `SettingsEngine.find` and `.insert` and `SecretStoreEngineLike.delete` now declare the `context` they receive. No symbol is new on any package entry. The shared constants (`FAN_OUT_SYSTEM_CONTEXT`, `DISPATCHER_SYSTEM_CONTEXT`) live in package-internal modules. Rows 15 and 16 are not in this slice and wait for the maintainer. ## Measurement **Instrument (H2).** A local, uncommitted instrument sat at the security middleware. It recorded each principal-less, non-system context that reached the hand-off, with its stack. It recorded whether any of the six gates before the hand-off threw on such a call, and which of them matched the call's object and verb. It also recorded the outcome after `next()`: the result type, row count, key set, a hash of the non-volatile values, or the error code. In the AFTER leg it recorded the same outcome for each `isSystem` call whose stack ran through these four packages. Both legs covered the whole dogfood suite (206 files, 1590 tests passed, 9 skipped, identical in both legs) and a booted showcase dev composition. The boot covered seed-admin, a settings read plus two writes, a runtime datasource create / patch / read / delete, and admin and anonymous requests, then sat idle for 65 seconds so the dispatchers and the webhook refresh ticked. The instrument was then reverted, and the file's blob equals HEAD (`5b4ab28045af`). The plugin-security dist was rebuilt clean: `ablation-dist-preflight --absent` passes, and the marker had 3 hits in the instrumented dist. **Before and after, per function.** Columns: principal-less records BEFORE, principal-less records AFTER, and `isSystem` records AFTER. | Function | dogfood before / after / after-system | boot before / after / after-system | | :-- | --: | --: | | `SettingsService.loadRows` | 2090 / 0 / 2090 | 42 / 0 / 42 | | `SettingsService.upsertRow` (probe + insert) | 7 / 0 / 7 | 3 / 0 / 3 | | setting-audit `write` | 4 / 0 / 4 | 2 / 0 / 2 | | `loadDatasourceRows` | — | 1 / 0 / 1 | | `persistDatasourceRow` | — | 4 / 0 / 4 | | `deleteDatasourceRow` | — | 2 / 0 / 2 | | `AutoEnqueuer.doRefresh` | — | 3 / 0 / 3 | | `SqlNotificationOutbox.claim` | 8 / 0 / 8 | 56 / 0 / 56 | | `SqlNotificationOutbox.claimDigest` | 8 / 0 / 8 | 56 / 0 / 56 | | `SqlNotificationOutbox.reapExpired` | 1 / 0 / 1 | 7 / 0 / 7 | | `SqlHttpOutbox.claim` | 8 / 0 / 8 | 56 / 0 / 56 | | `SqlHttpOutbox.reapExpired` | 1 / 0 / 1 | 7 / 0 / 7 | | `MessagingService.writeEvent` | 8 / 0 / 8 | — | | inbox `send` (row insert) | 8 / 0 / 8 | — | | `writeDeliveredReceipt` | 8 / 0 / 8 | — | | `PreferenceResolver.loadRows` | 16 / 0 / 16 | — | | `RecipientResolver.resolveEmail` | 1 / 0 / 1 | — | Principal-less totals moved 35245 → 33077 in dogfood (Δ 2168) and 422 → 183 at boot (Δ 239). Each delta is exactly the sum of the rows above. No principal-less record attributed to any moved function remains. The hand-off still sees row 15 and every other lane's producers. No run reached these, so each is held by its unit pin instead: `loadDatasourceRow`, the secret binder (this repo wires it into no composition), the redeliver guard, the inbox recipient-locale read (template path), and the claim path's `update` and read-back (no pending rows in any run). **Gates before the hand-off (Zone 1).** Across 35245 dogfood and 422 boot principal-less records, the "gate threw" record fired 0 times. The package-managed, system-row, curated-capability and audience-anchor gates never matched an object or verb these producers touch. Neither did the delegated-administration gate. The engine-owned guard matched the bucket on the writes to engine-owned objects. On a context with no user id, its own `isUserContextWrite` predicate returns before it can refuse. **No producer is held back.** **What each call answers is unchanged.** Per function, call counts per object and verb are equal before and after. So are the outcome shapes (result type, row count, key set). There were 0 errors in either leg. Content hashes are equal for 11 of 14 functions in dogfood and 7 of 9 at boot. The rest differ only on values that change every run: the receipt's `at` timestamp (all 8), and inbox and notification payloads that carry a per-run record id or date (2 of 8 and 3 of 8, from the approval and sweep tests). At boot, the probe's own per-phase file path sits in the stored datasource record. The plugin-audit rows these writes produce (`sys_audit_log`, `sys_activity`) are written in equal numbers before and after. **H6, `loadRows`.** The call count is the same (2090 + 42), and the returned settings have equal hashes on every call. The opt-in adds one frozen context object. The middleware now exits at its system short-circuit instead of running the six gates and the hand-off. No wall-clock figure is quoted, because the container is shared. **H7, reads on another principal's behalf.** What these reads return (a user id for an address, a locale, preference rows) is consumed inside the fan-out. `emit()` answers the notification id, counts and per-delivery outcomes. Its three in-repo callers (approvals, the flow notify node and comment mentions) relay counts and the id only. The opt-in changes none of this, because the principal-less read returned the same rows. **One engine branch keyed on the flag stops running on these writes.** It is row 23 of the `isSystem` census page: the dangling-reference check is skipped for an `isSystem` write. Before the move, it ran 10 times nested under these producers (`writeEvent` 2, inbox `send` 2, setting-audit `write` 6), on the `actor_id` lookups, and resolved every time. After the move it does not run. A local probe (real ObjectQL and SQLite, deleted after the run) showed what that means for an `actor_id` that names no user. With no context, today's path refuses with `VALIDATION_FAILED` ("Actor: no sys_user record has id …"). Under `isSystem` the row is written. A real user is written both ways. That `actor_id` comes from `emit()`'s `actorId`, which a flow notify node can author. So the behaviour on measured traffic is unchanged, and a latent difference remains for an `actorId` that names no user. The Acceptance notes carry it. **H4 pins and ablations.** There is one pin per package. The engine double sits behind the package's real call path, proves the population ran, and asserts `isSystem` on every call. Each pin was ablated by dropping the opt-in through `scripts/ablation-replace.mjs` (the anchor must hit). Seven legs ran: settings `loadRows`, the fan-out constant, the dispatcher constant, the datasource `sys_metadata` constant, the secret-binder constant, and the two webhook constants. Every leg went red under the mutation, and the failure names the call, for example "find on sys_setting: expected undefined to deeply equal { isSystem: true }". Every leg was restored with blob equal to HEAD and an empty `git diff HEAD`, and went green again. The pins are package-local, imported from `src` with no dist in the path. **Census pages (H3).** The `isSystem` census (`check-system-context-census`) is OK, and `--fix` changed nothing: this change adds no elevation read site. The tenant-audit census did move, because the write sites now thread a context. It was regenerated with `tenant-audit-census.mjs --write`. On its page, the hand-written figures follow the census: the provable no-context, tenancy-enabled count went 9 → 2, unreadable 67 → 60, decidably elevated 114 → 121. **Serial (H5).** `origin/main` was merged twice. It now includes #21906's squash, and the merge was clean. A `git merge-tree` against #21877's head (`5c405846`, now closed as a draft) is clean. This PR edits neither PR's region: `datasource-admin-plugin.ts` and `datasource-secret-binder.ts` only, in `service-datasource`. ## Tests - At `10e77fef6f`, after merging `origin/main` `faf8dce482`. The next merge (`9dce635337`, which brings this PR to `62960ffa1a`) touches no file in these four packages. Typecheck of the four packages: exit 0. - Unit suites: service-settings 614 passed, service-messaging 510, service-datasource 743, plugin-webhooks 165. All exit 0, unchanged apart from the new pins and tests that came in from `main`. - ESLint, narrowed to the 19 changed TS files with `--no-inline-config --format json`: 19 files, 0 errors, 0 warnings. Those files are inside the config's own `packages/**/*.{ts,…}` population, and the config enables no type-aware linting, so this diff cannot move a verdict on any untouched file. The full `pnpm lint` run belongs to CI. - At `62960ffa1a`, the head this PR opens with, every one of the 105 commands `dispatch-gates --commands --repo objectstack-ai/objectstack` derives exited 0. `dispatch-gates --ran` reports: "105 derived famil(ies) accounted for — 105 run, 0 NOT-MEASURED". In an earlier pass, four of these went red on this branch, and they are now fixed. `check:tenant-audit-census` needed the census regenerated. `check:engine-double-contract` and `check:objectql-double-limit` needed the pin doubles routed through the shared dispatch asserts and holding a find's bound, with the ledger recording the new pinned coverage. `check:dual-build-cjs-loads` needed eight unrelated packages built first. ## Acceptance notes - **Producers in these packages that the card does not name.** A static read finds that they still reach the engine with no context. No run exercised them, so the measured table never listed them. Without a route, #21908's deny breaks each one, so they are listed for the seat's closure rather than moved here: - service-settings: the `sys_secret` store the plugin builds (`insert` / `get` / `update`), and `SettingsService.readStoredHandle`. - service-messaging: `SqlNotificationOutbox` and `SqlHttpOutbox` `enqueue`, `ack` and `list`; the email and SMS channels' recipient reads; `RecipientResolver.resolveRole` / `resolveTeam` / `resolveOwnerOf`; the emit dedup lookup; the template renderer's read. - `resolveOwnerOf` reads a business object, and its posture is not neutral. Today the sharing middleware answers a principal-less read of a `private` object with a deny-all filter, so an `owner_of:` recipient on such an object resolves to nobody. Under the opt-in, that filter would be bypassed. - **Request-door producers that act on the caller's own rows, like rows 15 and 16** (report-only, for the maintainer's ruling): the inbox unread count, and mark-read / mark-all-read (`unreadNotificationIds`, `upsertReadReceipt`, `notificationOrganization`). - **The row-23 difference above:** the dangling-reference check stops running on the `actor_id` of `sys_notification`, `sys_inbox_message` and `sys_setting_audit`. - One posture question was noted on a request-door read and is held off-thread. It was not measured. ## Seat's append: patch round 1 at `57f738dfb1` (written by `domain:services` seat 1 from the dev's report `6009307655`; the dev does not edit this body) **What changed in the patch round** (seat verdict `6008259054`). The sections above describe `62960ffa1a`; where they differ, this append is current. - **`Clause-②: yes (widening)`.** The exported `SettingsEngine` (`find`, `insert`) and `SecretStoreEngineLike` (`delete`) gain an optional `context`, so `@objectstack/service-settings` and `@objectstack/service-datasource` take a `minor`. `service-messaging` and `plugin-webhooks` stay `patch`. Line 2 above, the changeset and the claim (`6003840075`) moved together. Nothing accepted or refused at any door changes. - **A user reference that names no user is still refused.** The engine skips its dangling-reference check for an `isSystem` write and has no option to keep it. So each producer that writes a user reference does one guarded `sys_user` read by id under the opt-in, then refuses an unknown id with the engine's own answer: `VALIDATION_FAILED`, one `reference_not_found` finding, and the same message. - The checked references are the `actor_id` of `sys_notification` (`writeEvent`), of `sys_inbox_message` (the inbox send) and of `sys_setting_audit` (the setting-audit writer), and the `user_id` of a user-scope `sys_setting` row on `SettingsService.upsertRow`'s insert. The last is the same difference, which this PR's opt-in introduced on that insert. - The refusal is built by `validationFailure` from `@objectstack/types`, already a runtime dependency of both packages, so neither package stamps the code itself and `check:error-code-provenance` is green with no spec row and no waiver. It is shape-identical to the engine's refusal but not `instanceof` objectql's `ValidationError`; the callers on these paths read the message or the code, and every door maps the shape to `400 VALIDATION_FAILED`. - A write that names no user is unchanged. A read that cannot run lets the write through, as the engine's check does. The cost is one extra `sys_user` read per write that names a user. - Differential pins over a real engine hold each producer's answer equal to the engine's own refusal of a context-less insert. Four ablations went red and were restored with blob equal to HEAD. - **#21935 merged in** (`a3c2209a68`). `check:pm-dispatch-gates` exits 0. - **Gates at `57f738dfb1`:** 105 derived, 105 run, all exit 0. The 54 roster families: 51 exit 0, and 3 are NOT WIRED locally (they need a pull-request context; CI runs them). - **`service-settings/vitest.config.ts`** gains one anchored alias (`platform-objects/identity` → `src`) for the new pin, which `check:test-source-alias` asks for. **Carried, not filed here:** - Producers in these packages that the card does not name are recorded on #21908's census (rows 24 onward). `resolveOwnerOf` is not neutral to move. - The inbox unread count and mark-read / mark-all-read join the maintainer's open ruling on rows 15 and 16. - One request-door posture question is held off-thread, at class level only. --- _Generated by [Claude Code](https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a3bd157 commit 76fec88

30 files changed

Lines changed: 1308 additions & 94 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/service-settings": minor
3+
"@objectstack/service-messaging": patch
4+
"@objectstack/service-datasource": minor
5+
"@objectstack/plugin-webhooks": patch
6+
---
7+
8+
Platform plumbing in these four packages now passes the explicit system opt-in (`{ isSystem: true }`) on its data-engine calls. Until now it reached the engine with no principal and no opt-in, and the security middleware let that through only because of its principal-less hand-off.
9+
10+
Clause-②: yes (widening)
11+
12+
- **Why `yes (widening)`:** two exported option types gain an optional `context` that an adapter must forward as-is. They are `SettingsEngine.find` / `.insert` (`@objectstack/service-settings`) and `SecretStoreEngineLike.delete` (`@objectstack/service-datasource`), so both packages take a `minor`. An implementation written against the old types still type-checks, and nothing accepted or refused at any door changes.
13+
- **service-settings:** `SettingsService` reads and writes its own `sys_setting` rows under the opt-in: `loadRows`, plus the existence probe and insert in `upsertRow` (the update already used it). The `sys_setting_audit` writer does too.
14+
- **service-datasource:** the `sys_metadata` helpers behind runtime datasources use the opt-in. They cover boot restore, cluster convergence, and persist and delete behind the admin doors. So do the `sys_secret` binder's `bind`, `unbind` and `resolve`.
15+
- **plugin-webhooks:** the auto-enqueuer's subscription refresh and the redeliver guard's subscription lookup use the opt-in.
16+
- **service-messaging:** two paths use the opt-in. One is the dispatcher's claim path: `claim`, `claimDigest` and the visibility-timeout reap on both outboxes. The other is the emit fan-out: the `sys_notification` row, the recipient's address and locale reads, the preference reads, the inbox row and the delivered receipt.
17+
- **A user reference that names no user is still refused.** The engine skips its dangling-reference check for an `isSystem` write, so each producer that writes a user reference checks it first. The checked references are the `actor_id` of `sys_notification`, `sys_inbox_message` and `sys_setting_audit`, and the `user_id` of a user-scope `sys_setting` row. An unknown id is refused with the engine's own answer: `VALIDATION_FAILED`, one `reference_not_found` finding, and the same message. A write that names no user is unchanged.
18+
- What each call reads and writes is otherwise unchanged. None of the gates the middleware runs before its hand-off applies to these objects.
19+
- ⛔ No new export on any package entry, and no new elevation API.

‎content/docs/permissions/tenant-audit-census.mdx‎

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ are reported as `undecidable` rather than assumed either way.
122122

123123
The same holds twice over for the context. An options argument spelled as a
124124
literal can be read; one spelled `options`, `{ ...opts }`, or handed through a
125-
forwarding shim cannot, and **67 of the 233 sites are spelled that way**. A
125+
forwarding shim cannot, and **60 of the 233 sites are spelled that way**. A
126126
context resolved from an inline literal or a local `const` can be tested for
127127
`isSystem`; one arriving from a helper call cannot.
128128

@@ -150,8 +150,8 @@ now **0**: nothing on this surface threads a context that provably lacks the fla
150150

151151
**"No tenant context" counted sites it had not read.** An options argument the
152152
walker could not parse was folded into the same bucket as one it had read and
153-
found empty. That published **84 sites "carrying no tenant context at all"**
154-
when 17 said so and 67 were simply unread — an over-claim in the *alarming*
153+
found empty. That published **69 sites "carrying no tenant context at all"**
154+
when 9 said so and 60 were simply unread — an over-claim in the *alarming*
155155
direction, on the very figure this page tells other cards to cite. `carries` is
156156
now three-valued, and an unreadable argument can never contribute to the
157157
provable count.
@@ -188,9 +188,9 @@ reproduce them. Where it disagrees, it disagrees on the page:
188188
| carried figure | where it survives | this census |
189189
| :--- | :--- | ---: |
190190
| 175 write call sites | quoted in the merged changeset | **233** |
191-
| 24 carrying no tenant context | quoted in the merged changeset | **9** provable and tenancy-enabled; **34** more whose options argument is unreadable |
191+
| 24 carrying no tenant context | quoted in the merged changeset | **2** provable and tenancy-enabled; **33** more whose options argument is unreadable |
192192
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **155 of 233** decidable, **78** undecidable |
193-
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 114 decidably elevated, 0 decidably not, 102 undecidable |
193+
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 121 decidably elevated, 0 decidably not, 103 undecidable |
194194
| 141 and 132, two independent re-derivations | the card that filed this work | — |
195195

196196
**The differences are not reconciled, and deliberately so.** The old census's
@@ -207,14 +207,14 @@ would report a smaller number and would not say so.
207207

208208
The fourth row is the one worth flagging to anyone citing it. **The 135 / 77%
209209
figure has no surviving corroboration anywhere in the tree.** This census reads
210-
114 of 233 (49%) as decidably elevated, with 102 more whose elevation is a
210+
121 of 233 (52%) as decidably elevated, with 103 more whose elevation is a
211211
run-time fact — so the claim is neither confirmed nor refuted, and the honest
212212
answer is that a static reading cannot settle it.
213213

214-
⇒ **Cite `9 / 233`, and say what it is**: the sites whose options argument was
214+
⇒ **Cite `2 / 233`, and say what it is**: the sites whose options argument was
215215
READ and holds no tenant context, against a decidably tenancy-enabled object.
216216
That is the control's provable yield surface. ⛔ Do not cite it as "the sites
217-
without tenant context" — **34 further sites** have an options argument this
217+
without tenant context" — **33 further sites** have an options argument this
218218
cannot read, and they are neither in nor out.
219219

220220
{/* BEGIN GENERATED: tenant-audit-census (scripts/tenant-audit-census.mjs) — DO NOT EDIT */}
@@ -228,14 +228,14 @@ cannot read, and they are neither in nor out.
228228
| …whose object name is chosen at run time | 78 |
229229
| …against an object with tenancy ENABLED | 154 |
230230
| …against an object that declares tenancy off | 1 |
231-
| threading a tenant context | 149 |
232-
| PROVABLY carrying none (options read, no context key) | **17** |
233-
| …of those, against a decidably tenancy-enabled object | **9** |
234-
| options argument UNREADABLE — may or may not carry one | 67 |
235-
| …of those, against a decidably tenancy-enabled object | 34 |
236-
| threading a decidably ELEVATED (`isSystem`) context | 114 |
231+
| threading a tenant context | 164 |
232+
| PROVABLY carrying none (options read, no context key) | **9** |
233+
| …of those, against a decidably tenancy-enabled object | **2** |
234+
| options argument UNREADABLE — may or may not carry one | 60 |
235+
| …of those, against a decidably tenancy-enabled object | 33 |
236+
| threading a decidably ELEVATED (`isSystem`) context | 121 |
237237
| threading a context that is decidably NOT elevated | 0 |
238-
| threading a context whose elevation is a run-time fact | 102 |
238+
| threading a context whose elevation is a run-time fact | 103 |
239239

240240
| how the instrument reached the site | count |
241241
| :--- | ---: |
@@ -297,13 +297,13 @@ holds still. They are required to be HERE and to say WHEN they were true;
297297
their values are not compared. The reasoning, and the measurement behind it,
298298
are in `scripts/check-tenant-audit-census.mjs`.
299299

300-
Measured on 2026-10-05 at `3d34c6efd`.
300+
Measured on 2026-10-06 at `3832674ac`.
301301

302302
| corpus scale (not enforced) | count |
303303
| :--- | ---: |
304-
| tracked non-test sources scanned | 607 |
305-
| engine-shaped types recognised | 69 |
304+
| tracked non-test sources scanned | 609 |
305+
| engine-shaped types recognised | 70 |
306306
| declared objects in the registry | 116 |
307-
| same-named calls subtracted as non-engine | 158 |
307+
| same-named calls subtracted as non-engine | 159 |
308308

309309
{/* END GENERATED: tenant-audit-census */}

‎docs/audits/2026-08-tenant-audit-write-call-sites.counts.md‎

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,14 @@ silent, and `node scripts/tenant-audit-census.mjs --write` is the resolution.
3838
| Object name chosen at run time | 78 |
3939
| Against a tenancy-enabled object | 154 |
4040
| Against an object declaring tenancy off | 1 |
41-
| Threading a tenant context | 149 |
42-
| Provably carrying none | 17 |
43-
| …and decidably tenancy-enabled | 9 |
44-
| Options argument unreadable | 67 |
45-
| …and decidably tenancy-enabled | 34 |
46-
| Threading a decidably elevated context | 114 |
41+
| Threading a tenant context | 164 |
42+
| Provably carrying none | 9 |
43+
| …and decidably tenancy-enabled | 2 |
44+
| Options argument unreadable | 60 |
45+
| …and decidably tenancy-enabled | 33 |
46+
| Threading a decidably elevated context | 121 |
4747
| Threading a decidably non-elevated context | 0 |
48-
| Threading a context of undecidable elevation | 102 |
48+
| Threading a context of undecidable elevation | 103 |
4949

5050
## Subtractions the census could NOT defend — enforced
5151

@@ -90,14 +90,14 @@ holds still. They are required to be HERE and to say WHEN they were true;
9090
their values are not compared. The reasoning, and the measurement behind it,
9191
are in `scripts/check-tenant-audit-census.mjs`.
9292

93-
Measured on 2026-10-05 at `3d34c6efd`.
93+
Measured on 2026-10-06 at `3832674ac`.
9494

9595
| corpus scale (not enforced) | count |
9696
| :--- | ---: |
97-
| tracked non-test sources scanned | 607 |
98-
| engine-shaped types recognised | 69 |
97+
| tracked non-test sources scanned | 609 |
98+
| engine-shaped types recognised | 70 |
9999
| declared objects in the registry | 116 |
100-
| same-named calls subtracted as non-engine | 158 |
100+
| same-named calls subtracted as non-engine | 159 |
101101

102102
## Every site
103103

@@ -208,24 +208,26 @@ Measured on 2026-10-05 at `3d34c6efd`.
208208
| `packages/services/service-automation/src/suspended-run-store.ts` | `delete` | `sys_automation_run` | enabled | elevated | 3 |
209209
| `packages/services/service-automation/src/suspended-run-store.ts` | `insert` | `sys_automation_run` | enabled | elevated | 2 |
210210
| `packages/services/service-automation/src/suspended-run-store.ts` | `update` | `sys_automation_run` | enabled | elevated | 2 |
211-
| `packages/services/service-datasource/src/datasource-admin-plugin.ts` | `delete` | `sys_metadata` | enabled | PROVABLY NONE | 1 |
212-
| `packages/services/service-datasource/src/datasource-admin-plugin.ts` | `insert` | `sys_metadata` | enabled | PROVABLY NONE | 1 |
213-
| `packages/services/service-datasource/src/datasource-admin-plugin.ts` | `update` | `sys_metadata` | enabled | PROVABLY NONE | 2 |
214-
| `packages/services/service-datasource/src/datasource-secret-binder.ts` | `delete` | `sys_secret` | enabled | PROVABLY NONE | 1 |
215-
| `packages/services/service-datasource/src/datasource-secret-binder.ts` | `insert` | `sys_secret` | enabled | PROVABLY NONE | 1 |
211+
| `packages/services/service-datasource/src/datasource-admin-plugin.ts` | `delete` | `sys_metadata` | enabled | elevated | 1 |
212+
| `packages/services/service-datasource/src/datasource-admin-plugin.ts` | `insert` | `sys_metadata` | enabled | elevated | 1 |
213+
| `packages/services/service-datasource/src/datasource-admin-plugin.ts` | `update` | `sys_metadata` | enabled | elevated | 2 |
214+
| `packages/services/service-datasource/src/datasource-secret-binder.ts` | `delete` | `sys_secret` | enabled | elevated | 1 |
215+
| `packages/services/service-datasource/src/datasource-secret-binder.ts` | `insert` | `sys_secret` | enabled | elevated | 1 |
216216
| `packages/services/service-job/src/db-job-adapter.ts` | `insert` | `sys_job` | enabled | elevated | 1 |
217217
| `packages/services/service-job/src/db-job-adapter.ts` | `update` | `sys_job` | enabled | elevated | 3 |
218218
| `packages/services/service-job/src/db-job-adapter.ts` | `insert` | `sys_job_run` | enabled | elevated | 1 |
219219
| `packages/services/service-job/src/db-job-adapter.ts` | `update` | `sys_job_run` | enabled | elevated | 1 |
220-
| `packages/services/service-messaging/src/inbox-channel.ts` | `insert` | `objectName` | undecidable | options unreadable | 1 |
221-
| `packages/services/service-messaging/src/inbox-channel.ts` | `insert` | `receiptObject` | undecidable | PROVABLY NONE | 1 |
220+
| `packages/services/service-messaging/src/inbox-channel.ts` | `insert` | `objectName` | undecidable | context, elevation undecidable | 1 |
221+
| `packages/services/service-messaging/src/inbox-channel.ts` | `insert` | `receiptObject` | undecidable | context, elevation undecidable | 1 |
222222
| `packages/services/service-messaging/src/messaging-service.ts` | `insert` | `RECEIPT_OBJECT` | undecidable | PROVABLY NONE | 1 |
223223
| `packages/services/service-messaging/src/messaging-service.ts` | `update` | `RECEIPT_OBJECT` | undecidable | options unreadable | 1 |
224-
| `packages/services/service-messaging/src/messaging-service.ts` | `insert` | `sys_notification` | enabled | options unreadable | 1 |
224+
| `packages/services/service-messaging/src/messaging-service.ts` | `insert` | `sys_notification` | enabled | context, elevation undecidable | 1 |
225225
| `packages/services/service-messaging/src/sql-http-outbox.ts` | `insert` | `this.objectName` | undecidable | options unreadable | 1 |
226-
| `packages/services/service-messaging/src/sql-http-outbox.ts` | `update` | `this.objectName` | undecidable | options unreadable | 5 |
226+
| `packages/services/service-messaging/src/sql-http-outbox.ts` | `update` | `this.objectName` | undecidable | context, elevation undecidable | 2 |
227+
| `packages/services/service-messaging/src/sql-http-outbox.ts` | `update` | `this.objectName` | undecidable | options unreadable | 3 |
227228
| `packages/services/service-messaging/src/sql-outbox.ts` | `insert` | `this.objectName` | undecidable | options unreadable | 1 |
228-
| `packages/services/service-messaging/src/sql-outbox.ts` | `update` | `this.objectName` | undecidable | options unreadable | 4 |
229+
| `packages/services/service-messaging/src/sql-outbox.ts` | `update` | `this.objectName` | undecidable | context, elevation undecidable | 3 |
230+
| `packages/services/service-messaging/src/sql-outbox.ts` | `update` | `this.objectName` | undecidable | options unreadable | 1 |
229231
| `packages/services/service-queue/src/db-queue-adapter.ts` | `delete` | `sys_job_queue` | enabled | context, elevation undecidable | 2 |
230232
| `packages/services/service-queue/src/db-queue-adapter.ts` | `insert` | `sys_job_queue` | enabled | context, elevation undecidable | 1 |
231233
| `packages/services/service-queue/src/db-queue-adapter.ts` | `update` | `sys_job_queue` | enabled | context, elevation undecidable | 6 |
@@ -235,7 +237,7 @@ Measured on 2026-10-05 at `3d34c6efd`.
235237
| `packages/services/service-settings/src/settings-service-plugin.ts` | `delete` | `sys_secret` | enabled | elevated | 1 |
236238
| `packages/services/service-settings/src/settings-service-plugin.ts` | `insert` | `sys_secret` | enabled | options unreadable | 1 |
237239
| `packages/services/service-settings/src/settings-service-plugin.ts` | `update` | `sys_secret` | enabled | options unreadable | 1 |
238-
| `packages/services/service-settings/src/settings-service-plugin.ts` | `insert` | `sys_setting_audit` | enabled | PROVABLY NONE | 1 |
240+
| `packages/services/service-settings/src/settings-service-plugin.ts` | `insert` | `sys_setting_audit` | enabled | elevated | 1 |
239241
| `packages/services/service-settings/src/settings-service.ts` | `insert` | `this.objectName` | undecidable | options unreadable | 1 |
240242
| `packages/services/service-settings/src/settings-service.ts` | `update` | `this.objectName` | undecidable | options unreadable | 1 |
241243
| `packages/services/service-storage/src/attachment-lifecycle.ts` | `update` | `sys_file` | enabled | elevated | 3 |

‎packages/plugins/plugin-webhooks/src/auto-enqueuer.ts‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ import {
2222
type LegacyDefinitionCredentialKey,
2323
} from './webhook-legacy-cleartext.js';
2424

25+
/**
26+
* [#21913] The execution context the subscription cache refresh
27+
* ({@link AutoEnqueuer.refresh}) reads `sys_webhook` under: the explicit system
28+
* opt-in. It is the platform reading its own delivery configuration on a boot
29+
* and timer path that has no caller, so it may not rely on a missing principal
30+
* to pass the security middleware's principal-less hand-off, which ADR-0096 D5
31+
* closes.
32+
*/
33+
const SYSTEM_CTX = { isSystem: true } as const;
34+
2535
/**
2636
* The authored trigger vocabulary, taken from the spec rather than restated
2737
* here — this file both validates authored triggers and maps events onto them,
@@ -378,9 +388,11 @@ export class AutoEnqueuer {
378388
private async doRefresh(): Promise<void> {
379389
let rows: any[];
380390
try {
381-
rows = await this.engine.find(this.subscriptionsObject, {
382-
where: { active: true },
383-
});
391+
rows = await this.engine.find(
392+
this.subscriptionsObject,
393+
{ where: { active: true } },
394+
{ context: SYSTEM_CTX },
395+
);
384396
} catch (err) {
385397
this.logger?.warn?.(
386398
`[webhook-auto-enqueuer] failed to load ${this.subscriptionsObject}`,

‎packages/plugins/plugin-webhooks/src/redeliver-guard.ts‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ import {
5959
resolveWebhookSecret,
6060
} from './webhook-secret.js';
6161

62+
/**
63+
* [#21913] The execution context the guard reads `sys_webhook` under: the
64+
* explicit system opt-in. The guard runs inside the messaging service's
65+
* redeliver path, after the delivery row has been read under the requesting
66+
* caller's organization, and reads the subscription that row belongs to only
67+
* for its existence, name and secret posture — the inputs of the refusal
68+
* reason it returns. What it reads and returns is unchanged by the opt-in; it
69+
* may simply no longer rely on a missing principal to pass the security
70+
* middleware's principal-less hand-off, which ADR-0096 D5 closes.
71+
*/
72+
const SYSTEM_CTX = { isSystem: true } as const;
73+
6274
/** The delivery-row fields this guard reads. Structural — no messaging import. */
6375
export interface RedeliverGuardRow {
6476
/** Producer domain; only `'webhook'` rows are this guard's business. */
@@ -79,9 +91,11 @@ export function createWebhookRedeliverGuard(
7991
return async (row) => {
8092
if (row.source !== 'webhook') return undefined;
8193

82-
const subscription = (await engine.findOne(subscriptionsObject, {
83-
where: { id: row.refId },
84-
})) as Record<string, unknown> | null;
94+
const subscription = (await engine.findOne(
95+
subscriptionsObject,
96+
{ where: { id: row.refId } },
97+
{ context: SYSTEM_CTX },
98+
)) as Record<string, unknown> | null;
8599

86100
if (!subscription) {
87101
return (

0 commit comments

Comments
 (0)