Skip to content

Fleet setup, generalizable machine roles & the Fleet Manager #1316

Closed
4 of 5 issues completed
Closed

Description

@jeonghun-jj-lee

Important

Problem — Setting up an Amicode fleet is hand-work: joining means hand-authoring fleet.json + running the installer; there is no enroll flow (the "Fleet — Enroll" the installer/guard reference does not exist). A machine's role is a closed serve-stance union (standalone/server/client) with no way to say "this laptop roams" or "this box is for compute." And there is nowhere to see or manage the fleet — the sidebar section is a "Coming soon" placeholder, and management is scattered across an editor panel + four Command Palette commands. On feature/free-tier-fleet the plumbing (pluggable transport, durable hub, posture, /amicode/*→host proxy) is landed; the setup UX, the generalizable role model, and the management surface are not.
Approach — Five spine-first vertical slices on the landed plumbing. (1) A generalizable capability model — keep Server mode as the serve-stance, add an orthogonal open capability-tag set, recorded in a new host-owned roster served at /amicode/roster. (2) An idempotent, verify-attach enroll primitive (amico fleet enroll) that fills the dangling Enroll. (3) A /create-a-fleet agentic orchestrator that drives the primitive across machines — discover → confirm → verify 2-way → guide-and-resume on gaps. (4) A read-only sidebar section (devices + health). (5) A Fleet Manager Work Column tab consolidating all management.
Approaches considered — Widen the Server mode enum (rejected: conflates stance with capability, breaks the closed union the guard + amicissimo + 3 consumers depend on). Capabilities in fleet.json (rejected: forces a cross-repo amicissimo parser change). Capabilities in VS Code settings (rejected: no cross-fleet roster). A roster route under /amicode/fleet/* (rejected: that prefix is the client's local honesty surface the #1262 proxy refuses to proxy — it would 404 on clients; the roster lives on the proxied /amicode/roster). Whole-fleet orchestration as a dumb script (rejected: cannot debug a broken reverse link — the agentic loop is the robustness). Fleet Manager as an editor panel / second sidebar view (rejected: less discoverable / fights the slim-sidebar intent).
Scope — in: the capability model; host-owned roster + GET/POST /amicode/roster carried to clients by the landed /amicode/* proxy; amico fleet enroll + amicode.fleet.enroll; the /create-a-fleet skill; the slimmed read-only sidebar section; the Fleet Manager tab absorbing devices/roles/enroll, this-machine mode+transport+go-standalone+repair, hub service, versions; CONTEXT.md + ADR 0026 (flip to accepted). out: a fleet-peer solve executor (compute stays inert); changing Server mode's union or the fleet.json contract; amicissimo parser changes; changing the /amicode/* proxy itself; full remote amicode bootstrap from bare machines (the orchestrator guides that gap).
Assumptions — the landed transport providers, durable hub service, /amicode/*→host proxy (#1262, which proxies every /amicode/* path EXCEPT the reserved /amicode/fleet/*), posture state, and version-skew primitive on feature/free-tier-fleet are the foundation; the Fleet token + join-token pattern reuse existing credential machinery; each target has amicode installed or the human is guided to install it.

Acceptance Criteria

Slice 1 (#1318) — capability model + roster

  • A roster row carries server_mode, an open capabilities[], name, sshAlias, transport, last_report, health (with health ∈ {reachable, degraded, down}); each machine writes only its own row.
  • compute and roaming are recognized known tags; an arbitrary descriptive tag round-trips without loss.
  • The host serves the fleet-wide roster at GET /amicode/roster; a client obtains the byte-identical roster through the landed /amicode/*→host proxy (asserted against the stub host).
  • The reachability status job (ops/fleet-status.sh) derives its device list from the roster rows, not a hardcoded array.
  • An unauthenticated self-report write (POST /amicode/roster) is refused; an authenticated one updates only the reporting machine's row.

Slice 2 (#1319) — enroll primitive

  • amico fleet enroll --as-server provisions the durable hub service, mints the Fleet token, and emits a join token bundling canonical coords + token + transport hint + pin version.
  • Redeeming a join token writes fleet.json (role + canonical only), registers the roster row via POST /amicode/roster, sets the transport (defaulting to tailscale when the machine is tagged roaming), runs the installer, and verifies attach before reporting success.
  • A pin mismatch is rejected at enroll (via the reused versionSkewVerdict against a host-version probe); a failed verify-attach reports the specific cause + fix and does NOT report success; a re-run repairs rather than duplicates.
  • The verb emits the structured enroll-result JSON (see Data Contracts) consumable by the orchestrator.

Slice 3 (#1320) — /create-a-fleet orchestrator

  • A read-only discovery helper enumerates candidate machines from tailnet peers, ssh config, and the roster; the skill asks which to include with per-machine role + capabilities.
  • Each remote enroll is surfaced and confirmed per machine (no silent fan-out); a machine is never reported enrolled until its enroll JSON shows verify-attach passed.
  • A missing 2-way link or absent amicode pauses that machine with an exact guided step and resumes after, without aborting the rest of the fleet.
  • On completion the skill reports fleet-wide state: each client attached, hub healthy, roster complete.

Slice 4 (#1321) — sidebar fleet section (read-only)

  • The sidebar fleet section renders a read-only per-device roster (its server_mode shown as "role" + capability chips, health, last_report shown as "last-seen") + this machine's posture badge + a Manage affordance; it replaces the placeholder and drives no mutations.
  • It reads the roster from GET /amicode/roster; when the roster is empty/unreachable it shows an honest empty/degraded state, not a spinner or fabricated list.

Slice 5 (#1322) — Fleet Manager Work Column tab

  • The fleetManager tab appears in both render paths with Devices / This machine / Hub service / Versions sections; the standalone Fleet & Versions panel is retired and its command routes here.
  • Devices reads GET /amicode/roster; an inline capabilities edit persists to the local machine's own row (a compute chip renders an honest "not yet wired" affordance; server_mode/role changes route through enroll/Go-Standalone, not the chip editor).
  • This-machine controls (transport selector writing amicode.fleetTransport, Go Standalone, Repair) and Hub-service restart invoke the existing registered commands, which remain in the Command Palette.
  • The Enroll action launches /create-a-fleet when present and shows an honest not-yet-available state when absent (a degradation-render test covers the absent case).

Testing Decisions

Reuse the branch's stub-host harness, adding a GET /amicode/roster route to it, for the roster-proxy + verify-attach tests. Extend the fleet guard/never-fork suites to prove enroll-on-client installs the guard. The enroll verb gets its own suite (a new enroll verb file, distinct from the amico fleet session-registry). Orchestrator: a dry-run test of the discovery helper + a simulated missing-link / absent-amicode case asserting guide-and-resume + per-machine confirm; amico skills check covers only the SKILL.md frontmatter. Sidebar: sidebar_view/webview render + read-only (no-mutation) + empty-state assertions. Tab: presence/close/reorder mirror the pulseInspector state tests; content tests target a pure roster row-shaper (not a full component render); the panel-retirement + Enroll-degradation each get a test; token conformance is checked in the design-system review (there is no check:design gate).

Key Decisions

  • Two axes: Server mode (serve-stance, unchanged closed union) + an orthogonal open capability set.
  • The roster is a new amicode-owned artifact on the Canonical Server; fleet.json is untouched (one-parser invariant, ADR 0023).
  • The roster is surfaced at GET/POST /amicode/roster — deliberately OUTSIDE /amicode/fleet/* (the reserved local honesty surface the Fleet thin client — Slice 2: proxy /amicode/* to the host (host owns all state) #1262 proxy refuses to proxy) — so the existing proxy carries it unchanged.
  • /create-a-fleet is an agentic orchestrator over a robust, independently-runnable per-machine enroll primitive.
  • Fleet Manager is a Work Column tab (a global exception alongside Home) that consolidates all management; existing commands stay registered and are invoked by the tab.

Data Contracts

  • Roster row — { machine_id, name, server_mode, capabilities[], sshAlias, transport, last_report, health }; health ∈ {reachable, degraded, down}; single-writer per row (the owning machine, or the orchestrator acting for it); schema-versioned.
  • Roster routes — GET /amicode/roster (read the fleet-wide roster) and POST /amicode/roster (self-report the caller's own row). Both ride the authenticated /amicode/ data plane (accept-both service/engine mint, App-shelf: extension service serves the app bundle + proxies the engine (fork-cutover static and proxy slice) #822) to the loopback-bound host; the write calls the bind_host loopback check like other mutation routes. NOT the ADR-0005 Fleet token (that guards SSH enrollment).
  • Join token — { canonical{host,port,sshAlias}, fleet_token, transport_hint, pin_version }; a secret, handled at 0600, never logged.
  • Enroll-result JSON — { machine_id, name, server_mode, capabilities[], transport, verify_attach: { ok, cause? } }; the single shape Fleet enroll primitive (amico fleet enroll + join token) #1319 emits and /create-a-fleet orchestrator skill #1320 consumes.

Constraints & Invariants

  • One topology reader (ADR 0023): the roster is not a second fleet.json parser; the guard-assert gate stays green.
  • Never-fork (ADR 0005): enroll on a client installs the guard; no client spawns an engine.
  • Loopback bind + mutation refusal (ADR 0002/0005): the roster write rides the authed data plane (accept-both mint) to a loopback host and calls the bind_host check.
  • No silent fallback (ADR 0024/0025): verify-attach and the orchestrator surface honest failures; transport never silently reroutes.
  • Window-mode is a separate axis from link-health posture (ADR 0025 β.4 — One-lab provisioning slice (bundle + install + healthcheck) #4): the tab's window-mode control writes its own field.
  • Multi-machine mutation is surfaced + confirmed per machine.

Prior Art

  • The landed pluggable transport provider seam + its three providers; the durable hub service unit; the /amicode/*→host proxy + its shouldProxyAmicodeToHost rule (server.ts:250 reserves /amicode/fleet/* as local); the versionSkewVerdict primitive; the posture state feed; the stub-host harness + the /amicode/* proxy relay test.
  • The fleet topology reader + health report; the fleet fallback writer; the base-tier projection producer; ops/fleet-status.sh (the hardcoded device array).
  • The sidebar section renderer + bridge; the Work Column tab machinery (the Pulse Inspector precedent); the existing Fleet & Versions panel; the four fleet commands.
  • The existing fleet diagnosis skill; the installer + guard + launchd tunnel; amico skills check.

Source

Notes

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