Skip to content

test(rest): measure what a swallowed execution context reads as at the packages door - #13153

Merged
os-trump merged 1 commit into
mainfrom
claude/issue-12537-execctx-swallow-downstream-read
Aug 29, 2026
Merged

test(rest): measure what a swallowed execution context reads as at the packages door#13153
os-trump merged 1 commit into
mainfrom
claude/issue-12537-execctx-swallow-downstream-read

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Part of #12537 — the rider (part 2) of the maintainer ruling of 2026-08-29 (option B). This is a measurement, not a fix: zero wire-behaviour change, and option A (un-swallowing) is deliberately not done here.

The question, and why the thread's answer did not close it

The thread already carried a wire-level reading: a resolver that throws synchronously reaches sendThrownError, one that rejects is swallowed and the caller sees the anonymous floor. That answers which status code comes out. The rider asks what an undefined execution context means inside the permission decision, and three readings can produce byte-identical responses:

  1. a subject that holds nothing (evaluated, denies),
  2. an evaluation that is skipped,
  3. a fall-through to a default / system subject.

(2) and (3) are fail-open postures; (1) is not.

Measured answer: (1), at both clauses. The swallow fails CLOSED.

refusePackageRequest (packages/rest/src/package-routes.ts) touches the resolved context through optional chaining only — ctx?.userId, ctx?.isSystem, ctx?.systemPermissions — so undefined is not a branch, it is a subject whose every field is absent.

driveanswerwhat it shows
rejecting resolver, GET401 UNAUTHENTICATEDread as the anonymous subject; refused at the floor
rejecting resolver, anonymous floor isolated403 FORBIDDENcapability clause evaluates it as holding the empty set
isSystem: true context (control)200the door can serve — the refusals are decisions, not artefacts
capable context (control)200allow is observable

⇒ not skipped, not a system subject. ⛔ The stop condition in the ruling does not fire: this is not a permission-adjacent fail-open.

Two things that correct the record

1. This .catch is the SECOND net, not the first.RestServer.computeExecCtx wraps its whole body in try { … } catch { return undefined; }, so a production resolve fulfils with undefined on a fault instead of rejecting — the wrapper's .catch(() => undefined) has nothing to catch on that path, and the fault-to-anonymous conversion happens one level below it. Pinned white-box, against a control that injects a real inner rejection and shows the wrapper is what swallows when there is something to swallow.

2. "sync throw ⇒ 403 PERMISSION_DENIED" is not a fact about this door. A sync throw produces no undefined context at all: it escapes the non-async wrapper, lands in the route's own try, and resolveThrownHttpError reads the status off the thrown error. The same seam thrown a plain Error answers 500, measured. So the 403 was a property of the injected error, not a gate decision. This does not re-open the seam census — the sync limb remains the declared test-only injection point.

What the swallow actually costs

Diagnosability, not permission: a faulting resolver, an unwired resolver and a genuinely anonymous caller are one answer, byte-identical on the wire. Pinned, with a control showing the same comparison separates two answers that differ.

