Uh oh!
There was an error while loading. Please reload this page.
test(rest): measure what a swallowed execution context reads as at the packages door - #13153
Conversation
…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📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 3 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
Uh oh!
There was an error while loading. Please reload this page.
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 anundefinedexecution context means inside the permission decision, and three readings can produce byte-identical responses:(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— soundefinedis not a branch, it is a subject whose every field is absent.GETUNAUTHENTICATEDFORBIDDENisSystem: truecontext (control)⇒ 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
.catchis the SECOND net, not the first.RestServer.computeExecCtxwraps its whole body intry { … } catch { return undefined; }, so a production resolve fulfils withundefinedon 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
undefinedcontext at all: it escapes the non-asyncwrapper, lands in the route's owntry, andresolveThrownHttpErrorreads the status off the thrown error. The same seam thrown a plainErroranswers 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
packages/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.git hash-objectvs theHEADblob), restore under anEXIT/INT/TERMtrap using absolute paths: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.{ isSystem: true }: 6 red, including the byte-identity pin and the "service never reached" zero.vitestnot 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.rest-server.ts, instrument calibrated in both directions: base and head both emit sha256a54bd8a47802cc1df20f68140ceaa2c9e14fed6f14ffc80eb7987ec2f6a35722underremoveComments: true(EQUAL); a code-token mutant hashes976e407a…(DIFFERS); a comment-token mutant hashesa54bd8a4…(SAME).Test Files 4 passed (4)/Tests 175 passed (175).@objectstack/restbuild program and test program both green;check:test-typecheck: OKwith the debt ledger unchanged at3 file(s) / 6 error(s), none of them this file. Coverage stated rather than implied:--listFilesputs the new test in the test program (1 hit) and not the build program (0 hits).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.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.pnpm lintand no whole-farm run;check:type-check-debtneeds a built workspace closure and was not run locally (check:test-typecheckfor this package, the ratchet the new test file could move, is green with the ledger unchanged).check-test-completeness.mjsneeds a turbo test log CI supplies — NOT MEASURED, not a red. CI runs the farm regardless.Generated by Claude Code