test(rest): drive the #13906 fail-closed window against the auth-gate allow-list (#15021 measurement) - #16658
Merged
Conversation
… allow-list Measurement only — no behaviour change, and `rest-server.ts` is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
…th-gate-allowlist-window
…urement Both are the grading inputs triage named: whether the window latches across requests, and whether the gate's own re-read can fail while identity resolution stayed healthy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
…ation path The reachability qualifier the fork needs: §2 proves the seam is path-blind, this proves the refusal reaches no remediation route on this door today. Red here means that qualifier is gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
Contributor
📓 Docs Drift CheckNothing 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
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Sep 7, 2026
os-sales
marked this pull request as ready for review
September 7, 2026 16:51
os-sales
enabled auto-merge
September 7, 2026 16:52
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #15021 — the measurement half only.
⛔ #15021 remains open after this PR merges, deliberately, and no closing keyword names it anywhere in this body. The card's remaining half is a runtime-authorization fork that the 2026-08-28 negative-boundary ruling reserves to the maintainer, so the card has to survive to reach the decision inbox.
⛔ No behaviour change.
packages/rest/src/rest-server.tsis not edited — that file is held by open PR #16648 (card #16337), and a measurement should not need to rewrite the door it reads. The whole diff is one new test file.Why this exists
#15021 declares itself NOT MEASURED in its author's own words — "I did not drive this" — and is a reading of where the
AuthzStoreUnavailableErrorthrow sits relative to thepathExemptcomputation inenforceAuth. Triage routed it as work rather than escalating it, and scoped the work to exactly one thing:This is that drive.
Verdict —⚠️ IT REPRODUCES
Under the #13906 decision-2-B window (
isAuthGateActive()answeredtrueand the gate's session re-read then failed), every allow-listed path receives the same 503 as a protected one.enforceAuthis never reached, sopathExemptis never computed for that request, and the refusal is byte-identical on an allow-listed and a protected path.⛔ Nothing is chosen here. The card's options 2 and 3 are a runtime-authorization judgement; this PR states the reading and stops.
What was driven
All legs run the real composition — the public entry to
computeExecCtx(resolvePackageRouteExecutionContext), then the real consumer (enforceAuth) on the same request. Constructor seams only; nothing private is replaced.SERVICE_UNAVAILABLE/object: auth_gate, re-read counter proves the fault was reachedauthGate,enforceAuthdoes not blockcodeisAuthGateAllowlistednorreq.pathCONTROL B is what makes the verdict readable: on this exact fixture, on these exact paths, the allow-list does keep a gated user through. Remove only the fault and the door opens; put the fault back and it does not. So the 503 is the window, not an allow-list that never applied.
⭐ Reachability qualifier — please read this before ruling
The card's impact sentence is "a gated user cannot remediate during a session-backend fault." The seam-level finding above is certain. The impact claim needs one more fact, and §3 measures it:
GET /api/v1/discovery,GET /api/v1/environments/:environmentId/discovery).GET /api/v1/discoveryanswers 200 while the gate re-read is failing: its handler resolves no execution context, so the window cannot reach it./api/v1/:object/:idwould capture/api/v1/auth/sign-in. Measured against the shipped:param/*semantics, over five canonical remediation paths and all three configurations: zero mounted patterns capture any of them. That is now a pinned assertion, not a recorded number.computeExecCtx—plugin-authmountsrawApp.all('/api/v1/auth/*')(defaultbasePath/api/v1/auth) andplugin-hono-serverregisters/auth/me/*ahead of it.⇒ On today's tree the defect is real at the seam and not reachable through any mounted remediation route on this door. That is a materially different thing to rule on than the card's impact sentence implies, in the direction that makes option 1 easier — and it is fragile in the direction that makes it harder. §5 is the tripwire: if it ever goes red, a mounted route now answers a remediation path, the qualifier is gone, and §2 says every such answer is a 503 for the duration of the fault. ⛔ It must not be relaxed to let a new mount pass.
Grading inputs for triage — ⛔ not a re-grade
Triage named the two conditions that would move
priority:p3. Both are measured (§4), and both are reported as readings:WeakMapon the request object), so the refusal does not carry forward.The fork — for the decision inbox
Stated as the card states them, ⛔ with no recommendation and no ranking:
isAuthGateAllowlisted(req.path)before raising. Cost:computeExecCtxstarts branching on the request path for an authorization decision, and the memoized per-request context becomes path-sensitive.resolveExecCtxcaches on(req, environmentId)with no path in the key.enforceAuth— carry "the gate could not be read" as a distinct context value and let the consumer that already knows aboutpathExemptdecide. WidensExecutionContext.Options 2 and 3 both change
packages/rest/src/rest-server.ts, which #16648 holds. Whoever takes the ruling should sequence against that PR.Verification
All of it ran on
d084b089da, this branch's final commit.pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/auth-gate-allowlist-fault-window.measurement.test.ts— 14 tests, all green.pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/rest-auth-gate.test.ts src/execctx-authz-input-seam-reachability.test.ts src/rest-exec-ctx-principal-kind.test.ts src/package-door-execctx-fault-reachability.test.ts— the sibling pins on this window: 4 files, 93 tests, all green and unmodified.pnpm --filter @objectstack/rest typecheck.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsand reconciled with--ran: 46 derived, 46 run, 0 UNRUN. 44 exit 0.pnpm check:dual-build-cjs-loadsandpnpm check:type-check-debt. Both read built output across the whole tree, and this worktree built only the@objectstack/restdependency closure. Declared to CI, which builds the whole tree and is the authority on them. The narrowing is safe to declare rather than close locally for a reason that is checkable: this diff adds one.test.ts,git grepfinds no importer of it anywhere in the repo, and tsup's entry issrc/index.ts— so nothing it changes can reach any package'sdist/.Every exit code above was captured before any pipe.
skip-changeset: this diff publishes nothing.packages/rest'sfilesis["dist","README.md","CHANGELOG.md"]and tsup's entry issrc/index.ts, so a.test.tsundersrc/never reachesdist.验收备注
Out of scope for this card, filed nowhere, recorded here so ACCEPT can see them:
ALLOW_SUFFIXESrule inpackages/core/src/security/auth-gate.tsis a suffix test on the path, not a route test, so a concrete/api/v1/data/health(an object literally namedhealth) reads as allow-listed and the ADR-0069 gate does not fire on it. That over-broad direction is [Decision] Fail-close isAuthGateAllowlisted's "no path ⇒ exempt" default (Option 2 of #7432) #7898's subject — the card explicitly asks that the two not be merged by keyword, and they are not merged here. Noted, not filed.pathExemptinsideenforceAuthis reached by no allow-listed route pattern this server mounts (§3). Whether that makes the branch load-bearing only through the:objectsubstitution above is a question about [Decision] Fail-close isAuthGateAllowlisted's "no path ⇒ exempt" default (Option 2 of #7432) #7898's territory, not this card's. Noted, not filed.If a repair lands
§2 pins what the tree does, including the answer the card questions. Options 2 and 3 both change it. When one lands, re-aim the assertion in place and quote the superseded text beside it — the discipline
execctx-authz-input-seam-reachability.test.tsalready follows for this same window. ⛔ Do not delete the leg.🤖 Generated with Claude Code
https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
Generated by Claude Code