Skip to content

test(rest): census what an absent execution context means at all 72 resolveExecCtx consumers - #13213

Merged
os-trump merged 1 commit into
mainfrom
claude/issue-13160-execctx-consumer-census
Aug 29, 2026
Merged

test(rest): census what an absent execution context means at all 72 resolveExecCtx consumers#13213
os-trump merged 1 commit into
mainfrom
claude/issue-13160-execctx-consumer-census

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#13160

A per-site census of what an absent execution context means at every resolveExecCtx consumer in packages/rest/src/rest-server.ts. Measurement only — this PR changes no behaviour and repairs nothing. One new test file, packages/rest/src/execctx-consumer-census.test.ts.

⛔ It does not generalise PR #13153's packages-door result. That door is measured there; every row below is measured here, independently.

How each row was produced

Every classification is driven, never read off the grep shape. The mounted handler is called under two wirings of one instrument — an absent context, and an entitled one — and the decision is read off the response. The site line reached by each driven route is read off the stack, so the route-to-site correspondence is observed rather than transcribed.

⭐ Every "this site denies" row stands beside a same-instrument row that is served. 30+ of the driven routes answer a real 2xx to an entitled caller on the same boot, and the audience gate answers 403 PERMISSION_DENIED to an entitled caller holding the wrong permission set — so the 401s are decisions, not a stuck needle.

Two corrections to the thread's numbers, re-derived from the tree

1. The caught/bare split is 20 / 52, not 16 / 52. The thread's own table does not add to 72, and this is why: four sites spell their .catch(() => undefined) on the continuation line, invisible to a single-line grep. 16 same-line + 4 continuation-line = 20 locally caught; 72 − 20 = 52 bare. The 52 was right; the 16 was the undercount. Both totals the card offered as controls hold: 72 invocation sites, 89 mentions.

2. ⭐ The sequencing rationale is inverted — the bare half is the guarded half. Measured, mechanically, on all 72:

52 of 52 bare sites carry if (this.enforceAuth(req, res, context)) return; on the immediately following line. 0 of the 20 locally-caught sites do.

The bare sites are bare because the shared anonymous floor is the very next statement. The locally-caught sites carry their own .catchbecause they sit outside that floor and each must decide for itself. The card's reasoning — "a reader at one of those lines sees an ordinary await and the swallow is 500 lines away" — is true about the swallow, but the reader also sees the guard on the next line. The surprise, if there is one, lives in the 20, not the 52. (The 52 were still done first, as dispatched; they are simply the uniform half.)

The census

Supplier (measured, not assumed). A faulting resolve fulfils with an absent context rather than rejecting, so a local .catch has nothing to catch on that path. Positive control: a healthy resolve fulfils with the context. The dispatch's line numbers for that method are stale by +31 — it is rest-server.ts:1853:2046 on origin/main (f951e945), not :1822:2015, and the wrapper's second net is :1521, not ~:1490. #13153's own 31-line comment block is the shift. The substance is unchanged.

The 52 bare sites — all 52 driven, none inferred. An absent context is the anonymous subject: every one refuses 401 UNAUTHENTICATED through the shared floor, and reaches no downstream service. Fail-CLOSED.

The umbrella (the metadata surface). One wrapper guards every /meta endpoint. An absent context is refused 401 there on every route outside the carve-out below.

With the umbrella isolated — a counterfactual, the same "isolate the floor to read the clause underneath" move #13153 used — the inner sites split in two:

own reading of an absent contextsites
refuses on its own (403 FORBIDDEN, capability clause) — double-guarded_drafts, _migrate-stored, meta item PUT, meta item DELETE, publish, rollback
does not refuse — reads it as "anonymous, no org scope" and serves — single-guardedmeta list, meta item read, /layers, /audit, /published, the object masker

⚠️ The second row is not a fail-open: on the production path the umbrella refuses first, which is measured. It is a defence-in-depth reading — for those five routes the umbrella is the only refusal on the path.

⭐ The one production path where an absent context reaches a consumer.isPublicAudienceRead deliberately lets an anonymous GET of the book/doc list, the book/doc item read and the book tree past the anonymous floor, so the ADR-0046 §6.7 audience gate decides instead. Measured across four audiences:

book audienceitem read, absent contextentitled control
undeclared401 UNAUTHENTICATED200
org401 UNAUTHENTICATED200
public200 — declared, ADR-0046 §6.7200
permission-set401 UNAUTHENTICATED403 PERMISSION_DENIED

Fail-CLOSED, and the gate reads an absent context as unauthenticated rather than as authenticated-holding-nothing — those are two different answers and it picks the first. The tree route answers the same way.

⚠️The list route does not refuse, it FILTERS.200 with an empty body is the refusal there and 200 with the item is the allow, so status alone reads it wrong. Asserted on the body.

⚠️ Two readings that look exactly like a fail-open and are not

