LearnNewsExamplesServices
Frontmatter
id15320
titleAuthenticate Fleet HTTP ingress and bind viewer identity
stateOpen
labels
enhancementaiarchitecturesecurity
assignees[]
createdAt1:03 AM
updatedAt1:06 AM
githubUrlhttps://github.com/neomjs/neo/issues/15320
authorneo-gpt
commentsCount0
parentIssue13015
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[ ] 15270 FM cockpit per-agent mailbox pane — read-only AgentDetail tab (S1 view half, D#15249)

Authenticate Fleet HTTP ingress and bind viewer identity

Open Backlog/active-chunk-6 enhancementaiarchitecturesecurity
neo-gpt
neo-gpt commented on 1:03 AM

Context

Surface S1 from the graduated Fleet Manager design record D#15249 requires explicit operator/control-plane viewer admission for cross-inbox reads. #15269 landed the fail-closed mailbox adapter, and #15316 / PR #15312 land the read seam plus cockpit view while deliberately leaving the production source unwired.

The missing last mile is a trustworthy browser-to-Fleet request boundary. Until that boundary exists, wiring the real mailbox source would turn ambient local reachability into CAN_READ_INBOX_OF authority.

Live duplicate sweep: latest 20 open issues, 50 most recently updated open issues, broader GraphQL search, and the latest 30 all-state A2A messages checked at 2026-07-16T22:59Z. No equivalent active ticket exists. Closest precedents (#15185, #13172, #14988, #15283) own different boundaries.

The Problem

At PR #15312 head f624d5fb32:

  • fleetBridgeServer.mjs is loopback-only but unauthenticated, emits wildcard CORS, reads the request body, and dispatches without a trust context.
  • installFleetBridge.mjs sends only Content-Type.
  • dispatchFleetRequest.mjs forwards {method, params} directly to the bridge.
  • FLEET_WIRE_METHODS includes lifecycle, registry, tenant, and credential-bearing operations—not merely read-only mailbox traffic.
  • fleetMailboxMirrorAdapter.mjs correctly refuses to claim admission without a bound request identity.
  • The shipped getBootIdentity, fleetActivity, and fleetRoster JSDoc carries the same false present-tense “authenticated registryBridge” claim. Their behavior remains in scope; their prose must stop asserting a transport state that does not exist.

Loopback reachability is not authentication. Bearer possession is not viewer identity. A browser-supplied viewer id is not authority.

The Architectural Reality

  • RequestContextService.run() is the existing per-request identity propagation boundary. Its identity must originate at transport authentication, not from forwarded identity headers or request params.
  • MailboxService.listMessages() already enforces CAN_READ_INBOX_OF against the bound canonical identity. The Fleet adapter must consume that authority; it must not recreate it.
  • #15185 is reusable transport precedent, not a duplicate: its local MCP bearer proves possession only and deliberately creates no identity.
  • ADR 0019 governs any required AiConfig leaves: declarative provider-owned leaves, read at the use site; no env re-derivation, pass-along aliases, runtime mutation, or hidden defaults.
  • ADR 0026 §2.7 keeps the client-reachable Fleet restartAgent surface distinct from daemon-core L0 lifecycle authority. Authenticating the Fleet UI must not grant or imply that L0 envelope.

The Fix

Implement one atomic Fleet ingress trust boundary:

  1. The cockpit launch/bootstrap path generates one process-lifetime secret from exactly 32 cryptographically random bytes, encoded as unpadded base64url. It reaches the Fleet server and browser runtime without URL, request body, log, file, database, or durable-config persistence.
  2. Fleet binds to literal loopback and independently enforces bounded Host validation, an exact cockpit-Origin policy, and constant-time bearer verification before body parsing or dispatch.
  3. Fleet resolves the viewer independently at trusted server bootstrap, binds it to one canonical AgentIdentity, and rejects caller-authored identity headers or params. The bearer establishes possession of the local Fleet session; it never becomes the identity fact.
  4. Every accepted request executes inside RequestContextService.run({userId, username, agentIdentityNodeId, source}, callback) with that server-stamped viewer. Concurrent requests remain isolated.
  5. Only after steps 1–4 are proven, wire mailboxMirrorSource to the real MailboxService.listMessages path plus a dynamic RequestContextService.getAgentIdentityNodeId() resolver. FleetControlBridge remains dependency-injected and authors no admission fact.
  6. The one-command Fleet boot/probe path carries the same authenticated contract. An unknown, stale, or mismatched process receives a named refusal; it is never silently reused.
  7. Any new configuration surface follows ADR 0019 and remains secret-safe. Runtime code never mutates the shared AiConfig provider.

Bearer auth, viewer binding, request context, source wiring, and boot handoff remain one lane: every partial combination is either unusable or spoofable.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback / Failure Docs Evidence
Fleet HTTP listener #15185 transport precedent + this ticket Literal loopback, Host guard, exact Origin policy, process bearer before parsing Startup/refusal fails closed Server JSDoc Host/Origin/bearer matrix
Local Fleet session Cockpit bootstrap One 32-byte random unpadded-base64url bearer shared in memory only Missing/malformed/mismatched bearer → 401 Launch contract Non-observability + negative probes
Viewer identity RequestContext identity authority + #15269 Independently server-resolved canonical AgentIdentity; never browser-authored Unbound/stale identity → startup or request refusal Binding JSDoc spoof/unbound/canonicalization battery
Fleet dispatch FLEET_WIRE_METHODS Every verb runs only after transport and identity gates No dispatch and zero side effects Dispatcher JSDoc mutation/credential-verb negatives
Request scope RequestContextService.run() Server-stamped viewer per accepted request No context → no bridge call Transport JSDoc delayed concurrent A/B isolation
Mailbox mirror source #15269 + #15270 Real MailboxService read using dynamic bound viewer Remains honestly unwired/degraded Source JSDoc grant/deny/cross-subject e2e
Existing-process probe One-command Fleet boot contract Authenticated identity-aware liveness/ownership check Named mismatch; no ambient reuse Launch docs/JSDoc stale/wrong-token/wrong-viewer probes

Decision Record impact

aligned-with ADR 0019 and ADR 0026; no amendment. This consumes the existing config SSOT and request-context identity authority while preserving the Fleet UI versus daemon-core L0 envelope distinction.

Decision Record

NOT_NEEDED — the graduated S1 record and existing ADRs already fix the trust boundaries; this ticket composes them.

Acceptance Criteria

  • Fleet startup fails closed unless the listener is literal loopback, a valid process-lifetime bearer exists, and the server-side viewer resolves to one canonical AgentIdentity.
  • The launch path generates exactly 32 random bytes, encodes unpadded base64url, and supplies the one-run value to server and browser without URL, body, logger, SQLite, file, durable-config, or PR/test artifact persistence.
  • Missing, malformed, wrong, and length-mismatched bearer values return 401 before request-body parsing, method lookup, or Fleet dispatch.
  • Host validation remains independently enforced. Foreign/null Origin values are rejected; the exact cockpit Origin is echoed with Vary: Origin; wildcard CORS is removed. An absent Origin is accepted only with the valid bearer.
  • The browser client sends Authorization: Bearer … and sends no viewer-identity claim.
  • Authentication gates the complete FLEET_WIRE_METHODS surface. Negative tests prove unauthenticated lifecycle, registry, tenant, and credential-bearing calls produce zero side effects.
  • The server derives userId, username, and agentIdentityNodeId independently of request data, then wraps every admitted call in RequestContextService.run().
  • A delayed concurrent viewer A/B falsifier proves request context cannot leak or swap across requests.
  • mailboxMirrorSource is wired only after the trust boundary is active, using the dynamic bound-identity resolver and real MailboxService.listMessages.
  • End-to-end evidence covers: admitted read, named CAN_READ_INBOX_OF denial, unbound viewer, spoofed viewer input, cross-subject refusal, pagination, and an unauthenticated call that never reaches MailboxService.
  • The one-command boot/probe path proves correct-token/correct-viewer reuse and named refusal for stale, unknown, wrong-token, or wrong-viewer processes.
  • JSDoc and operator-facing docs describe the actual trust state. The false present-tense “authenticated registryBridge” claims are removed from the staged mailbox seam and the already-shipped getBootIdentity, fleetActivity, and fleetRoster verbs; no site asserts authentication before this boundary is live.
  • Any AiConfig additions satisfy ADR 0019 at the use site; tests isolate by construction and never mutate the shared provider.
  • Focused unit, integration, and Neural Link evidence covers the full browser → HTTP → RequestContext → MailboxService admission path.

Out of Scope

  • Remote/cloud Fleet hosting, multi-tenant authentication, GitHub PAT/OAuth/OIDC productization.
  • Legacy HTTP+SSE support.
  • Broad Fleet authorization redesign or changing ADR 0026 daemon-core L0 authority.
  • Mailbox mutation, reply-as-operator, archive browsing, or mark-read.
  • Visual redesign beyond the minimal authenticated client plumbing.

Avoided Traps

  • Treating loopback, Host, Origin, or bearer possession individually as viewer identity.
  • Accepting a browser-authored viewerIdentity, forwarded identity header, or fixed request param.
  • Wiring MailboxService before a request-bound identity exists.
  • Protecting only fleetMailboxMirror while leaving credential/lifecycle verbs ambient.
  • Reusing #15185’s possession token as an authorization grant.
  • Splitting the trust boundary into mergeable partial tickets whose intermediate state is unsafe.

Related

Parent Epic: #13015
Blocks: #15270
Related only: #15316 / PR #15312 (independently mergeable with the source inert)
Admission primitive: #15269
Transport precedent: #15185
Source record: D#15249

Origin Session ID: 019f6120-ef4d-79f0-83f5-43e6d27f36f5

Retrieval Hint: Fleet HTTP ingress local bearer viewer identity RequestContext CAN_READ_INBOX_OF mailboxMirrorSource Host Origin

— Euclid (@neo-gpt) · OpenAI GPT-5.6 Sol Ultra