LearnNewsExamplesServices
Frontmatter
id16911
titleBrowser cockpit self-redeems the fleet bearer (one-command hand-off)
stateClosed
labels
enhancementaiarchitecture
assigneesneo-fable-clio
createdAtAug 10, 2026, 9:48 PM
updatedAtAug 10, 2026, 11:18 PM
githubUrlhttps://github.com/neomjs/neo/issues/16911
authorneo-fable-clio
commentsCount0
parentIssue16694
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 10, 2026, 11:18 PM
milestonev13.2

Browser cockpit self-redeems the fleet bearer (one-command hand-off)

Closed Backlog/active-chunk-15 enhancementaiarchitecture
neo-fable-clio
neo-fable-clio commented on Aug 10, 2026, 9:48 PM

Context

#16694 (operator friction→gold, HIGH) delivered transport self-supply for the Electron shell via #16708 / PR #16696: npm start spawns its own fleet child and the renderer receives the bearer through the shell IPC seam. The browser one-command flow (npm run cockpit, buildScripts/devCockpit.mjs) composes both processes and hands the launcher-generated bearer to the fleet child via env — but the page still cannot receive it without an agent: apps/agentos/app.mjs:47-51 documents that only "the Electron main process, the Neural Link, or a test init-script" may place globalThis.AgentOS.fleet.bearerToken before app start. The 2026-08-09 live roster runs (receipts on #16694) each required a Neural-Link wireFleetBridge injection — an agent in the loop of a flow whose whole point is zero coordination.

The sunset runbook on #16694 (comment IC_kwDODSospM8AAAABN-PPsA) names this exact gap as finish-line A: "a one-time loopback handshake endpoint the launcher serves and the page fetches" — never a URL param, never persisted.

The Problem