Both were hit during this work and both are written into the file, because each produced a confident wrong answer first:

  1. An unwrapped metadata fixture.getMetaItem answers the declared envelope and the handler unwraps it once. A fixture returning the bare document leaves the unwrapped value absent, so every downstream gate guarded on it is skipped — including the audience gate — and the raw envelope is still served 200. That reads precisely as "the gate did not run for an anonymous caller". It was caught only because the sibling tree route refused on the same boot.
  2. A 200 whose body is empty, above.

What is NOT measured here

  • The packages-door wrapper (rest-server.ts:1521) is mounted by the package registrar, not by this server's own route registration, so it is outside this file's route table. Measured in package-door-execctx-fault-reading.test.ts (PR test(rest): measure what a swallowed execution context reads as at the packages door #13153) — fail-CLOSED, two ablation legs, both rival readings falsified. Recorded as deferred to that file, ⛔ never as "assumed closed". Every other one of the 72 is driven here.
  • check:test-completenessNOT MEASURED locally: it grades a saved test log, and its own text says the no-argument invocation is not a red and there is nothing to fix.

No fail-open found

⛔ No site in this census reads an absent execution context as "no gate applies". Nothing here is escalated as a security card.

Ablations — the census can go wrong

Both mutated after the implementation was committed, both confirmed on disk by blob hash before the run, both restored and the restore confirmed by blob equality plus an empty git diff HEAD. The mutation changed the outcome in each case, which is itself the proof that the resolution path is the source and not a stale build.

  • Removing one enforceAuth guard from a single bare site: 2 red — the 52-of-52 invariant, and the driven leg with GET /api/v1/data/:object under an absent context: expected 200 to be 401. The census names the exact door.
  • Disabling the audience gate: 4 red — all three refusing audiences flip to 200, and the entitled 403 discriminator flips too.

Verification

Union re-run on the final commit 7c31930e.

  • packages/rest: 156 test files, 2580 tests passed; typecheck exit 0. The new file is inside the test tsc program (--listFiles, 1 hit) — not a green over source nothing read.
  • check:authz-resolver — the authorization-relevant gate for this file — its own verdict line: "single shared authorization resolver intact; both entry points delegate."
  • Gate family derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack from the real change set (1 path), plus the convention-triggered set: all green — cross-package-test-inputs, dispatcher-error-vocabulary, logger-receiver-detach, objectql-double-limit, page-declaration-shape, published-files, slot-lookup, test-source-alias, type-source-resolution, dual-build-cjs-loads, query-options-erasure, engine-double-contract, where-matcher, type-check-coverage, type-check-debt, route-envelope, nul-bytes, ci-filter-parity, comment-mask-adoption, keyed-text-bounds, plugin-teardown-shape, shard-attestation, undeclared-dep-imports, affected-docs, drift-comment.
  • check:type-check-debt --re-measure and check:dual-build-cjs-loads both refused on an unbuilt worktree (exit 3 / prerequisite, not a red). The workspace closure was built and both then ran green — "30 ledger entries re-measured, none above its recorded number" and "102 published require entry points across 66 packages load".

No changeset: this PR publishes nothing from any package. skip-changeset applied.


Generated by Claude Code

…esolveExecCtx consumers
Every row is DRIVEN — the mounted handler is called under two wirings of one
instrument (an absent context, and an entitled one) and the decision is read
off the response, never off the grep shape. The site reached by each driven
route is read off the stack, so the route-to-site correspondence is observed
rather than transcribed.
Measured:
- all 52 bare sites read an absent context as the ANONYMOUS SUBJECT and
refuse 401 UNAUTHENTICATED, each with a same-instrument positive control;
- the metadata umbrella refuses the same on every non-carve-out /meta route;
- with the umbrella isolated, six inner sites do not refuse on their own
reading (single-guarded) while six do (double-guarded);
- the public-audience carve-out — the one production path where a swallowed
context reaches a consumer — is fail-CLOSED at the item read and the tree,
and fail-CLOSED by FILTERING at the list, where a 200 with an empty body is
the refusal.
Two corrections to the thread's counts, both re-derived from the tree:
the caught/bare split is 20/52, not 16/52 (four sites spell the catch on the
continuation line and a single-line grep misses them, which is why 16+52 does
not reach 72); and all 52 bare sites are guarded by enforceAuth on the very
next line while none of the 20 caught ones is — the inverse of the reason the
thread gave for doing the bare half first.
No behaviour change: one new test file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json a51da094b04a4510d220776def16e0405d8045d4packageMentionDocs.

@os-trumpos-trump added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 29, 2026 — with Claude
@os-trump
os-trump marked this pull request as ready for review August 29, 2026 12:33
@os-trump
os-trump added this pull request to the merge queueAug 29, 2026
Merged via the queue into main with commit 4801296Aug 29, 2026
37 checks passed
@os-trump
os-trump deleted the claude/issue-13160-execctx-consumer-census branch August 29, 2026 12:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

1 participant

@os-trump