You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Fleet transport as a pluggable provider — ssh (default), tailscale (opt-in), direct #1260
Problem — In fleet mode the client↔hub data plane rides a single launchd SSH -L forward, and #792 deferred the transport ("the tunnel remains the transport under the relay"). That one tunnel is the shared cause of #777 (a healthy hub is unattachable at ~750 ms RTT — every request ~2.5 s against a 1.5 s attach budget) and composes with #775 (hub wedges under direct fleet reconnect). It also collapses under packet loss (TCP-over-TCP), drops every stream on a network roam, and is macOS-specific (a Linux host needs systemd). Approach — Make the transport a pluggable provider behind one config knob (amicode.fleetTransport): a provider yields a base URL + health + lifecycle, and the existing hub proxy targets whatever URL it is given, so the app needs no transport-specific code. Ship three — ssh (default; the current forward refactored behind the seam, systemd as its Linux form), tailscale (opt-in; the host runs tailscale serve fronting its loopback service, the client points at the MagicDNS origin — WireGuard roaming/NAT/low-latency with no TCP-over-TCP, and the loopback bind-guard stays intact because the app still binds localhost), and direct (a reachable URL for a host already on a VPN/LAN — the operator arranges reachability; the host still binds loopback behind its own edge, no tunnel-manager lifecycle). Approaches considered — Bind the engine to the tailnet IP directly (rejected as default: trips the loopback mutation guard); keep the single SSH tunnel (rejected: the #775/#777 cause); mandate Tailscale for everyone (rejected: forces a dependency on non-mac users — the seam gives the option without the mandate); a cloud relay (deferred: extra hop + hosted dependency). Scope — in: the provider interface (base URL + health + lifecycle); the ssh provider (refactor the current launchd forward behind it + systemd form); the tailscale provider (tailscale serve host-side + MagicDNS client resolution + health); the direct provider; the amicode.fleetTransport setting (default ssh); per-provider posture/hysteresis signatures. · out: the thin-client routing itself (#792 — the data plane this pipe carries); the attach-budget fix (#777); the hub-wedge root cause (#775); credential portability (#782). Assumptions — the hub proxy's arbitrary-URL targeting is stable; tailscale serve onto a loopback service preserves the host's loopback bind (verify in the first slice); the SSH path stays the zero-dependency floor.
Acceptance Criteria
amicode.fleetTransport selects the provider; unset / ssh reproduces today's behavior with no regression for current fleet clients.
The ssh provider yields the loopback base URL + a health signal through the provider interface (the launchd forward becomes its implementation, not a special case).
The tailscale provider resolves the host's MagicDNS origin as the base URL AND the host's engine/service bind hostname remains loopback (the mutation guard never trips) — asserted together.
The direct provider targets a supplied URL with a health probe and no tunnel-manager lifecycle.
A provider with no reachable host surfaces the honest hub-down posture (feeds the existing detector), never a silent fallback to another provider.
Posture/hysteresis distinguishes a transport drop from a slow-but-healthy link, per provider.
Testing Decisions
Extend the fleet beta smoke with a provider-conformance case per provider (base URL + health + honest-down) against a stub host — reuse the stub-hub harness the PRD: Fleet thin client — local shell, remote data #792 relay test introduces rather than a new fixture.
Add a loopback-preservation assertion to the bind-host guard suite for the tailscale serve path (bind stays loopback).
Key Decisions
One knob, amicode.fleetTransport, default ssh; providers independently disableable.
tailscale serve (loopback app + tailnet proxy) is the chosen Tailscale integration, NOT a direct tailnet bind — the bind guard is the reason.
SSH is the only required provider; tailscale / direct are additive.
Data Contracts
Transport provider: resolveBaseUrl() → URL, health() → status, start()/stop(). The hub proxy consumes resolveBaseUrl(); posture consumes health(). The extension host constructs the provider from the setting; the projection/guard contract is unchanged.
Constraints & Invariants
Loopback-only bind preserved (ADR 0002/0005): every provider proxies to a host that binds loopback; the tailnet-IP bind is explicitly out.
Never-fork (ADR 0005): the transport yields a data-plane connection, never an engine.
One topology reader (ADR 0023): the provider sits under the installer; the guard assertion stays green.
No silent cross-provider fallback: a down transport is an honest posture, not a reroute.
This is a PRD-parent (like #792) — coherent as one design-of-record + one ADR + one knob, but not a single implementable unit. Slice before implementation:
seam + ssh — the provider interface + amicode.fleetTransport knob (default ssh) + refactor the launchd forward behind it. Walking skeleton; AC "no regression" is the gate. Owns the shared drop-vs-slow-link posture contract.
ssh systemd form — the Linux implementation of the same provider (may fold into the seam slice).
Important
Problem — In fleet mode the client↔hub data plane rides a single launchd SSH
-Lforward, and #792 deferred the transport ("the tunnel remains the transport under the relay"). That one tunnel is the shared cause of #777 (a healthy hub is unattachable at ~750 ms RTT — every request ~2.5 s against a 1.5 s attach budget) and composes with #775 (hub wedges under direct fleet reconnect). It also collapses under packet loss (TCP-over-TCP), drops every stream on a network roam, and is macOS-specific (a Linux host needs systemd).Approach — Make the transport a pluggable provider behind one config knob (
amicode.fleetTransport): a provider yields a base URL + health + lifecycle, and the existing hub proxy targets whatever URL it is given, so the app needs no transport-specific code. Ship three —ssh(default; the current forward refactored behind the seam, systemd as its Linux form),tailscale(opt-in; the host runstailscale servefronting its loopback service, the client points at the MagicDNS origin — WireGuard roaming/NAT/low-latency with no TCP-over-TCP, and the loopback bind-guard stays intact because the app still binds localhost), anddirect(a reachable URL for a host already on a VPN/LAN — the operator arranges reachability; the host still binds loopback behind its own edge, no tunnel-manager lifecycle).Approaches considered — Bind the engine to the tailnet IP directly (rejected as default: trips the loopback mutation guard); keep the single SSH tunnel (rejected: the #775/#777 cause); mandate Tailscale for everyone (rejected: forces a dependency on non-mac users — the seam gives the option without the mandate); a cloud relay (deferred: extra hop + hosted dependency).
Scope — in: the provider interface (base URL + health + lifecycle); the
sshprovider (refactor the current launchd forward behind it + systemd form); thetailscaleprovider (tailscale servehost-side + MagicDNS client resolution + health); thedirectprovider; theamicode.fleetTransportsetting (defaultssh); per-provider posture/hysteresis signatures. · out: the thin-client routing itself (#792 — the data plane this pipe carries); the attach-budget fix (#777); the hub-wedge root cause (#775); credential portability (#782).Assumptions — the hub proxy's arbitrary-URL targeting is stable;
tailscale serveonto a loopback service preserves the host's loopback bind (verify in the first slice); the SSH path stays the zero-dependency floor.Acceptance Criteria
amicode.fleetTransportselects the provider; unset /sshreproduces today's behavior with no regression for current fleet clients.sshprovider yields the loopback base URL + a health signal through the provider interface (the launchd forward becomes its implementation, not a special case).tailscaleprovider resolves the host's MagicDNS origin as the base URL AND the host's engine/service bind hostname remains loopback (the mutation guard never trips) — asserted together.directprovider targets a supplied URL with a health probe and no tunnel-manager lifecycle.Testing Decisions
tailscale servepath (bind stays loopback).Key Decisions
amicode.fleetTransport, defaultssh; providers independently disableable.tailscale serve(loopback app + tailnet proxy) is the chosen Tailscale integration, NOT a direct tailnet bind — the bind guard is the reason.tailscale/directare additive.Data Contracts
resolveBaseUrl() → URL,health() → status,start()/stop(). The hub proxy consumesresolveBaseUrl(); posture consumeshealth(). The extension host constructs the provider from the setting; the projection/guard contract is unchanged.Constraints & Invariants
tailscale/directprovider must reach the hub through that front door (or its successor), never around it — a bypass re-exposes the wedge hub: server wedges (100% CPU, silent HTTP death) under direct fleet reconnect — front-door proxy deployed as mitigation; root cause open #775 is still open on.Decomposition
This is a PRD-parent (like #792) — coherent as one design-of-record + one ADR + one knob, but not a single implementable unit. Slice before implementation:
ssh— the provider interface +amicode.fleetTransportknob (defaultssh) + refactor the launchd forward behind it. Walking skeleton; AC "no regression" is the gate. Owns the shared drop-vs-slow-link posture contract.sshsystemd form — the Linux implementation of the same provider (may fold into the seam slice).tailscaleprovider —tailscale serve+ MagicDNS resolution + the loopback-bind-preservation assertion.directprovider — supplied URL + health probe, no tunnel lifecycle.Slices 2–4 are additive, opt-in, independently shippable once the seam lands.
Prior Art
sshprovider's current form).Source
docs/adr/0024-fleet-thin-client-pluggable-transport.md).Notes
/amicode/*proxying, theamicode_servicere-base + churn question, WebSocket/PTY upgrade proxying, cursor-resumable SSE) are posted as a gap-analysis comment on PRD: Fleet thin client — local shell, remote data #792 — they are PRD: Fleet thin client — local shell, remote data #792's to fold in, not this issue's.