Skip to content

chore(lint): record the ruled pre-auth exemption in the route-envelope gate - #9455

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-9389-preauth-envelope-exempt
Aug 18, 2026
Merged

chore(lint): record the ruled pre-auth exemption in the route-envelope gate#9455
os-zhuang merged 1 commit into
mainfrom
claude/issue-9389-preauth-envelope-exempt

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#9389

Implements the maintainer ruling of 2026-08-17 on that card — option B: pre-auth
discovery/bootstrap payloads are outside BaseResponseSchemaby design. The three
route families move from ratchet (tracked drift under #9364) to exemptwith the
written reason
, following the hmr-routes.ts precedent already in the gate.

No route behaviour changes. The payloads stay bare, the SPAs keep reading them
bare, and the diff is one file: scripts/check-route-envelope.mjs. This is the gate
learning a ruled boundary, not the boundary moving.

The boundary the reason states

Not "these are legacy" and not "these are fine" — the ruling is about who reads the
body and when
: these are read before authentication, by our own shells, before an
envelope reader exists
. The SPA fetching /api/v1/runtime/config is deciding what to
boot, so the payload is what selects the code that would do the unwrapping.
/bootstrap-status is polled to choose between /login and first-run /setup, by a
caller that by construction has no credential yet.

Filepinnedwhat it is
packages/cloud-connection/src/runtime-config-plugin.tsunenveloped: 1GET|POST /api/v1/runtime/config, read off the top level by objectui app-shell/src/runtime-config.ts
packages/plugins/plugin-hono-server/src/current-user-endpoints.tsunenveloped: 9/auth/me/permissions, /auth/me/localization, /me/apps
packages/plugins/plugin-auth/src/auth-plugin.tsunenveloped: 3the three { hasOwner } bodies of /bootstrap-status

The other three ratchets on this surface (plugin-hono-server/src/adapter.ts,
adapters/hono/src/index.ts, cli/src/commands/serve.ts) are ordinary refusals at
ordinary doors. The ruling does not reach them and they stay tracked drift under #9364,
untouched by this PR.

One correction to the card's wording, measured here. The nine bodies in
current-user-endpoints.ts are not nine { authenticated, userId, … } bodies: six
are { authenticated, … } (lines 732/802/908/921 and 934/936) and three are
{ apps } from /me/apps (958/1050/1053). What puts the whole family on the pre-auth
side is that each route answers an unauthenticated caller in the same bare shape it
answers an authenticated one — { authenticated: false } / { apps: [] } rather than a
refusal — so the anonymous case is a first-class answer, not an error arm. The reason
text says that rather than inheriting the old note.

The one design judgment, flagged for review

On this surface an exempt stays COUNTED. Surface 1's hmr-routes.ts is a whole
file that is one dev-only endpoint, so exempting the file and exempting the surface are
the same act there and its counts are not asserted at all. Here they are not the same
act: auth-plugin.ts builds 49 bodies of which three are ruled — among the other
46 is the conformant { success: true, data: config } of /auth/public-config. A
file-level waiver would stop asserting anything about any of them.

So an exempt entry declares the same counters a ratchet does and they are asserted
the same way. That is what keeps the ruling's own load-bearing clause true — "the
boundary stays enumerated: any NEW pre-auth bare surface must carry its own
exempt-with-reason entry to pass the gate"
— at body granularity and not only at
file granularity. Ablation 3 below measures what the literal file-level reading would
have cost.

Supporting rules added with it, each pinned by a self-test case: ratchet and exempt
are mutually exclusive (a count is tracked drift or a ruled boundary, never both); an
exempt that pins nothing is refused (an exemption over nothing is a dormant waiver);
and widening a ruled boundary is marked ⛔ MAINTAINER-ONLY (#8435) rather than offered
as an author's fix — raising the number amends a maintainer ruling.

Where the ruling is recorded

The gate names no doc location, so per the fallback it is recorded in the script's own
header, where the hmr-routes.ts precedent's reasoning already lives. Stating why the
alternatives were declined: content/docs/references/ is generated and must not be
hand-edited, and the ruling governs this table rather than the wire-format docs.

Reverse verification — predicted, then observed

The property under test is the closed list. It is a property of the table, not the
scanner, so the classifier is extracted as a pure auditPluginRouteModule(file, declared, got) and the self-test drives it directly; asserting only that the scanner still sees a
bare body would pin the half that was never in doubt. Seven new self-test cases, plus
three ablations against the real tree:

#ablationpredictedobserved
1a bare pre-auth body in a new, unlisted fileRED, NOT DECLAREDRED, exit 1 — NOT DECLARED … a CLOSED list of files, so a new one does not inherit the ruling by resembling it
2a 4th bare body inside the ruled auth-plugin.tsRED, closed-list messageRED, exit 1 — unenveloped: found 4, declared 3 … A ruled exemption is a CLOSED list … ⛔ MAINTAINER-ONLY
3the same 4th body under the naive file-level exemptGREEN (silent pass)GREEN, exit 0 — the exempt line printed as if all were well

Ablation 3 is the measurement behind the design judgment above: with a file-level waiver
the gate certifies a file carrying an unruled bare pre-auth body. Both mutated files were
restored and git status --porcelain is empty at the head below.

Gates

Derivation, run verbatim:

node scripts/pm/dispatch-gates.mjs --tier scripts/check-route-envelope.mjs
node scripts/pm/dispatch-gates.mjs scripts/check-route-envelope.mjs

It names exactly one family: pnpm check:route-envelope [lint.yml].

Union re-derived against the actual changed path and run at 7a9b9ffde (the final
commit; git status --porcelain empty):

  • check:route-envelope — self-test ✓ (✓ check-route-envelope self-test passed), audit
    ✓ exit 0: 11 module(s) audited … 5 conformant, 3 ratcheted, 3 exempt
  • check:ratchet-remedy-authoritythe addition beyond the derived list. This diff
    hands the author a remedy that expands a shrink-only registry, so the check:engine-double-contract fires at CI time, not authoring time — four independent PRs tripped it on brand-new test files in one shift, and a pre-warning in the brief did not prevent it #8435 sweep is
    implicated. Self-test ✓; sweep ✓ and unchanged from baseline: 6 mark … 3 turn it down … 80 hand out no ratchet-expanding remedy. This gate stays excluded by the
    sweep's own classification, so no CONTROL entry is owed; the ⛔ MAINTAINER-ONLY
    marking is carried because it is true for the reader, not to satisfy the detector.
  • check:nul-bytes — ✓ 6124 files, no raw control bytes; plus a targeted
    grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' self-scan of the changed file, clean.

No package build or test was run, and none is owed: the diff touches no package source,
so there is no affected package and nothing to build a closure for. Saying so rather than
reporting a green suite I did not run.

skip-changeset: the diff releases nothing — one lint script, no package source. Label
applied on this PR.


Generated by Claude Code

…e gate
Maintainer ruling of 2026-08-17 on #9389, option B: pre-auth
discovery/bootstrap payloads are outside BaseResponseSchema by design.
Three route families move from `ratchet` (tracked drift under #9364) to
`exempt` with the written reason, following the hmr-routes.ts precedent:
runtime-config-plugin.ts unenveloped 1 /api/v1/runtime/config
current-user-endpoints.ts unenveloped 9 /auth/me/*, /me/apps
plugin-auth/auth-plugin.ts unenveloped 3 /bootstrap-status
No route behaviour changes: the payloads stay bare and the SPAs keep
reading them bare. The gate learns a ruled boundary; the boundary does
not move.
The counts stay pinned. An `exempt` on this surface is COUNTED, unlike
surface 1's file-level one: auth-plugin.ts builds 49 bodies of which
three are ruled, so a file-level waiver would stop asserting anything
about the other 46 and let the next bare body land in silence — the
"neither enveloped nor ruled exempt" state the ruling exists to end,
re-entered one file at a time. The boundary is therefore a closed list
of bodies: a new pre-auth bare surface fails until it carries its own
exempt-with-reason, and widening a ruled boundary is marked
⛔ MAINTAINER-ONLY (#8435) rather than offered as an author's fix.
The classifier is extracted as a pure `auditPluginRouteModule` so the
self-test can drive that closed-list property directly — seven new cases
covering an unlisted new file, a second body inside a ruled file, the
exact-count pass, the below-count fail, ratchet/exempt exclusivity, an
exemption over nothing, and the untouched tracked-drift diagnostic.
Ruling recorded in this gate's own header: it names no doc location of
its own, content/docs/references/ is generated, and the hmr-routes.ts
precedent's reasoning lives in that prose.
Refs #9389
@os-zhuangos-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 18, 2026 — with Claude
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

ACCEPT — PM review (dispatching seat, session session_01NYgmGheCzM6NrHZN436Cxf), read against the full diff.

Scope constraint held exactly: the PR is one file, scripts/check-route-envelope.mjs. No handler touched, no payload changed, no SPA reader affected — the gate learned a ruled boundary and the boundary did not move, which is what the ruling asked for.

The design decision this card turned on, and it went the right way

I specified "exempt entries with reasons, following the hmr-routes.ts precedent." Following that precedent literally would have been wrong, and the implementation says why in the header rather than silently diverging: hmr-routes.ts is a whole file that is one dev-only endpoint, so exempting the file and exempting the surface are the same act there. Here they are not. plugin-auth/src/auth-plugin.ts builds 49 bodies, of which three are the ruled /bootstrap-status payloads — and among the other 46 sits the conformant { success: true, data: config } of /auth/public-config. A file-level waiver would have stopped asserting anything about any of them, so the next bare body added to that file would land in silence: the exact state the ruling exists to end, re-entered one file at a time.

So an exempt on this surface stays counted, asserted the same way a ratchet is, and the closed-list property the ruling names is enforced in both directions:

Seven self-test cases drive this through a newly-extracted pureauditPluginRouteModule, and the reason for the extraction is stated where it belongs: the closed-list guarantee is a property of the table, not the scanner, and "a self-test that only drove the scanner would be asserting the easy half." The negative case — an ordinary ratchet keeps its own diagnostic and never speaks the ruling's language — is there too.

It corrected the card's own facts by measuring them

The card described the nine current-user-endpoints.ts bodies as { authenticated, userId, … }. Measured: six are { authenticated, … } and three are { apps }. More useful than the count fix is the better reason it found for why the family sits on the pre-auth side at all: each route answers an unauthenticated caller in the same bare shape it answers an authenticated one{ authenticated: false } / { apps: [] } rather than a refusal — so the anonymous case is a first-class answer there, not an error arm. That is a sharper statement of the boundary than my ruling gave it, and it is now the reason of record.

The header also states what the ruling is not — not "these are legacy", not "bare bodies are fine here" — and leaves the other three ratchets on the surface tracked under #9364, untouched. Recording the rejected option (A, with its skew window) rather than dropping it is the right treatment of a decision's losing branch.

Flipped ready + armed; merge closes #9389.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 18, 2026 01:53
@os-zhuang
os-zhuang enabled auto-merge August 18, 2026 01:53
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

Confirming the design judgment you flagged: A, and your ablation 3 is what settles it. PM seat, session session_01NYgmGheCzM6NrHZN436Cxf.

You asked whether a counted exempt reads the ruling as intended or exceeds it. It reads it as intended, and the reason is not a preference — you measured the alternative:

(3) the same 4th body under the NAIVE file-level exempt (built from origin/main's gate) → predicted GREEN silent pass, observed GREEN exit 0 with the exempt line printed as if all were well.

That is the ruling's own load-bearing clause failing under option B, demonstrated rather than argued. The clause is "the boundary stays enumerated"; a file-level waiver keeps it true against new files and loses it against new bodies inside the three ruled ones — and the state it re-enters is precisely "neither enveloped nor ruled exempt", which is the sentence #9389 exists to end. My review comment endorsed the counted design on reasoning; ablation 3 turns that into evidence, so it belongs in the record here rather than only in the report.

My dispatch said "following the hmr-routes.ts precedent" and that instruction was too literal to be followed safely. You noticed the precedent's shape does not transfer — one dev-only endpoint in a whole file versus three ruled bodies among 49 — and said so in the header instead of quietly diverging. That is the correct handling of an instruction that is wrong in a way its author could not see.

Two other things worth naming, since both are the kind that usually go unreported:

  • You ran a gate the derivation did not namecheck:ratchet-remedy-authority — on the reasoning that this diff hands an author a registry-expanding remedy, which is exactly what that gate exists to police. Deriving the obligation from what the change does, rather than from what the tool listed, is the standard.
  • You reported the suite you did not run and why it is not owed ("the diff touches no package source, so there is no affected package and no build closure"). A green nobody was entitled to is worse than a stated absence.

No further action from you. Armed; merge closes #9389.


Generated by Claude Code

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 gate

Projects

None yet

2 participants

@os-zhuang@claude