Verification

  • New suitepackages/rest/src/package-door-execctx-fault-reading.test.ts: Test Files 1 passed (1) / Tests 20 passed (20). Every "did not happen" reading carries a same-shaped positive control.
  • Ablation, both legs, mutation and restore proved on disk by blob hash (git hash-object vs the HEAD blob), restore under an EXIT/INT/TERM trap using absolute paths:
    • leg A — make the capability clause skip evaluation for undefined: 1 red, and it is the anonymous-floor-isolated case. Every other assertion stayed green, because the floor short-circuits ahead of the capability clause on every wire-reachable method. That leg is why the isolation instrument exists.
    • leg B — make the swallow yield { isSystem: true }: 6 red, including the byte-identity pin and the "service never reached" zero.
    • ⚠️ A first ablation attempt exited 127 (vitest not on the path used) — the mutation was on disk but nothing ran. Recorded as NOT MEASURED and re-run; the numbers above are from the run that happened.
  • Comment-only proof for rest-server.ts, instrument calibrated in both directions: base and head both emit sha256 a54bd8a47802cc1df20f68140ceaa2c9e14fed6f14ffc80eb7987ec2f6a35722 under removeComments: true (EQUAL); a code-token mutant hashes 976e407a… (DIFFERS); a comment-token mutant hashes a54bd8a4… (SAME).
  • Sibling suites unaffected: the four package-door suites together, Test Files 4 passed (4) / Tests 175 passed (175).
  • Typecheck: @objectstack/rest build program and test program both green; check:test-typecheck: OK with the debt ledger unchanged at 3 file(s) / 6 error(s), none of them this file. Coverage stated rather than implied: --listFiles puts the new test in the test program (1 hit) and not the build program (0 hits).
  • Gates from node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (change set taken from git itself), exit codes captured before any pipe — 20 families run, all exit 0.
  • Ratchets re-run after the final commit: RATCHET RUN AT HEAD=a5ca6b21d (worktree clean: 0 path(s) dirty) — nul-bytes, where-matcher, objectql-double-limit, engine-double-contract, query-options-erasure, slot-lookup, type-check-coverage all exit 0.
  • Narrowing declared: no repo-wide pnpm lint and no whole-farm run; check:type-check-debt needs a built workspace closure and was not run locally (check:test-typecheck for this package, the ratchet the new test file could move, is green with the ledger unchanged). check-test-completeness.mjs needs a turbo test log CI supplies — NOT MEASURED, not a red. CI runs the farm regardless.

Generated by Claude Code

…e packages door
The packages door swallows a failing execution-context resolution into
`undefined`. The thread carried a wire-level reading of that (401 vs 403)
but not an INTERNAL one, and the three internal readings that response
could reflect are different security postures:
1. a subject that holds nothing (evaluated, denies),
2. an evaluation that is skipped,
3. a fall-through to a default / system subject.
Measured: (1), at both clauses of the gate. `refusePackageRequest` touches
the context through optional chaining only, so `undefined` is the anonymous
subject rather than a branch. On every wire-reachable method the anonymous
floor decides and refuses (401); with that floor isolated, the capability
clause reads the same `undefined` as holding the empty capability set and
refuses again (403). It fails CLOSED — not a permission-adjacent fail-open.
Also measured, and it corrects the reason on record: the wrapper's
`.catch(() => undefined)` is the SECOND net. `computeExecCtx` wraps its
whole body in `try { … } catch { return undefined; }`, so a production
resolve fulfils with `undefined` on a fault instead of rejecting, and the
fault-to-anonymous conversion happens one level below the swallow.
No behaviour change: a new test file, plus a comment block on the wrapper
whose emit is byte-identical under `removeComments` with the instrument
calibrated in both directions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest, touching 1 documentable anchor(s).

3 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via RestServer (symbol))
  • content/docs/releases/v12.mdx(via RestServer (symbol))
  • content/docs/releases/v16.mdx(via RestServer (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

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 — 13 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 56470d86bf25269b90fad0123cbee6282bc08be7packageMentionDocs.

Which tree this was computed on

This run read content/docs from 791fa3d7d18905c77dd40eeb2749e5dd39d0acee — the merge of head a5ca6b21d1888eff13b4f1268b3f5bbfb5702600 into base 56470d86bf25269b90fad0123cbee6282bc08be7, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 791fa3d7d18905c77dd40eeb2749e5dd39d0acee && git checkout 791fa3d7d18905c77dd40eeb2749e5dd39d0acee
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 56470d86bf25269b90fad0123cbee6282bc08be7 a5ca6b21d1888eff13b4f1268b3f5bbfb5702600 && git checkout -B drift-repro 56470d86bf25269b90fad0123cbee6282bc08be7 && git merge --no-ff a5ca6b21d1888eff13b4f1268b3f5bbfb5702600
node scripts/docs-audit/affected-docs.mjs --json 56470d86bf25269b90fad0123cbee6282bc08be7

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 56470d86bf25269b90fad0123cbee6282bc08be7 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-litantos-litant 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 10:08
@os-trump
os-trump added this pull request to the merge queueAug 29, 2026
Merged via the queue into main with commit ba2ffbcAug 29, 2026
37 checks passed
@os-trump
os-trump deleted the claude/issue-12537-execctx-swallow-downstream-read branch August 29, 2026 10:55
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-litant@os-trump@claude