Skip to content

Fleet Panel: setup wizard + fleet lifecycle (create/add/remove/dismantle) #354

Description

@jeonghun-jj-lee

Important

Approach

Implement the fully automated setup wizard and fleet lifecycle management. The wizard
SSHs into the target machine, validates connectivity, configures both machines
(fleet.json, guard, tunnel, canonical server), and validates end-to-end. Post-setup,
the panel supports adding machines, removing machines, dismantling the fleet, and
reconfiguring the canonical server's coordinates.


Acceptance Criteria

Create Fleet

  • "Create Fleet" button visible when standalone; launches the wizard
  • Topology choice: "This machine is the server" or "A remote machine is the server"
  • SSH target input: text input for SSH alias or user@host; validates connectivity
    with a test command (ssh <target> echo ok)
  • Pre-flight checklist (rendered live, step by step):
    • SSH reachable (green check or red X with error)
    • Opencode binary present on remote (if missing: offer to SCP it)
    • Target port available on remote (lsof check)
  • Remote configuration (over SSH):
    • Writes ~/.amico/ops/fleet/fleet.json with role: "server"
    • Installs guard script to ~/.local/bin/
    • Starts canonical server as launchd service (macOS) or systemd (Linux)
    • Generates fleet token, stores at 0600
  • Local configuration:
    • Writes fleet.json with role: "client" + canonical coordinates
    • Installs guard + tunnel plist
    • Stores fleet token locally
  • End-to-end validation: tunnel connects, health probe passes, topology graph
    updates to show two nodes with solid connection line
  • Failure handling: any step failure shows specific error + suggested fix; wizard
    does not proceed past a failed step; user can retry or cancel

Add Machine

  • "Add Machine" button in topology section (visible when fleet exists)
  • Same SSH validation + pre-flight as Create, but skips server setup
  • Configures new machine as client; node appears in graph on success

Remove Machine

  • Node popover → "Remove from Fleet" action
  • SSHs into target, writes {"role": "standalone"} to fleet.json, unloads
    tunnel/guard; node disappears from graph
  • Cannot remove the server node (shows explanation: dismantle instead)

Dismantle Fleet

  • Offered when removing the last client, or via overflow menu
  • Stops canonical server on remote, reverts all machines to standalone
  • Removes canonical from fleet.json (not just sets role to standalone) — this is
    what distinguishes "dismantled" from "disconnected"
  • Topology graph returns to single-node standalone state with "Create Fleet" button

Reconnect Fleet

  • When fleet.json has role: "standalone" BUT canonical coordinates are present
    (user went standalone but the fleet still exists), panel shows "Reconnect Fleet"
    button instead of "Create Fleet"
  • One click: validates SSH/tunnel connectivity to the stored canonical, switches role
    back to client, reinstalls tunnel, validates end-to-end
  • If validation fails: shows error with option to retry or dismantle (remove canonical)

Reconfigure

  • Edit canonical coordinates (host, port, SSH alias) from the panel
  • Validates new connection before committing the change
  • Updates fleet.json on both machines; restarts tunnel

Testing Decisions

  • Unit test: fleet_wizard.test.ts — mock SSH execution (child_process.spawn), verify
    correct command sequences for each flow (create, add, remove, dismantle)
  • Unit test: pre-flight check logic — given SSH success/failure, binary present/absent,
    port available/taken → correct checklist state
  • Integration test: full wizard flow with mocked SSH → fleet.json written correctly on
    both sides, topology message emitted to webview
  • Manual verification (HITL): test against a real remote machine before merge

Key Decisions

Decision Rationale
SSH via child_process.spawn("ssh", [...]) No external SSH library; Node.js native, works with user's existing SSH config/keys
Key-based auth only (no password prompt) Extension context cannot do interactive TTY auth; key auth is the only reliable path
Wizard is idempotent Re-running on an already-configured machine checks state, skips done steps, validates final state
One fleet at a time fleet.json declares a single canonical; reconfigure to change, dismantle to start fresh

Constraints & Invariants

  • Server survives client disconnect: the canonical server is a system service
    (launchd/systemd) with NO dependency on any client or editor being connected. Sessions
    and solves continue running when all clients close. Zero connected clients is normal.
  • SSH commands MUST use the user's SSH config (-F ~/.ssh/config or default) — never
    bypass their key/host setup
  • Wizard MUST NOT store SSH passwords or private keys — it uses whatever auth the user
    has configured
  • Fleet token generated with crypto.randomBytes(32).toString("hex"); stored at 0600
  • All remote file writes use atomic tmp+mv pattern (same as fleet_registry.ts)

Additional Acceptance Criteria: Server Independence

  • Canonical server installed as a system service (launchd on macOS, systemd on
    Linux) — NOT as a child process of any VS Code instance or editor session
  • Sessions and solves launched on the server continue running when ALL clients
    disconnect (close laptop, quit VS Code, lose network)
  • On client reconnect, the panel/dropdown reflect the current state of sessions that
    ran while disconnected (completed solves show results, running sessions show progress)
  • The server process has NO dependency on any client being connected — zero clients
    connected is a normal operating state

Source

Part of #350
Blocked by #352, #353
ADR 0007: docs/adr/0007-fleet-panel-and-profile-ui.md

Metadata

Metadata

Labels

enhancementNew feature or requesthitlNeeds human decision or review

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions