Skip to content

[Decision] Pre-auth discovery/bootstrap payloads: inside BaseResponseSchema (coordinated objectui flip) or ruled exempt with reasons — today they are neither #9389

Closed

Description

@os-steve

Split out of #9364 at triage (2026-08-17): the error-path half of that card is queued as mechanical envelope conformance; THIS card carries the half that is a genuine wire-contract ruling and must not be decided by a dev mid-PR.

The question

Three route families answer bare payloads (no success flag, fields at top level) and are read that way by SPAs before first paint / before authentication:

  • POST/GET /api/v1/runtime/config (packages/cloud-connection/src/runtime-config-plugin.ts) — objectui app-shell/src/runtime-config.ts reads body.cloudUrl / body.features / body.branding off the top level;
  • packages/plugins/plugin-hono-server/src/current-user-endpoints.ts — nine { authenticated, userId, … } bodies, read bare by the Account/Console SPAs;
  • plugin-auth's /bootstrap-status — three { hasOwner: … } bodies, same consumption shape.

scripts/check-route-envelope.mjs (third gate surface, added by #9267) counts all of them as unenveloped; the counters are pinned. Today they are neither enveloped nor ruled exempt — the state #9364 names as the one worth ending.

Options

A. Envelope them — coordinated breaking wire change: server flips + objectui SPA readers flip, with a compatibility story for consoles/runtimes on different versions (contract-first split: spec/server first, Blocked-by: objectui halves).

B. Rule them exempt — a written ruling that discovery/bootstrap endpoints read before authentication are outside BaseResponseSchema; each file gets an exempt entry with the reason in the gate. Precedent exists: hmr-routes.ts already carries exactly such an exempt-with-reason.

Four-prism block

  1. Platform long-term coherence — A maximizes envelope uniformity but buys a version-skew compatibility obligation on the least-versionable seam (pre-auth bootstrap). B draws a principled boundary ("pre-auth bootstrap surfaces are bare by design") rather than an accidental one; a written boundary with per-file reasons is coherent, an unruled gap is not. Either ends the "neither" state.
  2. Measured business pull — zero user-facing pull for A: no consumer wants these enveloped (the only readers are our SPAs, which read them bare today and would have to be changed to tolerate both shapes through a skew window). The pull is for the gap to be closed on paper, which B does at ~zero migration cost.
  3. AI-agent error-resistance — the envelope's value is highest where agent-authored apps consume responses; these three surfaces are consumed by our own shells, not by authored apps. A closed exempt list with mandatory reasons is itself a loud contract; silent non-conformance (status quo) is the only wrong answer.
  4. Startup scope discipline — A is a cross-repo breaking migration with a skew story, for surfaces only we read; B is a bounded ruling plus gate entries. Remove/declare-small beats maintain-a-migration.

Recommendation: B — rule pre-auth discovery/bootstrap payloads outside BaseResponseSchema, add exempt entries with reasons for exactly these three files (closed list; any NEW pre-auth bare surface needs its own exempt entry to pass the gate, so the boundary stays enumerated), and record the ruling where the gate's docs point. If the maintainer instead wants A, it must be contract-first split (spec/server first, objectui halves Blocked-by:) and version-skew handling named before any dispatch.

Refs: #9364 (worklist + measurements) · #9267 (gate surface) · #3843 (envelope guard founding class) · hmr-routes.ts exempt precedent.

Activity

  1. added theissue type on Aug 17, 2026
  2. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    on Aug 17, 2026
  3. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    Contributor

    Maintainer ruling recorded — maintainer, 2026-08-17, batch acceptance of the PM decision-inbox recommendations, verbatim: 「同意」. Four-dimension analysis delivered in-session; provenance: PM session session_01NYgmGheCzM6NrHZN436Cxf.

    Ruled: B — pre-auth discovery/bootstrap payloads are outside BaseResponseSchema by design. The three route families (runtime/config, current-user endpoints, bootstrap-status) get exempt entries with the written reason in scripts/check-route-envelope.mjs, following the hmr-routes.ts precedent. The boundary stays enumerated: any NEW pre-auth bare surface must carry its own exempt-with-reason entry to pass the gate. Record the ruling where the gate's docs point.

    Rationale on record: the envelope's value is where authored apps consume responses; these surfaces are read only by our own shells, and A would buy a version-skew compatibility obligation on the least-versionable seam for zero measured pull.

    Label needs-user-decision → pm:queue (the exempt-entries edit) in the same stroke.


    Generated by Claude Code

  4. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 17, 2026
  5. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    Contributor

    Claim: PM seat dispatch (post-ruling)
    Session: session_01NYgmGheCzM6NrHZN436Cxf
    Branch: claude/issue-9389-preauth-envelope-exempt
    Scope: implement ruling B as recorded above — exempt entries WITH written reasons in scripts/check-route-envelope.mjs for exactly the three pre-auth route families (runtime-config, current-user endpoints, bootstrap-status), following the hmr-routes.ts precedent; record the ruling where the gate's docs point. ⛔ No route behavior changes — the payloads stay bare; this is the gate learning the boundary, not the boundary moving. The closed-list property must hold: a NEW pre-auth bare surface still fails until it carries its own exempt-with-reason.
    Routing authority: maintainer ruling, 2026-08-17 「同意」 batch.

    Label pm:queue → pm:dispatched in the same stroke.


    Generated by Claude Code

  6. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 18, 2026
  7. self-assigned this
    on Aug 18, 2026
  8. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    Contributor
    {
      "issue": 9389,
      "status": "done",
      "branch": "claude/issue-9389-preauth-envelope-exempt",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9455",
      "premise_still_valid": true,
      "summary": "Ruling B implemented in scripts/check-route-envelope.mjs only — one file, +351/-69, no route behaviour change. The three pre-auth families move from ratchet (#9364 tracked drift) to exempt with the written reason, stating the boundary the ruling drew (read before authentication, by our own shells, before an envelope reader exists) and explicitly disclaiming the two misreadings ('legacy' / 'these are fine'). ONE DESIGN JUDGMENT FOR PM REVIEW: on this surface an exempt stays COUNTED rather than becoming a file-level waiver. auth-plugin.ts builds 49 bodies of which only 3 are ruled (the other 46 include the conformant /auth/public-config body), so a file-level exempt would stop asserting anything about the file — I measured that alternative and it passes green with an unruled 4th bare pre-auth body live (ablation 3). Counting keeps the ruling's own 'boundary stays enumerated' clause true at body granularity, not just file granularity. Supporting rules added: ratchet+exempt mutually exclusive; an exempt pinning nothing is refused; widening a ruled boundary is marked MAINTAINER-ONLY per the #8435 convention rather than offered as an author's fix. Ruling recorded in the script header — the gate names no doc location of its own (content/docs/references/ is generated; the hmr-routes.ts precedent's reasoning lives in that prose), and the PR says so. Measured correction to the card's wording: current-user-endpoints.ts is not nine '{ authenticated, userId }' bodies — six are '{ authenticated, ... }' and three are '{ apps }' from /me/apps; the reason text says that rather than inheriting the old note. The other three #9364 ratchets (adapter.ts, adapters/hono, cli serve.ts) are untouched — the ruling does not reach them.",
      "tests": "All at final commit 7a9b9ffde, git status --porcelain empty. Gate derivation run verbatim: 'node scripts/pm/dispatch-gates.mjs --tier scripts/check-route-envelope.mjs' (no path-derived tier mandate) and 'node scripts/pm/dispatch-gates.mjs scripts/check-route-envelope.mjs' -> names exactly one family, 'pnpm check:route-envelope [lint.yml]'. UNION RUN AT 7a9b9ffde: (1) check:route-envelope self-test '✓ check-route-envelope self-test passed', audit exit 0 '11 module(s) audited, 161 hand-built bodies: 5 conformant, 3 ratcheted, 3 exempt'; (2) check:ratchet-remedy-authority — THE ADDITION BEYOND THE DERIVED LIST, implicated because this diff hands the author a registry-expanding remedy: self-test OK, sweep OK and unchanged from baseline (6 marked / 3 refused / 80 excluded, control corpus set-equality both ways); this gate stays 'excluded' by the sweep's own classification so no CONTROL entry is owed; (3) check:nul-bytes OK, 6124 files, plus a targeted grep -naP control-byte self-scan of the changed file, clean. REVERSE VERIFICATION, direction predicted before running and observed as predicted in all three legs: (1) a bare pre-auth body in a NEW unlisted file -> predicted RED, observed RED exit 1 'NOT DECLARED ... a CLOSED list of files, so a new one does not inherit the ruling by resembling it'; (2) a 4th bare body inside the ruled auth-plugin.ts -> predicted RED, observed RED exit 1 'unenveloped: found 4, declared 3 ... A ruled exemption is a CLOSED list ... MAINTAINER-ONLY'; (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. Both mutated files restored; porcelain empty afterwards. Seven new self-test cases drive the extracted pure auditPluginRouteModule() so the closed-list property is asserted on the TABLE, not just the scanner: unlisted new file, second body in a ruled file (incl. the MAINTAINER-ONLY marking), exact-count pass, below-count fail, ratchet/exempt exclusivity, exemption-over-nothing, and the untouched tracked-drift diagnostic. NO package build or test was run and none is owed — the diff touches no package source, so there is no affected package and no build closure; stating that rather than reporting a suite I did not run. Deps: the gate imports only node:fs/path/url plus typescript, so no pnpm install was done — typescript was symlinked into the worktree and removed at teardown.",
      "open_questions": [
        {
          "question": "The ruling says the three families 'get exempt entries with the written reason, following the hmr-routes.ts precedent'. hmr-routes.ts is a whole-file exemption that asserts nothing; applied literally to auth-plugin.ts that would retire the audit of 46 unrelated bodies and let the next bare body land in silence (measured: ablation 3 passes green). I implemented exempt as a COUNTED classification instead — same reason string, counters still pinned and ticking down only. Confirming this reads the ruling as intended rather than exceeding it.",
          "options": [
            "A (implemented) — counted exempt: the reason plus a pinned count, so the boundary is a closed list of BODIES; a 4th bare body in a ruled file goes red until a maintainer widens the ruling",
            "B — literal file-level exempt like hmr-routes.ts: nothing asserted for the three files at all; simpler and matches the precedent's shape exactly, but the ruling's own 'boundary stays enumerated' clause then holds only against NEW FILES, not new bodies in the ruled ones"
          ],
          "recommendation": "A, because the ruling's load-bearing clause is the enumeration, and B measurably loses it inside the ruled files: with B the gate certifies auth-plugin.ts green while an unruled bare pre-auth body is live in it — the 'neither enveloped nor ruled exempt' state the card exists to end, re-entered one file at a time. A costs nothing at the wire (no route behaviour changed either way) and keeps the widening act where the ruling put it, with the maintainer."
        }
      ],
      "out_of_scope_findings": []
    }

    Notes not filed as issues, deliberately:

    • check-ratchet-remedy-authority classifies this gate as excluded even though its
      diagnostics now offer a registry-expanding remedy. I did not file this: that
      detector's header states it under-matches on purpose and treats a false positive as
      worse than a miss, and the pre-existing "declare the CURRENT counts plus a ratchet"
      text was equally excluded before this PR — so there is no measured defect, only a
      documented design property. The ⛔ MAINTAINER-ONLY marking is carried anyway because
      it is true for the reader.
    • The card's "nine { authenticated, userId, … } bodies" is imprecise (six are
      { authenticated, … }, three are { apps }). Corrected in the gate's reason text and
      entry comments rather than filed — the card is being closed by this PR.

    Generated by Claude Code


    Generated by Claude Code

  9. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    Contributor

    Carry-forward parked here before dispatch, so it cannot be lost between cards. domain:cli PM seat (#6024), session session_012WKSnqAaoqtW3QX7SSf1Vk. ⛔ Not a claim — this card stays pm:queue, unassigned, serial behind #9364 (same file, scripts/check-route-envelope.mjs).

    Release condition: PR #9456 merges. It is ACCEPTed and enqueued (d2ccb2f79); the moment it lands, this card is the next domain:cli dispatch.

    ⚠️ #9456 leaves three ratchet pointers aimed at a card that will be CLOSED, and they are yours

    Verified at line level on the #9456 branch, not inferred: after it merges, scripts/check-route-envelope.mjs still carries

    • :537 — ratchet: '#9364 (envelope /api/v1/runtime/config)'
    • :542 — ratchet: '#9364 (envelope the bare plugin-route payloads)'
    • :563 — ratchet: '#9364 (envelope the bare plugin-route payloads)'

    covering exactly the three files this card rules on — runtime-config-plugin.ts, current-user-endpoints.ts, auth-plugin.ts. #9364 was fenced away from them on purpose, so it correctly did not touch their entries; but merging it closes #9364 and leaves three live pointers naming a closed issue.

    Retargeting them is part of this card's work, alongside the ruled exempt-with-reason entries. Under the maintainer's ruling (2026-08-17, B) these three surfaces are outside BaseResponseSchema by design, so the honest end state is an exempt entry carrying the written reason — not a ratchet pointing at anything. Confirm on the merged ref rather than from this comment; line numbers in that file moved once already tonight.

    (Other #9364 mentions in the same file are past-tense explanatory comments — historical record, correct as-is. ⛔ Do not "fix" those.)

    Also inherited: a sibling card that is deliberately NOT in your scope

    #9436 — packages/adapters/hono's two bare { data } discovery bodies. Same pre-auth bare-payload fork this card rules on, but a different consumer population: SDKs and codegen read that mount's discovery, not the Console SPA. It sits outside this card's deliberately closed three-file list, so #9364 retargeted its gate entry to #9436 (Blocked-by #9389) instead of stretching your scope. ⛔ Do not absorb it — the closed list is the ruling's boundary, and widening it here would decide by accident what the ruling decided on purpose.

    Ruling recap for whoever picks this up

    Maintainer, 2026-08-17, verbatim 「同意」 (relayed by PM session session_01NYgmGheCzM6NrHZN436Cxf): option B — pre-auth discovery/bootstrap payloads are outside BaseResponseSchema by design; the three route families get exempt entries with the written reason, following the hmr-routes.ts precedent. The boundary stays enumerated: any NEW pre-auth bare surface must carry its own exempt-with-reason entry to pass the gate. Option A (envelope them) was declined — it buys a version-skew compatibility obligation on the least-versionable seam for zero measured pull.


    Generated by Claude Code

  10. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    Contributor

    ⛔ Correction by the author: my carry-forward note above was written to a card that was ALREADY CLAIMED, and I did not re-read before writing

    domain:cli PM seat (#6024), session session_012WKSnqAaoqtW3QX7SSf1Vk. Correcting within the hour, because the note as written misdescribes this card's state and could mislead whoever reads it next.

    What I asserted at 01:58:25Z: that this card "stays pm:queue, unassigned, serial behind #9364", with its release condition being PR #9456's merge.

    What was already true at 01:34:29Z — twenty-four minutes earlier: this card was claimed by PM session session_01NYgmGheCzM6NrHZN436Cxf under the maintainer direct-dispatch channel, branch claude/issue-9389-preauth-envelope-exempt, and was in flight. It landed as PR #9455 at 02:16:04Z and this card is closed completed.

    So every state claim in that note was wrong, and it was wrong at the moment I wrote it.

    The cause, stated plainly: I read this card at ~01:0xZ to settle its [Decision]-title-versus-pm:queue-label question, and then wrote to it at 01:58Z from that hour-old reading. The claim arrived in between. This lane already carries the lesson "read a card's full comment thread before writing a dispatch brief" — banked after a brief contradicted a ruling sitting in the comments. This is the same failure one step over: re-read before WRITING to a card, not only before dispatching from it. A cross-seat claim is exactly the kind of fact that appears in the gap.

    ⛔ No harm reached the work — the note was advisory, this seat never claimed the card, and the other session's dispatch and landing were unaffected. But an incorrect state assertion sitting on a closed card is the kind of thing a future seat reads as history.

    What survives from that note, re-verified on origin/main just now rather than restated:

    One consequence worth flagging to whoever owns the queue next: #9455 landing while #9456 was in flight left PR #9456 with a real merge conflict in that shared gate script. It is being resolved by re-deriving the counters on top of main's structure, ⛔ not by picking hunks — a blind resolution there could silently restore a counter or drop one of this card's exempt entries.


    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

Labels

domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions