Repository navigation
fleet: wire the D3 resolver into live dispatch — complete ADR 0027 H1 peer attach #1376
Description
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()callsresolveAmicodeTargetfor 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 -
FleetPlanecarries optionalattachedandkeeperupstream slots alongside the existinghub - Attach spins up the per-attachment
ssh -Lforward (via the existingattachment_transportmodule), registers theattachedHubProxy, and injects the UI client mint credential; detach tears down and clears - A peer writes its roster
servingtag + reach coordinates (placement descriptor) +device_typeon 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 -Lto localhost; engine/SSE traffic lands on the attached peer (distinct marker) while/amicode/rosterlands 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 toAmends: ADR 0027 Decision #1 -
#1346additive-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#1261hub-down503) 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;
FleetPlanegets one optionalattachedupstream, not aMap. 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
FleetPlaneinterface: adds optionalattached?: HubProxy,keeper?: HubProxy,peerUnreachablePointer?: () => string | nullalongside existinghub/client/writes/hubDownPointer/onNoUpstream.- Roster row:
servingtag +device_typepopulated on boot (the placement descriptor surface fromfleet_roster.ts:195). - Peer-unreachable posture: a
503with{ ok: false, error: "attached server unreachable", reason: "peer-unreachable", pointer }— same shape as the hub-down contract.
Constraints & Invariants
- The
#1346additive-invariants gate passes: enroll verb, guard shim, and enroll test suite byte-unchanged. - One parser of
fleet.json(ADR 0023); zeroamicissimochanges. - 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=clientand never installs the guard.
Prior Art
- The
amicode_servicereverse 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.
Activity
docs(fleet): unify ADR 0027/0029 — chain, promote to accepted (#1377)
docs(fleet): unify ADR 0027/0029 — chain, promote to accepted (#1377)