After npm run cockpit, the cockpit page boots fail-closed (installFleetBridge with bearerToken: null → every call rejects locally) even though a healthy authenticated transport is listening one port away with a bearer generated by the very launcher that opened the page. A human cannot hand-carry the 43-char secret (the #16694 premise); an agent injection is coordination by another name. The one-command flow therefore cannot deliver the live roster to an operator without a maintainer agent present.

The Architectural Reality

  • buildScripts/devCockpit.mjs:200-231 — the launcher already owns the bearer custody decision (generates in memory, hands to the fleet child via env, "never via URL, log, or file").
  • ai/services/fleet/fleetIngressAuth.mjs:57-99 — one pure admission guard: Host allowlist → exact-Origin policy (absent Origin admissible because the bearer still gates) → constant-time bearer check. Preflights admitted on Host+Origin alone.
  • ai/services/fleet/fleetBridgeServer.mjs:114-171 — routes after admission: GET /fleet/probe (bearer-gated), POST /fleet. Exact-origin CORS echo with Vary: Origin already implemented.
  • apps/agentos/app.mjs:38-77onStart is sync and reads the designed pre-boot slot globalThis.AgentOS.fleet.bearerToken; src/worker/App.mjs:691 calls module.onStart() after importApp — a dynamic import, so top-level await in app.mjs completes before onStart runs: a pre-boot async redemption can fill the designed slot without touching the boot contract.
  • apps/agentos/fleet/installFleetBridge.mjs — bearer format gate (FLEET_BEARER_PATTERN), refuses credential-shaped URL params; the slot consumer stays unchanged.
  • ai/configBase.mjs:272-350 — the fleet config subtree (ADR 0019 SSOT); cockpitOrigins is the existing exact-origin authority the handshake reuses.

The Fix

  1. Config leaf (ai/configBase.mjs, fleet subtree): bearerHandshake: leaf(false, 'NEO_FLEET_BEARER_HANDSHAKE', 'boolean') — opt-in, default off, so a standalone npm run ai:fleet-server exposes zero new surface. Boolean leaf, not plane-anchored → no planeMember decision required.
  2. Admission (fleetIngressAuth.mjs): guard option bearerHandshake; when armed, GET /fleet/handshake admits on Host + present-and-allowlisted Origin (stricter than preflight: absent Origin REFUSED — this endpoint serves browsers only; the boot probe never calls it), returning {admitted: true, handshake: true, corsOrigin} before the bearer check it exists to bootstrap. Guard stays pure/stateless.
  3. Route (fleetBridgeServer.mjs): on decision.handshake, respond 200 {ok: true, result: {bearerToken}} with the exact-origin echo, Vary: Origin, Cache-Control: no-store, Connection: close; log the redemption (origin + timestamp — identity facts, never the bearer). Unarmed servers 404 the path (fail-closed default unchanged).
  4. Arming (devCockpit.mjs): set NEO_FLEET_BEARER_HANDSHAKE: '1' in the fleet child env alongside NEO_FLEET_BEARER — arming is the launcher's custody decision, exactly where the bearer already lives. Reuse path (endpointStatus: 'fleet'): protocol identity is COMPATIBILITY, never adoption authority — reuse requires the AUTHENTICATED probeExistingFleetServer proof ("same token, same viewer", driven by a pinned NEO_FLEET_BEARER + the resolved identity claim); an incumbent this launcher cannot verify is REFUSED with the remediation named, before any credential-redeeming page opens. (Corrected per PR review cycle 1 — the original sentence claimed honest arming inheritance while silently adopting an unverified incumbent's credential and server-bound viewer; disclosure in the PR #16912 response comment.)
  5. Redemption module (NEW apps/agentos/fleet/redeemFleetBearerHandshake.mjs, sibling of installFleetBridge.mjs, worker-realm, Neo-free): derive <fleet-origin>/fleet/handshake from the resolved fleet URL, bounded GET (AbortSignal.timeout), validate the bearer format, return token or null — fail-closed, never throws, response only ever held in memory.
  6. Pre-boot fill (app.mjs): module-level top-level await — browser mode only, only when the designed slot is empty — places a successful redemption into globalThis.AgentOS.fleet.bearerToken; onStart stays byte-identical in behavior.
  7. Cockpit launch entry (.claude/launch.json): add a cockpit configuration so the in-app browser pane can front the one-command flow.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
fleet.bearerHandshake leaf ADR 0019 (ai/configBase.mjs fleet subtree) opt-in boolean, env NEO_FLEET_BEARER_HANDSHAKE default false = today's surface exactly leaf JSDoc unit spec
GET /fleet/handshake fleetIngressAuth guard + fleetBridgeServer route armed: bearer to exact-allowlisted browser Origins; unarmed: 404 fail-closed 403/404 module JSDoc threat-delta section fleetBridgeServer.spec.mjs + fleetIngressAuth cases
redeemFleetBearerHandshake() NEW worker-realm module token or null, bounded, never throws null → existing fail-closed boot module JSDoc new spec (mirror tree)
globalThis.AgentOS.fleet.bearerToken existing designed slot (app.mjs:47-51) one more launch path fills it pre-boot unchanged comment update names the handshake path installFleetBridge.spec.mjs untouched (slot contract identical)
devCockpit child env buildScripts/devCockpit.mjs launch contract + NEO_FLEET_BEARER_HANDSHAKE: '1' file JSDoc devCockpit.spec.mjs

The custody decision, named

While armed, any local process able to forge an Origin header on a loopback GET can redeem the bearer — armed dev-mode custody collapses browser-caller auth to exact-Origin (browsers cannot forge Origin; non-browser local processes can). Before: a local attacker needed process-memory/env access. This is a deliberate, opt-in, dev-only widening: the browser one-command topology is the transitional Option-B surface (app.mjs:66-68), the packaged Electron product path keeps strict custody, and the default-off leaf keeps every existing deployment at today's posture. Redemptions stay observable in the transport log. Multi-redemption while armed is chosen over one-shot deliberately: a page reload (F5) is a normal cockpit operation and must not strand the operator on a fail-closed page.

Decision Record impact

aligned-with ADR 0019 (declarative leaf; entrypoint reads at use site and injects at the named startFleetBridgeServer bootstrap boundary — the existing bearerToken/allowedOrigins shape). No ADR challenged.

Acceptance Criteria

  • Unarmed (default): GET /fleet/handshake is 404; guard/server specs prove no new admitted surface.
  • Armed + allowlisted Origin: redemption returns the process bearer with exact-origin echo + no-store; armed + absent/foreign/null Origin: refused (403), no CORS grant.
  • redeemFleetBearerHandshake spec: success, refusal, timeout, malformed-body, malformed-token → null on every non-success path.
  • devCockpit spec: spawned fleet child env carries the arm flag; plan/probe semantics unchanged.
  • Live receipt on the operator machine: npm run cockpit (plane env exported) → cockpit renders the live roster with presence bands, zero agent involvement in the bearer hand-off — posted to #16694 as its remaining-AC receipt.
  • Scoped unit run green + fleet vocabulary parity lint green.

Out of Scope

  • Electron/shell topology (delivered, #16708) and the packaged-product profile.
  • The composed plane fleet-server (C2 landed the wire contract; its admission is PAT-based, not this bearer).
  • Plane env ergonomics (NEO_FLEET_PLANE_BASE/NEO_FLEET_PLANE_BEARER are one-process deployment declarations, not two-process coordination — the #16694 defect class this leaf closes).
  • Any bearer-prompt UX.

Avoided Traps

  • Bearer via URL param / persisted file — forbidden by the launch contract (installFleetBridge refuses credential-shaped params; history/log persistence).
  • Webpack dev-server middleware or proxy serving the secret — widens custody into the webpack process and couples generic build substrate to fleet secrets.
  • One-shot redemption — strands every page reload; fail-closed-after-F5 punishes the normal operator action while stopping only the attacker class (forged-Origin local) that trivially wins the race anyway.
  • Arming by default — silently changes the standalone fleet server's threat posture; the flag rides the launcher, where bearer custody already lives.
  • onStart made async / bridge re-install dancesrc/worker/App.mjs:691 deletes Neo.bootingWindowId synchronously after onStart(); top-level await in the app module is the seam that needs no framework change.

Related

#16694 (umbrella, remaining-AC receipt) · #16708 (Electron sibling, delivered) · #16743 (C2 wire contract, closed) · #16787 (presence axis, merged 55219f40d8) · #14560 (Lane B epic).

Live latest-open sweep: latest 20 open issues checked at 2026-08-10T19:44Z — no equivalent (nearest: #16834 banner vocabulary, #16824 scoped-empty roster; both different surfaces). A2A sweep (last 15, all read-states): no overlapping claim.

Origin Session ID: ff94e740-acb8-4f25-a94b-b614bdd91ea1

Retrieval Hint: "browser cockpit bearer handshake redemption exact-origin armed devCockpit"

tobiu referenced in commit 08f96bf - "feat: the cockpit page redeems the fleet bearer itself (#16911) (#16912) on Aug 10, 2026, 11:18 PM
tobiu closed this issue on Aug 10, 2026, 11:18 PM