You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(service-analytics): the ROW-SCOPE bridge to the `security` service tells the same three resolutions apart as the object-level one — a broken security service refuses the query instead of running it with no row policy (#16918)
6
+
7
+
`AnalyticsServicePlugin` bridges to the `security` service twice: once for the OBJECT-level read grant (`admitObjectRead` → `canReadObject`, #16645) and once for the ROW-level read scope (`getReadScope` → `getReadFilter`, ADR-0021 D-C). The object-level bridge tells three resolutions apart — ABSENT admits, THROWING and METHOD-LESS deny at `error`. The row-scope bridge collapsed all three into one:
A throwing resolver and a registered service without `getReadFilter` both produced `undefined` — the same value an absent security service produces, and the value `ISecurityService.getReadFilter` reserves for one meaning only: *"this caller has no row restriction on this object"*. So on a deployment whose security service was wired but broken (a boot-order fault, a mis-registered plugin, a failing dependency, a provider that is not the contract it claims to be) analytics queries ran with **no row-level policy at all**, and nothing said so. One door of the file failed closed on a throwing resolver and its neighbour failed open — and the neighbour is the one carrying row-level policy.
20
+
21
+
**What changes.** The bridge now resolves the same explicit three-way, at the same reporting level:
22
+
23
+
-**ABSENT** — no `security` service resolved: **unchanged**. No row-scope provider on this deployment, which is a legitimate configuration (a single-tenant kernel that ships no `plugin-security`, where `/data` carries no row-level policy either) and is already reported loudly at init. ⛔ Deliberately not tightened: refusing here would break every such deployment.
24
+
-**THROWING** resolver, or a registered service with **no `getReadFilter`** — the query is **REFUSED**, and the reason is reported at `error` naming the object and which of the two states it was. The refusal is a throw, which `AnalyticsService.resolveReadScopes` — fail-closed since ADR-0021 D-C — already turns into "deny the whole query rather than emit SQL with that object unscoped". A log over an `undefined` would not have been a refusal.
25
+
26
+
**This change only NARROWS what analytics serves, and only in a state where the security service is broken.** No deployment with a working `security` service, and no deployment with none, changes behaviour by so much as a byte. Nothing that was refused becomes admitted.
27
+
28
+
**No published-surface delta.** No new error code (the refusal rides the seam's existing fail-closed error), no exported symbol, no key on `AnalyticsServicePluginOptions` or any payload, and no documented envelope changes shape. Graded `minor` rather than `patch` because it is a behaviour narrowing on a published package's read path, matching how its object-level sibling was graded in the same lockstep window.
29
+
30
+
⚠️ Deliberately **not** answered here: which tenant wall the platform's is (plugin-security's posture-gated Layer 0, or driver-sql's posture-independent auto-scope) — the escalated maintainer decision of triage condition 5. Refusing to serve is neutral between them: it answers *"should we serve at all"*, never *"what shape is the wall"*.
The error-code ledger's TSDoc stops naming a retired verdict as a live mechanism, and states the published-face rule it is actually held to.
6
+
7
+
`packages/spec` ships `src/**/*.zod.ts`, so `api/error-code-ledger.zod.ts`'s header is published prose — a consumer reads these sentences out of the tarball. Two of them stopped being true when `check-dispatcher-error-vocabulary`'s face refusal widened from `packages/spec/src/**` to every published package's `src/` and the dispatcher vocabulary's `boot-refusal` verdict retired with it (#16649).
8
+
9
+
The first said the `boot-refusal` verdict **records** reachability for codes not yet registered, and pointed at the module the verdict was being deleted from. That is a claim about where a live mechanism lives, not about a case that can no longer arise, so a reader following the pointer would have found nothing. It now records the retirement and names what replaced it: a `door: 'none'` code has no resting place short of a row in the ledger.
10
+
11
+
The second opened `packages/spec/src/** is held to this mechanically`. True before the widening and an understatement after it — a reader would conclude only the spec tree is guarded, which is the "guarded a part" / "guarded it" confusion this whole class of gate exists to remove. It now states the published face, the stricter spec sub-face where `pending-registration` has no allowance, and the named, dated allowance outside it owed to #8846, with both finding kinds named.
12
+
13
+
No schema, accept set, default or refusal moves. `ERROR_CODE_LEDGER` holds the same members before and after, and the generated reference page is regenerated from this prose rather than hand-edited.
0 commit comments