Skip to content

fleet: wire the D3 resolver into live dispatch — complete ADR 0027 H1 peer attach #1376

Closed
6 of 6 issues completed

Description

@jeonghun-jj-lee

Important

Problem — PR #1353 built and merged the peer-fleet control plane (attach/detach verbs, pointer, keeper, transport, credential, Fleet Manager Attach control, the three-way resolver — all unit-tested) but deliberately left the data-plane re-target unwired as a reviewer decision. dispatch() in server.ts still calls the binary shouldProxyAmicodeToHost; resolveAmicodeTarget (attachment_pointer.ts:203) has zero production callers. Both machines in the fleet communicate over SSH, but only as hub-and-spoke — attach flips a pointer that nothing reads, so a peer can never actually host its engine for others.

Approach — Complete the parked integration of #1342 / #1343. Wire the already-built, already-tested D3 resolver into live dispatch() and handleUpgrade() as a peer-only branch that leaves the thin-client→hub path byte-intact. Single attached upstream (the H1 pointer is singular). Ship the ADR 0027 / 0029 unification alongside (chain: 0029 authoritative, both promoted to accepted).

Approaches considered — (1) Full N-keyed upstream multiplexer — rejected: H1 pointer is singular, N-keyed is speculative H2 surface with more failure states. (2) Unify client + peer through one resolver — rejected: the resolver's empty→local fail-safe is invalid for a never-fork client (no local engine), which breaks the #1261 hub-down honesty. (3) New ADR before touching server.ts — rejected: the #1346 additive-invariants gate does not freeze server.ts (only enroll + guard); ADR 0027 §D2 already decided the resolver generalizes shouldProxyAmicodeToHost; this is completion, not a new decision.

Scope — in: resolver wiring into dispatch() + handleUpgrade(), FleetPlane generalization (single attached + keeper slots), attach/detach upstream lifecycle, peer-unreachable posture, serving advertisement on boot, real-ssh two-engine e2e (CI-gated), ADR 0027/0029 unification edits. out: N simultaneous attachments, H2 compute-federation executor/peer token, ADR 0029 surface-collapse (Fleet Manager single device list, enroll interview, skill+README rewrite — separate follow-up issue), operational cleanup (stale process restart).

Assumptions — The merged #1353 control plane (pointer, attach action, transport, credential, resolver) is correct and tested. The #1346 gate does not freeze server.ts. ADR 0027's H1 acceptance criteria (AC1–AC7) remain the bar. SSH connectivity between fleet machines is already working.

Acceptance Criteria

  • dispatch() calls resolveAmicodeTarget for an engine-armed (!client) machine in fleet mode with a resolved attachment; the client→hub branch is byte-unchanged
  • handleUpgrade() applies the same resolver decision for SSE/WebSocket
  • FleetPlane carries optional attached and keeper upstream slots alongside the existing hub
  • Attach spins up the per-attachment ssh -L forward (via the existing attachment_transport module), registers the attached HubProxy, and injects the UI client mint credential; detach tears down and clears
  • A peer writes its roster serving tag + reach coordinates (placement descriptor) + device_type on boot (AC1)
  • Attached-set-but-unreachable surfaces a named "peer-unreachable" posture and holds — no silent local session data; detach-to-local is an explicit user action
  • Empty/corrupt pointer defaults to local engine (resolver fail-safe, unchanged)
  • One real-ssh two-engine e2e test, CI-gated: two engines on two ports, ssh -L to localhost; engine/SSE traffic lands on the attached peer (distinct marker) while /amicode/roster lands on the keeper (AC2 distinct-hosts + AC3 real-transport)
  • ADR 0027 status → accepted — §D1 superseded by ADR 0029; "Amended by" block extended; banner under Decision β.1 — amico-run thin process orchestrator #1; supersession note in Consequences
  • ADR 0029 status → accepted; Supersedes: relabeled to Amends: ADR 0027 Decision #1
  • #1346 additive-invariants gate still passes (enroll + guard + one-parser unchanged)

Testing Decisions

Extend the existing amicode_service_attachment_pointer.test.ts resolver suite (already covers the D3 decision table) and fleet_client_relay.test.ts (already covers the client relay). New: the real-ssh two-engine integration test, modeled on the AC4 p95 harness from #1353 (which already ran over real ssh -N). No new test file for the resolver itself — the fast unit tests already exist.

Key Decisions

  • Dual-path dispatch. The never-fork client→hub path (shouldProxyAmicodeToHost, the #1261 hub-down 503) is preserved byte-intact. The resolver governs a new peer-only branch activated only for engine-armed, fleet-mode machines with a resolved attachment.
  • Single attached slot, not N-keyed. H1's "current attached server" pointer is singular; FleetPlane gets one optional attached upstream, not a Map. YAGNI — a singular slot doesn't block evolving to N for H2.
  • Peer-unreachable holds, never silent local. An unreachable attached peer surfaces a named posture (mirroring the hub-down honesty contract) because the local engine's DB holds different sessions — silently serving them as the peer's would be dishonest. Detach-to-local is an explicit user choice.
  • ADR unification ships alongside. The chain edit (0029 authoritative, both accepted) is a documentation change that reflects the commitment to the unified fleet model — it's a same-PR natural companion, not a code dependency.

Data Contracts

  • FleetPlane interface: adds optional attached?: HubProxy, keeper?: HubProxy, peerUnreachablePointer?: () => string | null alongside existing hub/client/writes/hubDownPointer/onNoUpstream.
  • Roster row: serving tag + device_type populated on boot (the placement descriptor surface from fleet_roster.ts:195).
  • Peer-unreachable posture: a 503 with { ok: false, error: "attached server unreachable", reason: "peer-unreachable", pointer } — same shape as the hub-down contract.

Constraints & Invariants

  • The #1346 additive-invariants gate passes: enroll verb, guard shim, and enroll test suite byte-unchanged.
  • One parser of fleet.json (ADR 0023); zero amicissimo changes.
  • Single-writer-per-DB per machine (adopt-or-spawn, unchanged).
  • The local honesty surface (/amicode/fleet/*) and the switch pointer endpoint never proxy (resolver guarantees this).
  • The peer path never writes role=client and never installs the guard.

Prior Art

  • The amicode_service reverse proxy and its hub/client routing decision (the generalization point).
  • The D3 three-way resolver and its unit tests.
  • The Peer fleet studios (ADR 0027, Horizon 1) — #1341–#1346 #1353 AC4 p95 harness (real-ssh latency test, the pattern for the e2e).
  • ADR 0027 (peer studios + substrate seams), ADR 0028 (device identity), ADR 0029 (unified fleet model).

Source

Completes #1340 (the parked H1 data-plane integration). References #1342 (multiplexing resolver) and #1343 (per-attachment transport) as the original slice decomposition — those can be closed when this ships. The ADR 0029 surface-collapse (Fleet Manager, enroll interview, skill + README rewrite) is a separate follow-up issue.

Builds on #1353 (peer control plane, merged), #1368/#1374 (device identity, merged), #1369 (fleet docs, merged), #1370/#1373 (ADR 0028 + 0029 docs, merged).

Notes

The server.ts "byte-identical" hold was self-imposed by #1353's campaign as blast-radius conservatism, flagged in the PR body's "One decision for the reviewer" section. The #1346 gate's frozen set covers only the enroll verb and guard shim — server.ts was never gate-frozen. ADR 0027 §D2 already decided the resolver generalizes shouldProxyAmicodeToHost, so wiring it completes the ADR rather than amending it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions