Skip to content

[Decision] Fail-close isAuthGateAllowlisted's "no path ⇒ exempt" default (Option 2 of #7432) #7898

Description

@hotlong

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).
    • A1 packages/rest enforceAuth — path-bearing; now guarded by fix(rest): exempt a request from the ADR-0069 gate only when it carries a real path (#7432) #7836.
    • A2 packages/runtime/src/http-dispatcher.ts enforceAuthGate — path-bearing (cleanPath: string required).
    • A3 packages/core/src/security/auth-gate.ts evaluateAuthGate — path required, non-nullable.
    • B1 packages/core/src/security/anonymous-deny.ts shouldDenyAnonymous — 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).
  • B (defer / status quo). Land Option 1 (fix(rest): exempt a request from the ADR-0069 gate only when it carries a real path (#7432) #7836) only; leave the helper's no path ⇒ exempt. The remaining seams are safe by construction today (the hono adapter populates path; 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's enforceAuth passes req.path to isAuthGateAllowlisted unguarded — a request with no path silently disables the ADR-0069 gate, the exact trap the sibling seam documents #7432.
  • 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

  1. 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.
  2. 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.
  3. AI-agent error-resistance — A is strongest: a future adapter author cannot disable the gate by omitting 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.
  4. 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.


Generated by Claude Code

Activity

  1. hotlong commented on Aug 12, 2026

    @hotlong
    ContributorAuthor

    Maintainer ruling — 2026-08-12

    Ruled: Option B — defer. Option 1 (PR #7836's call-site guard) stands as the shipped protection; isAuthGateAllowlisted's "no path ⇒ exempt" default stays for now. Decision made, answer is "not now" ⇒ pm:on-hold (out of the decision box, not dispatched, not nagged).

    Provenance: maintainer, triage PM session chat (session_01NKGoRBFZELVKivDkiAswdz), 2026-08-12, verbatim: 「其他两张按照你的建议」 — adopting the seat's recommendation on this card (as presented: lenses ①/③ favor A, ②/④ favor B; zero current pull — census shows no caller depends on the exempt default and the hono adapter always populates path — makes defer the scope-disciplined choice).

    Hold record (2026-08-12):

    • Reason: purely preventive hardening; no reachable exposure today (A2's empty-path case reaches only gate-exempt discovery or 404).
    • Restart conditions — any one fires this back to the queue as the Option-A implementation card (fail-close at source + '' → '/' normalization, contract-first split: packages/core first, packages/runtime after):
      1. A second transport adapter appears (any dispatcher-request producer beyond adapters/hono).
      2. Any synthetic-request caller reaches the auth gate without a populated path.
      3. Any measured empty-path request reaching a non-discovery route.
    • Trigger files (any PR touching these must check this hold — for the lane seats' 派发前必查 sections): packages/core/src/security/auth-gate.ts · packages/runtime/src/http-dispatcher.ts · packages/adapters/** (new adapter = trigger 1 by definition).

    Generated by Claude Code

  2. hotlong commented on Aug 12, 2026

    @hotlong
    ContributorAuthor

    Half-state repair: removed needs-user-decision (kept pm:on-hold + domain:cli). Provenance: the 2026-08-12 08:15Z maintainer ruling on this card (Option B — defer) states "Decision made, answer is 'not now' ⇒ pm:on-hold (out of the decision box)"; the box label was never removed, leaving the card in both states at once. No new decision is made here — this completes the recorded one. Hold record and restart conditions in the 08:15Z comment stand unchanged.

    本评论来自分诊座位(scheduled session session_01DLzE5XVRASxFotEU1h9cjH),不构成认领。


    Generated by Claude Code

  3. os-steve commented on Aug 31, 2026

    @os-steve
    Collaborator

    ⚠️ Restart-when FIRED — recording it now, discharge in progress

    domain:cli execution PM seat (#6024) · session session_01UngCYXF98BVpYA9hfz6NYk

    This card's exit predicate, verbatim from its own first line:

    Restart-when: any PR touches packages/core/src/security/auth-gate.ts, packages/runtime/src/http-dispatcher.ts or adds an adapter under packages/adapters/**

    PR #13763 (#13408) modifies packages/runtime/src/http-dispatcher.ts — +189 / −21. ⇒ The predicate fires, literally and unambiguously, on a file this card names by path.

    Recording the firing before the answer is known, on purpose: a predicate that fires and lives only in the acting seat's head is indistinguishable from a predicate that never fired. The whole point of writing an exit condition down is that someone else can audit whether it was honoured.

    ⛔ Firing is NOT promotion — and the distinction is in this card's own text

    The implementing dev reported the firing accurately and correctly declined to act (acting on a hold is not a dev's call). It then compared the firing against the hold's promotion triggers — a second transport adapter, a synthetic-request caller — and found neither fires. True, but that is the wrong comparison, and the difference matters:

    • Promotion triggers ask "has the appetite changed?"
    • This file-touch clause asks "is the premise still true?"

    Option B is defensible today only because of a specific measured claim about that exact file:

    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.

    ⇒ ANY touch of that file wakes this card because a 189-line edit to it can quietly invalidate that sentence. "The promotion triggers didn't fire" does not answer "does A2 still hold".

    What I have asked for — a reading, not an opinion

    The implementing dev has been sent back to run this card's own re-check command on both its branch head and its merge base:

    git grep -n "replace(/\\/$/" -- packages/runtime/src/http-dispatcher.ts
    

    plus three questions: does cleanPath still yield '' for ${prefix}/; can any of the 189 added lines be reached with an empty cleanPath (i.e. is the new /ready code before or after the auth-gate seam, and is /ready gate-exempt by construction); and were any enforceAuthGate / isAuthGateAllowlisted calls added, moved or bypassed.

    ⇒ If A2's premise survives, I re-arm the hold with the measurement attached, and this comment becomes the audit record that the predicate was honoured rather than ignored. ⭐ That is worth more than the hold's status either way: the next firing will have a worked example of what discharging one looks like.

    ⇒ If it moved, the card is genuinely restarted and goes back to the maintainer with the reading — ⛔ and this seat still does not decide it. The card is an appetite call between A and B with lenses 1/3 against 2/4; that was escalated deliberately and nothing here changes it.

    ⛔ Nothing has been decided, re-graded, or dispatched on this card. pm:on-hold stands until the reading is in.


    Generated by Claude Code

  4. os-steve commented on Aug 31, 2026

    @os-steve
    Collaborator

    ✅ Predicate DISCHARGED — A2's premise re-measured and INTACT. Hold re-armed, unchanged.

    domain:cli execution PM seat (#6024) · session session_01UngCYXF98BVpYA9hfz6NYk

    Following my comment 5476861538: PR #13763 fired this card's file-touch clause by editing packages/runtime/src/http-dispatcher.ts (+189/−21). I said a reading was coming rather than an opinion. Here it is, measured by that PR's implementer on both its branch head and its merge base.

    1. The premise sentence itself — byte-identical

    This card's own re-check command, run on both refs:

    git grep -n "replace(/\/$/" -- packages/runtime/src/http-dispatcher.ts
    

    Returns a byte-identical statement on both — sha256 94417612249abecb. Only the line number moved. ⇒ cleanPath = path.replace(/\/$/, '') is unchanged, and ${prefix}/ still yields cleanPath === ''.

    2. ⭐ The new code is strictly downstream of the gate — measured by call order, not asserted

    dispatch()'s order on the branch head:

    step line
    cleanPath computed 2162
    resolveRequestScope 2174
    enforceAuthGate 2181
    domainRegistry.resolve 2210

    ⇒ The /ready handler resolves at 2210, after the gate at 2181. None of the 189 added lines sits between cleanPath and the gate.

    3. The empty-cleanPath path cannot reach the new code at all

    The /ready route is declared match: 'exact', and '' === '/ready' is false. ⇒ On the bare-root ${prefix}/ request this card is about, the added lines are unreachable — the case the card prices in does not touch them.

    4. /ready is gate-exempt by construction, and that did not move either

    Via ALLOW_SUFFIXES at auth-gate.ts:63 — identical on both refs. So the new handler is not relying on anything the diff introduced.

    5. No seam was added, moved, or bypassed

    The enforceAuthGate / isAuthGateAllowlisted call-site set diffs to nothing across the change, and packages/core and packages/adapters/** are untouched by PR #13763.


    ⇒ Verdict: the hold's premise SURVIVES. pm:on-hold stands, unchanged.

    A2 still reads exactly as this card records it: ${prefix}/ yields an empty cleanPath, reachable on the shipped hono adapter, and it still reaches only the gate-exempt discovery payload or a 404 — not a bypass.

    ⛔ Nothing decided, re-graded, promoted or dispatched. The A-versus-B appetite call is untouched and remains the maintainer's: lenses 1/3 for A, lenses 2/4 for B, exactly as escalated. ⛔ The substantive promotion triggers (a second transport adapter, a synthetic-request caller) did not fire — PR #13763 adds no adapter.

    ⭐ Why this comment exists at all

    The file-touch clause is a premise-staleness trigger, not a promotion trigger, and the two are easy to conflate — the implementing dev initially compared the firing against the promotion triggers, found neither fired, and concluded nothing was owed. That reasoning was reasonable and reached the right outcome by the wrong route: it would have held even if the diff had rewritten cleanPath.

    ⇒ The discharge is what makes the difference visible. This card now carries a worked example: a fired predicate is discharged by re-measuring the premise it protects, and the reading is recorded whether it survives or not. A predicate that fires, is judged in someone's head, and leaves no trace is indistinguishable from one that never fired — which is the whole reason it was written down.

    ⚠️ Note for the next firing: the line numbers in this card's Premises section have moved (they moved again in PR #13763). ⛔ Re-derive with the card's own grep; do not quote its line numbers.


    Generated by Claude Code

  5. os-steve commented on Aug 31, 2026

    @os-steve
    Collaborator

    ⚠️ This card's own re-check command is UNRUNNABLE as written — it returns a FALSE ZERO

    domain:cli execution PM seat (#6024) · session session_01UngCYXF98BVpYA9hfz6NYk

    Second discharge of this card's file-touch predicate today (PR #13811, #13623, packages/runtime/src/http-dispatcher.ts). The premise is again intact — reading below — but the implementer hit something that matters more than this firing:

    The defect

    The command this card prescribes for its own re-check:

    git grep -n "replace(/\/$/" -- packages/runtime/src/http-dispatcher.ts
    

    ⛔ returns zero through a shell, because git grep reads the pattern as a regex — (, /, $ are not literals there. The next author who discharges this hold runs the card's own command and is told "the premise sentence is gone", which is the single most alarming thing this card could falsely report: A2's premise vanishing is exactly what would force a restart.

    The runnable form — fixed-string, and never without a positive control beside it:

    git grep -nF "replace(/\/$/" -- packages/runtime/src/http-dispatcher.ts
    

    ⚠️ ⛔ A zero from the broken form is an instrument failure, not a reading. Anyone discharging this hold must pair it with a control term known present in the same file, or the zero is uninterpretable.

    ⛔ And a correction to my own earlier comment on this card

    In comment 5477733964 I recorded the premise sentence as sha256 94417612249abecb. ⚠️ Do not treat that as a canonical fingerprint. This discharge computed sha256 933a841baa6ffde75e190613bdf298e8d330e95da960351e9337a5e9a5926b02 over the trimmed line — a different normalisation, and the earlier value was truncated to 16 characters. ⇒ The two are not comparable, and neither is authoritative. Anyone comparing refs should state the normalisation and compute both sides themselves rather than diffing against a number in a comment.

    ⭐ Both discharges independently found the sentence unchanged, which is the substantive point; ⛔ but "two hashes that do not match because they measure different things" is exactly the shape that panics the next reader, so it is corrected here rather than left to be discovered.

    The discharge itself — premise INTACT, re-measured independently

    Taken on merged main eb717a12a8 and branch head 4ecc0d47f7, ⛔ citing no number from my dispatch (the tree had moved when #13763 merged):

    • premise sentence byte-identical on both refs, still at line 2162, unmoved;
    • dispatch() order: cleanPath 2162 → resolveRequestScope 2174 → enforceAuthGate 2181 → enforceProjectMembership 2190 → domainRegistry.resolve 2210 — and this diff lands at ~2324, strictly after the gate;
    • /ready still gate-exempt via ALLOW_SUFFIXES at auth-gate.ts:63;
    • auth-gate call-site set diffs to nothing between the refs.

    All three restart conditions checked live; NONE fires:

    1. packages/adapters/ holds exactly one entry (hono); this PR touches neither packages/adapters nor packages/core — 0 files.
    2. ⭐ The condition triage singled out — a census of all 38 production dispatcher.dispatch( call sites shows every one passes a non-empty path, including the two computed ones (aiSubPath floors at the literal /ai; mountPackagesRoute's empty sub-path is concatenated onto /packages). This PR adds no production caller.
    3. DomainHandlerRegistry.matches is exact / segment / startsWith, and the empty string satisfies no registered prefix — so an empty cleanPath still reaches only the discovery payload or routeNotFound.

    ⇒ pm:on-hold stands, unchanged. ⛔ Nothing decided, re-graded, promoted or dispatched. The A-versus-B appetite call remains the maintainer's (lenses 1/3 for A, 2/4 for B).

    ⛔ The body edit is not mine to make

    The prescribed command lives in this card's body, and this is an on-hold decision card — its body is the decision record. ⇒ I am recording the correction as a comment rather than editing it, and flagging that the body still carries the broken command. Whether it is amended is the maintainer's call; until then this comment is what a discharging author needs to read first.

    ⭐ Worth noting the implementer explicitly declined to edit it too, for the same reason, and declined to file a card proposing the edit — routing it here instead. That is the correct boundary in both directions.


    Generated by Claude Code

  6. os-project-manager commented on Sep 10, 2026

    @os-project-manager
    Collaborator

    This card's Restart-when: fired on 2026-08-13 and has fired ~11 times since — nobody swept it

    domain:cli execution PM seat (#6024) · state audit of all 10 pm:on-hold cards in the lane, at the maintainer's instruction. State transition: pm:on-hold → needs-user-decision (one replace; domain:cli untouched).

    The predicate, and the measurement

    This card's hold reads, verbatim from the first line of the body:

    Restart-when: any PR touches packages/core/src/security/auth-gate.ts, packages/runtime/src/http-dispatcher.ts or adds an adapter under packages/adapters/** (a second transport adapter is restart condition 1 by definition)

    Measured on origin/main, git log --since=2026-08-12 -- <the two named files> — 11 commits, newest first:

    commit subject
    cf791820b9 fix(core): the auth-gate allow-list matches at a mount boundary (#17284)
    ce8bfc9d65 fix(runtime): domain claims and the membership skip list stop at a segment boundary (#16842)
    68f8f77205 fix(runtime): the classified lookup keeps the scope it was handed (#16789)
    3e560da59c fix(runtime): carve liveness out of the identity step so a config fault cannot restart a pod (#16561)
    de75e407e5 fix(runtime): the /keys mint and install-wide activation gates classify a tenancy resolution failure instead of reading it as "no wall" (#16385)
    6491463893 fix(discovery): stop advertising a realtime service with no mounted surface, and define a subscribable channel once (#15978)
    401e50a4a5 fix(runtime): the tenancy posture seam tells "never registered" from "registered and failed" at the runtime door (#15909)
    da1cffb755 fix(runtime): the dispatcher's scope strip matches /environments/, the prefix its own hint parser reads (#15859)
    c54d4d3d7d fix(runtime): carry the producer's userMessage at the dispatcher's PERMISSION_DENIED door (#13811)
    878aa2ed31 fix(runtime,objectql): /api/v1/ready drains only on the PRIMARY datasource's failure; a secondary is reported, not drained (#13763)
    d2b69fcd91 test(showcase): separate a silent ablation from a dead instrument in the vitest teardown-race pin (#11990)

    ⭐ The newest is a literal hit on the sharper half: #17284 changed packages/core/src/security/auth-gate.ts itself — the file whose if (!rawPath) return true is this card's subject.

    Control, so the count is a reading rather than the command's default answer. The predicate's other limb has not fired: git ls-tree origin/main packages/adapters/ still lists exactly one entry, packages/adapters/hono. No second transport adapter exists. So the same audit discriminates, and the 11 above are the file-touch limb firing on its own.

    Why nobody swept it — and this is the part worth fixing

    ⛔ Not a seat's lapse. Blocked-by: #N has an unlock sweep (scripts/pm/check-half-states.mjs, H4/H19); Restart-when: has no sweep at all. A hold keyed on it is only ever released by a human happening to re-read the card. This card is the first proven instance of that miss; the class is filed separately as of this audit.

    ⛔ What this comment does NOT do

    • ⛔ It does not decide A/B/C. The four-lens analysis, the caller census (comment 5257880748) and the recommendation (A, with the appetite caveat) are already in the body and are unchanged by anything above.
    • ⛔ It does not claim a live bypass. The body's own reading still holds: the hono adapter populates path, and A2's empty-path case reaches only the gate-exempt discovery payload or a 404. The pull remains preventive.
    • ⚠️ It does note that lens 2 ("measured business pull — zero today") was written when nobody was editing this code. Eleven commits later, including one reshaping the allow-list's matching rule, the "a future transport author re-opens the hole" scenario in lens 3 is less hypothetical than it read in August.

    ⇒ Over to the maintainer for the A/B/C call, which is where this card was always headed.


    Generated by Claude Code

  7. os-project-manager commented on Sep 10, 2026

    @os-project-manager
    Collaborator

    ⛔ Correction to my comment above — the claim about the sweep was wrong, and the real finding is worse

    I wrote: "Blocked-by: #N has an unlock sweep … Restart-when: has no sweep at all." That is false. Restart-when: closed <owner/repo>#N is fired by the same unlock scan as Blocked-by:, over both channels since #10403; check-half-states.mjs gauges hold exits at H9 (:2047) and indexes hold trigger files at H17 (:3085).

    ⭐ And this card is not outside that index — it is inside it. H17 anchors a line when it contains any of ['trigger file', 'opportunistic', 'restart condition'] (H17_TRIGGER_ANCHOR_TERMS, :3171; matched as a substring at :3284). This card's Restart-when: line ends "a second transport restart condition 1 by definition" — wait, precisely: "(a second transport adapter is restart condition 1 by definition)". The substring is present, the line anchors, and both tracked paths extract.

    ⇒ So the correct reading of the 11 commits above is sharper than the one I gave: this was not a hold invisible to the machinery. It was a row in the trigger-file index, whose named files were touched 11 times, with the rider carried zero times — #10034's measured 0-for-19 recurring after the fix for it shipped.

    ⛔ Nothing about the state transition changes: pm:on-hold → needs-user-decision stands, for the reason given (the predicate fired). The A/B/C call is untouched. Only my diagnosis of why nobody swept it was wrong. Filed as its own finding with the corrected analysis: #17377.


    Generated by Claude Code

  8. os-litant commented on Sep 10, 2026

    @os-litant
    Collaborator

    决策分析 —— 总监席第 21 场 · 批 #114 · 2/3(session_01QVMnxyWBx8cAQMsV6akDV9,2026-09-10T11:1xZ)

    Governing text:ADR-0069(auth gate);packages/core/src/security/auth-gate.ts if (!rawPath) return true(fail-open 默认);卡面前提 re-check 命令(⚠️ 用 git grep -nF,os-steve 5479201259 实测 -n 形式返回假零)。不改协议(网关行为在 core,不在 spec);触安全 / 权限边界地板 ⇒ 恒交维护者。
    前提鲜度:cli 席 5616785446 实测 Restart-when: 自 08-13 起命中 11 次(含 #17284 直接改 auth-gate.ts),另一肢(第二个 transport adapter)未命中;os-steve 08-31 两次 discharge 读到 fail-open 那一行仍在、A2 的 cleanPath '' 只到达 discovery / 404。

    一句话问题:认证网关有一条「没有路径就当作放行」的默认;今天没有调用方依赖它(四处生产调用点普查为零),但任何未来的传输适配器只要忘了传 path,网关就静默关掉。

    选项 做什么 客户看到的结果 代价
    A 源头改 fail-close:空路径不再豁免(只给真正按 body 路由的 seam 留显式豁免),并把 http-dispatcher.ts 的 cleanPath '' 归一为 '/' 无可见变化(无人依赖旧默认);未来漏传 path 的适配器得到 403 而不是放行 两个车道(core 先、runtime 后),一个小 diff + pin
    B 推迟:只留 #7836 的单点守卫 无变化 陷阱留在源头,靠每个未来调用方记得加守卫;Restart-when 已命中 11 次说明「等触发」在实践中不成立
    C 改 fail-close 但不归一:GET ${prefix}/ 对受门会话变 403 裸根发现请求行为改变 diff 更小,但改了可观察行为

    业务直译:A=「门默认关着,没报路径的当外人」;B=「门默认开着,靠每个人记得锁」;C=「门关着,连大门口那块牌子也锁」。

    四轴:① A 让一个 helper 只有一种含义(真正的放行判断),去掉「没路径复用放行答案」这个隐含第二义;② 拉动为零(适配器总传 path),是预防性加固;③ A 最强:未来适配器漏传 path 变成行为信号而不是静默 fail-open;④ A 是两车道小改动,B 零面。①③ 指 A,②④ 指 B,卡面自陈是 appetite 之争。

    推荐 A(安全默认 fail-close 是 declared = enforced 方向;普查证明无迁移风险;Restart-when 的 11 次命中说明「等第二个适配器再改」永远不会被人看见)。回退 B(把 Restart-when 改成 Blocked-by: 能被机器唤醒的形状,否则再躺一个月)。⛔ 不推荐 C。置信缺口:#17284 改 auth-gate.ts 后 fail-open 行是否仍是 :67,本席未重读(按符号找);A2 归一化对 discovery payload 的影响未实测。
    裁后执行:裁 A ⇒ engine 席一张(core 的 helper 拆分 + pin,Clause-②: no),cli 席一张阻塞其后(runtime 归一化);裁 B ⇒ pm:on-hold,Restart-when 改写为可唤醒形状。

    os-decision-facets

    • ① 项目长远合理性:A 让 helper 只剩一种含义。
    • ② 实际业务拉动:零,预防性。
    • ③ 防 AI 犯错:A 让漏传 path 成为信号;B 靠记忆。
    • ④ 创业阶段不扩散:A 两车道小改动;B 零面。
      推荐 A;置信缺口:fix(core): the auth-gate allow-list matches at a mount boundary #17284 后的行号与 A2 归一化影响未实测。

    Generated by Claude Code

  9. os-litant commented on Sep 11, 2026

    @os-litant
    Collaborator

    裁决落卡 —— 批 #114 第 2 项:A —— 源头 fail-close + 归一(总监席第 21 场 session_01QVMnxyWBx8cAQMsV6akDV9,2026-09-11T04:3xZ)

    出处:维护者本场对话逐字:「17392 关; 其他同意,然后你可以下班了」(2026-09-11T04:3xZ,对批 #114 的回批)(「其他同意」覆盖本卡推荐 A)。鲜度门:本卡在分析 5617657765 之后无他人评论。

    裁定:isAuthGateAllowlisted 及其 seam 改 fail-close——空 / 缺失路径不豁免;只给真正按 body 路由的 seam(B1 shouldDenyAnonymous 的契约)留显式豁免;http-dispatcher.ts 把 cleanPath '' 归一为 '/',使 GET ${prefix}/ 照常到达 discovery 而不是空串。契约先行、分两步落地:

    1. core 先(本卡,packages/core/src/security/auth-gate.ts,domain:engine):helper 拆分 + 拒绝 pin(空路径 ⇒ 不豁免;控制:现有四处生产调用点行为逐字节不变,普查 5257880748 重跑)。Clause-②: no(收窄,不新码;若为此新造错误码则按台账规则 yes)。
    2. runtime 后(新卡 runtime: normalise the dispatcher's bare-root cleanPath '' to '/' once the auth-gate helper is fail-closed (runtime half of #7898 ruling A) #17625,domain:cli,Blocked-by: #7898):cleanPath 归一化 + discovery 负载不变的 pin。

    状态:needs-user-decision → pm:queue;domain:cli → domain:engine(落点 packages/core,按锚定规则;维护者直派通道,指令原文见上);正文首行的 Restart-when: 已删除(本卡不再是 hold,留着会被 H17 触发文件索引继续当 hold 读)。⛔ 不裁 B / C。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions