Skip to content

Fleet transport as a pluggable provider — ssh (default), tailscale (opt-in), direct #1260

Description

@jeonghun-jj-lee

Important

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

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:

  • 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).
  • tailscale provider — tailscale serve + MagicDNS resolution + the loopback-bind-preservation assertion.
  • direct provider — supplied URL + health probe, no tunnel lifecycle.
    Slices 2–4 are additive, opt-in, independently shippable once the seam lands.

Prior Art

  • The thin-client relay and its stub-hub test (PRD: Fleet thin client — local shell, remote data #792).
  • The hub proxy's arbitrary-URL targeting + credential translation; the fleet activation seam.
  • The bind-host loopback allowlist and its mutation-refusal guard.
  • The posture detector and poll-hysteresis modules.
  • The launchd tunnel agent + the fleet installer (the ssh provider's current form).

Source

Notes

Activity

  1. added 19 commits that reference this issue on Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    hitlNeeds human review before merge

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions