You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Splitting the Option 2 half of #7432 into its own decision card so it stays in the maintainer inbox after #7432 closes on PR #7836 (Option 1, the instance fix). The evidence is the dev's caller census on #7432, comment 5257880748.
Background
isAuthGateAllowlisted(undefined) returns true — it treats "no path" as allow-listed (packages/core/src/security/auth-gate.ts:67, if (!rawPath) return true). That is a fail-open default: any caller that reaches the ADR-0069 gate with an absent/empty path silently exempts the request. Option 1 (#7836) guards the one REST call site that could reach it by omission. Option 2 is the question of whether to remove the class at its source rather than guarding each call site.
Premises (each with a re-check command — run before acting)
Fail-open leg live: git show origin/main:packages/core/src/security/auth-gate.ts | sed -n '60,70p' → if (!rawPath) return true.
Census result — no caller depends on no path ⇒ exempt: 4 production call sites, bucketed by intent (comment 5257880748). Re-derive: git grep -n "isAuthGateAllowlisted" origin/main -- 'packages/**/*.ts' (excluding tests).
B1 packages/core/src/security/anonymous-deny.tsshouldDenyAnonymous — genuinely pathless, already guards the falsy branch before the helper sees it.
The one thing a flip must price in (A2): http-dispatcher.ts computes cleanPath = path.replace(/\/$/, ''), so ${prefix}/ yields cleanPath === '', reachable on a shipped transport (adapters/hono). Today that reaches only the discovery payload (gate-exempt by design) or a 404 — not a bypass. Re-check: git grep -n "replace(/\\\\/\$/" origin/main -- packages/runtime/src/http-dispatcher.ts.
The concrete question
Do we flip isAuthGateAllowlisted (and its seams) fail-closed — a falsy/empty path is not exempt — removing the fail-open class at source, and if so, how do we handle the A2 bare-root ${prefix}/ case?
Options
A (recommended) — fail-close at source + normalize. Split the helper so a falsy/empty path returns false (not exempt), keeping an explicit exemption only for the genuinely-body-routed seams (B1's contract). Normalize cleanPath '' → '/' in http-dispatcher.ts so ${prefix}/ resolves to root rather than the empty string. Removes the fail-open default; the per-call-site guards become belt-and-suspenders. Cross-domain: the helper is packages/core (domain:engine-core), the normalization is packages/runtime (domain:cli) ⇒ if chosen, contract-first split (core first, runtime after).
C — fail-close, gate the bare root. Flip the helper as in A but do not normalize: GET ${prefix}/ becomes 403 for a gated session. Smaller diff, but changes the observable behaviour of the bare-root discovery request for gated sessions.
Recommendation: A, with an honest appetite caveat
Fail-closed is the declared-=-enforced direction and the census removes the migration risk (zero current dependency on the old behaviour). But nobody hits this today, so the pull is preventive, not user-facing — which is exactly why it is an appetite call rather than a restore-invariant I would just dispatch. If the appetite is not there now, B is defensible: the seams are safe today and the promotion trigger is already recorded.
Four-lens analysis
Platform long-term coherence — A removes a helper that carries two meanings (a real allow-list decision, and "no path" silently reusing the allow answer); B leaves that latent trap guarded only by caller discipline.
Measured business pull — zero today: the adapter always sets path, and A2's empty-path case reaches only a gate-exempt discovery payload. The pull is preventing a future transport author from re-opening the hole, not a user-visible defect. This axis lowers urgency and is the strongest argument for B/defer.
Startup scope discipline — A is a real (small) change across two lanes plus a normalization; B adds zero surface. Given zero current pull, deferring is the scope-disciplined choice; adopting A is justified only as cheap preventive hardening of a security default.
Lenses 1/3 argue for A, lenses 2/4 argue for B — a genuine appetite trade-off, which is why it is escalated rather than decided from the seat.
Related: #7432 (parent finding, closes via #7836), census comment 5257880748, sibling seam anonymous-deny.ts.
Splitting the Option 2 half of #7432 into its own decision card so it stays in the maintainer inbox after #7432 closes on PR #7836 (Option 1, the instance fix). The evidence is the dev's caller census on #7432, comment
5257880748.Background
isAuthGateAllowlisted(undefined)returnstrue— it treats "no path" as allow-listed (packages/core/src/security/auth-gate.ts:67,if (!rawPath) return true). That is a fail-open default: any caller that reaches the ADR-0069 gate with an absent/empty path silently exempts the request. Option 1 (#7836) guards the one REST call site that could reach it by omission. Option 2 is the question of whether to remove the class at its source rather than guarding each call site.Premises (each with a re-check command — run before acting)
git show origin/main:packages/core/src/security/auth-gate.ts | sed -n '60,70p'→if (!rawPath) return true.no path ⇒ exempt: 4 production call sites, bucketed by intent (comment5257880748). Re-derive:git grep -n "isAuthGateAllowlisted" origin/main -- 'packages/**/*.ts'(excluding tests).packages/restenforceAuth— path-bearing; now guarded by fix(rest): exempt a request from the ADR-0069 gate only when it carries a real path (#7432) #7836.packages/runtime/src/http-dispatcher.tsenforceAuthGate— path-bearing (cleanPath: stringrequired).packages/core/src/security/auth-gate.tsevaluateAuthGate— path required, non-nullable.packages/core/src/security/anonymous-deny.tsshouldDenyAnonymous— genuinely pathless, already guards the falsy branch before the helper sees it.http-dispatcher.tscomputescleanPath = path.replace(/\/$/, ''), so${prefix}/yieldscleanPath === '', reachable on a shipped transport (adapters/hono). Today that reaches only the discovery payload (gate-exempt by design) or a 404 — not a bypass. Re-check:git grep -n "replace(/\\\\/\$/" origin/main -- packages/runtime/src/http-dispatcher.ts.The concrete question
Do we flip
isAuthGateAllowlisted(and its seams) fail-closed — a falsy/empty path is not exempt — removing the fail-open class at source, and if so, how do we handle the A2 bare-root${prefix}/case?Options
false(not exempt), keeping an explicit exemption only for the genuinely-body-routed seams (B1's contract). NormalizecleanPath '' → '/'inhttp-dispatcher.tsso${prefix}/resolves to root rather than the empty string. Removes the fail-open default; the per-call-site guards become belt-and-suspenders. Cross-domain: the helper ispackages/core(domain:engine-core), the normalization ispackages/runtime(domain:cli) ⇒ if chosen, contract-first split (core first, runtime after).no path ⇒ exempt. The remaining seams are safe by construction today (the hono adapter populatespath; A2's empty-path reaches only discovery/404). Re-open if a second transport adapter or a synthetic-request caller appears — the promotion trigger already recorded on finding: REST'senforceAuthpassesreq.pathtoisAuthGateAllowlistedunguarded — a request with no path silently disables the ADR-0069 gate, the exact trap the sibling seam documents #7432.GET ${prefix}/becomes 403 for a gated session. Smaller diff, but changes the observable behaviour of the bare-root discovery request for gated sessions.Recommendation: A, with an honest appetite caveat
Fail-closed is the declared-=-enforced direction and the census removes the migration risk (zero current dependency on the old behaviour). But nobody hits this today, so the pull is preventive, not user-facing — which is exactly why it is an appetite call rather than a restore-invariant I would just dispatch. If the appetite is not there now, B is defensible: the seams are safe today and the promotion trigger is already recorded.
Four-lens analysis
path, and A2's empty-path case reaches only a gate-exempt discovery payload. The pull is preventing a future transport author from re-opening the hole, not a user-visible defect. This axis lowers urgency and is the strongest argument for B/defer.path; the failure becomes a compile/behaviour signal instead of a silent fail-open. B relies on every future caller remembering the guard fix(rest): exempt a request from the ADR-0069 gate only when it carries a real path (#7432) #7836 adds at one site.Lenses 1/3 argue for A, lenses 2/4 argue for B — a genuine appetite trade-off, which is why it is escalated rather than decided from the seat.
Related: #7432 (parent finding, closes via #7836), census comment
5257880748, sibling seamanonymous-deny.ts.