Skip to content

Commit aab0e0f

Browse files
committed
fix: ADR-0096 unlinks the retired saved-report path; dogfood ledger figures, census rows and baseline ceiling follow the retired family
Claude-Session: https://claude.ai/code/session_013RWUA7bNq5bRhehLPqXwMg Co-authored-by: Claude <noreply@anthropic.com>
1 parent 527030b commit aab0e0f

5 files changed

Lines changed: 41 additions & 27 deletions

File tree

‎docs/adr/0096-execution-surface-identity-admission.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ All trace to the same `positions==0 && permissions==0 && !userId → skip` predi
191191
| Instance | Verdict | Issue |
192192
|:---|:---|:---|
193193
| Reports `getReport`/`deleteReport`/`listReports` discard the caller and query with `SYSTEM_CTX`; routes only `enforceAuth` → cross-user/cross-tenant IDOR (read+delete any report) | CONFIRMED exploitable | #2980 |
194-
| Scheduled reports (`packages/plugins/plugin-reports/src/report-service.ts#isSystem` `dispatchDue`) run `executeReport(..., {isSystem:true})` → a member-owned schedule emails the target object's **entire** table, RLS bypassed | CONFIRMED exploitable | #2980 |
194+
| Scheduled reports (`packages/plugins/plugin-reports/src/report-service.ts` `dispatchDue` — historical and deliberately unlinked: the saved-report stack was retired whole by #20102) run `executeReport(..., {isSystem:true})` → a member-owned schedule emails the target object's **entire** table, RLS bypassed | CONFIRMED exploitable | #2980 |
195195
| Knowledge/RAG `applyPermissionFilter` (`packages/services/service-knowledge/src/knowledge-service.ts#applyPermissionFilter`) returns **all** hits when `ctx` is missing/system; `chatWithTools`'s `ToolExecutionContext.actor` is optional with a system fallback → agent retrieval escapes the data ceiling | CONFIRMED (framework); exposure gated on cloud impl | #2981 |
196196

197197
These are *not* the `packages/plugins/plugin-security/src/security-plugin.ts#getReadFilter` fall-open (they use an unconditional `SYSTEM_CTX`), but they are exactly what a D4 conformance row (`caller-scoped?` proof) + the D2 audit would have flagged. Fixed independently of the mechanism, tracked as the mechanism's motivating evidence.

‎packages/qa/dogfood/test/authz-conformance.matrix.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
// hand-curated regex table reaching 1 of 17 REST registrars and 4 of 17
2525
// dispatcher domain files.
2626
//
27-
// The population comes from `packages/rest/src/rest-route-ledger.ts` (91 rows
28-
// / 19 families) and `packages/runtime/src/route-ledger.ts` (82 rows / 21
27+
// The population comes from `packages/rest/src/rest-route-ledger.ts` (83 rows
28+
// / 18 families) and `packages/runtime/src/route-ledger.ts` (82 rows / 21
2929
// domains) because those two are enumerated from a RUNNING server and guarded
3030
// in both directions by their own conformance tests — so a new family or
3131
// domain cannot be silently absent from them, and therefore cannot be silently
@@ -55,7 +55,9 @@
5555
// to the wrong change. It moved when #14503 took the three REST package
5656
// read/delete rows out of the ledger — 94 rows → 91, `sdk` 84 → 81, with
5757
// families unmoved at 19 because all three departing rows carried
58-
// `family: packages`, which survives on the publish row. Two stale figures in
58+
// `family: packages`, which survives on the publish row. (91 → 83 rows and
59+
// 19 → 18 families later, when #20102 retired the whole `reports` family.)
60+
// Two stale figures in
5961
// one docblock with two entirely different causes is the failure mode the pin
6062
// above ends. Widening a regex instead was refused: it rots on the next
6163
// added file, which is the mechanism this replaces. Deriving "gated" from

‎packages/qa/dogfood/test/authz-ledger-population.baseline.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ export const LEDGER_POPULATION_BASELINE: readonly string[] = [
8686
// `@objectstack/rest` mounts itself, through a different registrar.
8787
'rest-family:rest-route-ledger.ts:packages',
8888
'rest-family:rest-route-ledger.ts:record-shares',
89-
'rest-family:rest-route-ledger.ts:reports',
9089
'rest-family:rest-route-ledger.ts:search',
9190
'rest-family:rest-route-ledger.ts:security',
9291
'rest-family:rest-route-ledger.ts:security-explain',
@@ -123,6 +122,8 @@ export const LEDGER_POPULATION_BASELINE: readonly string[] = [
123122
* away; raising it is a reviewed decision, never a side effect of adding a
124123
* route family.
125124
*
126-
* 34 at 2026-08-31, the day the ledger population was adopted.
125+
* 34 at 2026-08-31, the day the ledger population was adopted. 33 at
126+
* 2026-09-25 (#20102): `rest-family:rest-route-ledger.ts:reports` left with the
127+
* retired saved-report family — deleted, not classified.
127128
*/
128-
export const LEDGER_POPULATION_BASELINE_MAX = 34;
129+
export const LEDGER_POPULATION_BASELINE_MAX = 33;

‎packages/qa/dogfood/test/authz-probe-blind-spot.census.ts‎

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
// conclusion that gets re-derived from scratch otherwise:
102102
//
103103
// WHAT THE LEDGERS DO COVER — richly, and more than this table ever has.
104-
// `packages/rest/src/rest-route-ledger.ts`: 91 audited rows over 19 families,
104+
// `packages/rest/src/rest-route-ledger.ts`: 83 audited rows over 18 families,
105105
// every route `@objectstack/rest` mounts, enumerated through
106106
// `RestServer.getRoutes()` on a booted server and guarded per route by
107107
// `rest-route-ledger.conformance.test.ts`. It reaches all 17 registrars;
@@ -324,25 +324,28 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
324324
file: 'packages/rest/src/rest-route-ledger.ts',
325325
kinds: ['ROUTE_ENUMERATION'],
326326
probes: 1,
327-
keys: 19,
328-
population: 91,
329-
reachable: 91,
327+
keys: 18,
328+
population: 83,
329+
reachable: 83,
330330
blindSpot: 0,
331331
populationRule: 'ledger rows inside REST_ROUTE_LEDGER; reachable = rows carrying a `family` (each distinct value mints a key)',
332-
controls: { "route: '": 91, "family: '": 91, RestRouteLedgerEntry: 2 },
332+
controls: { "route: '": 83, "family: '": 83, RestRouteLedgerEntry: 2 },
333333
note:
334334
'The audited disposition of every route @objectstack/rest mounts, enumerated through ' +
335335
'RestServer.getRoutes() on a booted server and guarded per route by rest-route-ledger.conformance.test.ts. ' +
336336
'That guard is why this file can be a population source and a regex table cannot: a mounted route with no ' +
337337
'row here is already RED in another package, so a new family cannot be silently absent from this file, ' +
338-
'and therefore cannot be silently absent from the authz ratchet either. 19 families; 1 classified by a ' +
339-
'matrix row (metadata), 18 enumerated in the shrink-only baseline. Re-measured 94 -> 91 when the ' +
338+
'and therefore cannot be silently absent from the authz ratchet either. 18 families; 1 classified by a ' +
339+
'matrix row (metadata), 17 enumerated in the shrink-only baseline. Re-measured 91 -> 83 (19 -> 18 families) ' +
340+
'when the whole saved-report `reports` family left with its eight routes, all eight carrying the family, so ' +
341+
'`reachable` moved with `population`. Earlier re-measured 94 -> 91 when the ' +
340342
'three REST package read/delete rows (GET /packages, GET /packages/:id, DELETE /packages/:id) left the ' +
341343
'ledger with their routes; each carried `family: packages`, so `reachable` moved with ' +
342344
'`population` (91/91) and the blind spot stays 0 -- the family itself survives on the publish row.',
343345
// The 94 -> 91 re-measurement above landed with #14503 (the REST registrar
344346
// keeps only POST /packages/publish; the dispatcher domain is the single
345-
// implementation of the reads and the delete). The id lives here, not in
347+
// implementation of the reads and the delete). The 91 -> 83 one landed with
348+
// #20102 (the saved-report stack retired whole). The ids live here, not in
346349
// the string: a runtime string reaches readers who cannot resolve it.
347350
},
348351
{
@@ -374,9 +377,9 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
374377
kinds: ['ROUTE_ENUMERATION', 'TRIPWIRE'],
375378
probes: 3,
376379
keys: 1,
377-
population: 80,
380+
population: 72,
378381
reachable: 19,
379-
blindSpot: 61,
382+
blindSpot: 53,
380383
populationRule:
381384
'route registration sites — `this.routeManager.register(` call sites, LESS the one inside ' +
382385
'`registerPerItemRoute` (the shared forwarder, not a route; its extent is bounded by the declaration\'s own ' +
@@ -457,22 +460,29 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
457460
// subtrahend itself, pinned at 1, so a low population with it at 1 is a
458461
// real drop and a low population with it off 1 is the slice eating too
459462
// much. It is deliberately the one SLICE-scoped control on this row.
463+
// [#20102] 80 / 19 / 61 -> 72 / 19 / 53, and `private register*Endpoints(`
464+
// 17 -> 16: `registerReportsEndpoints` was deleted whole with the retired
465+
// saved-report stack — eight direct `this.routeManager.register(` sites
466+
// (73 -> 65), each guarded, so `enforceAuth` 64 -> 56 (eight call sites; the
467+
// registrar's comments did not name the term). None of the eight was
468+
// inside `registerMetadataEndpoints`, so `reachable` does not move and the
469+
// blind spot shrinks by exactly the eight routes that no longer exist.
460470
controls: {
461-
'private register*Endpoints(': 17,
462-
'this.routeManager.register(': 73,
471+
'private register*Endpoints(': 16,
472+
'this.routeManager.register(': 65,
463473
// Both halves of the new rule carry their own control, so neither can go
464474
// silently to zero: a helper deleted and its routes inlined back would
465475
// still read population 80, and only these two controls would notice the
466476
// shape moved and force this provenance to be re-read.
467477
'registerPerItemRoute(': 8,
468478
'const registerPerItemRoute =': 1,
469479
'forwarder slice: this.routeManager.register(': 1,
470-
enforceAuth: 64,
480+
enforceAuth: 56,
471481
},
472482
note:
473-
'The single non-tripwire probe names ONE registrar of 17. The other 16 can never mint a key: ' +
474-
'registerCrudEndpoints, registerApprovalsEndpoints, registerDataActionEndpoints, registerReportsEndpoints, ' +
475-
'registerSharingRuleEndpoints, registerUiEndpoints and the rest. A runtime mount census reads 85/19/66. ' +
483+
'The single non-tripwire probe names ONE registrar of 16. The other 15 can never mint a key: ' +
484+
'registerCrudEndpoints, registerApprovalsEndpoints, registerDataActionEndpoints, ' +
485+
'registerSharingRuleEndpoints, registerUiEndpoints and the rest. A runtime mount census reads 77/19/58. ' +
476486
'registerUiEndpoints is NOT special — it is simply the registrar a census happened to walk past.',
477487
},
478488
{
@@ -638,16 +648,17 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
638648
/**
639649
* Entry points inside the probe table's OWN files that no mintable key can
640650
* reach, counting only the route/handler surfaces the ratchet's completeness
641-
* claim is about: rest-server.ts (61 static / 66 runtime), http-dispatcher.ts
651+
* claim is about: rest-server.ts (53 static / 58 runtime), http-dispatcher.ts
642652
* (13) and domains/mcp.ts (1).
643653
*
644654
* hono-plugin.ts's 6 mounts are deliberately EXCLUDED from this total and
645655
* reported beside it: they are middleware and static-asset routes, and folding
646656
* them in would overstate the data surface. Its real finding is the dead probe,
647657
* not the six.
648658
*/
649-
export const BLIND_SPOT_TOTAL_STATIC = 75;
650-
export const BLIND_SPOT_TOTAL_RUNTIME = 80;
659+
// [#20102] 75 / 80 -> 67 / 72: the retired saved-report routes left rest-server.ts.
660+
export const BLIND_SPOT_TOTAL_STATIC = 67;
661+
export const BLIND_SPOT_TOTAL_RUNTIME = 72;
651662

652663
/**
653664
* Re-measure every row above from the same sources the probes read.

‎packages/qa/dogfood/test/authz-probe-blind-spot.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ describe('authz probe blind-spot census (#13260)', () => {
9191
0,
9292
);
9393
expect(staticTotal).toBe(BLIND_SPOT_TOTAL_STATIC);
94-
// The runtime reading differs only for rest-server.ts (85 mounts vs 80 call
94+
// The runtime reading differs only for rest-server.ts (77 mounts vs 72 call
9595
// sites — the approvals route factories and the capability-gated batch
9696
// routes; see the census header).
9797
expect(BLIND_SPOT_TOTAL_RUNTIME - BLIND_SPOT_TOTAL_STATIC).toBe(5);

0 commit comments

Comments
 (0)