Repository navigation
Description
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 opencapabilities[],name,sshAlias,transport,last_report,health(withhealth ∈ {reachable, degraded, down}); each machine writes only its own row. -
computeandroamingare 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-serverprovisions 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 viaPOST /amicode/roster, sets the transport (defaulting totailscalewhen the machine is taggedroaming), runs the installer, and verifies attach before reporting success. - A pin mismatch is rejected at enroll (via the reused
versionSkewVerdictagainst 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_modeshown as "role" + capability chips,health,last_reportshown 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
fleetManagertab 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 (acomputechip 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-fleetwhen 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.jsonis 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-fleetis 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) andPOST /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 thebind_hostloopback 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 at0600, 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.jsonparser; 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_hostcheck. - 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 + itsshouldProxyAmicodeToHostrule (server.ts:250reserves/amicode/fleet/*as local); theversionSkewVerdictprimitive; 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
fleetdiagnosis skill; the installer + guard + launchd tunnel;amico skills check.
Source
- Design of record: this issue (PRD-parent) + ADR 0026 (capability model + host-owned roster) +
CONTEXT.mdupdates. - Part of the PRD: Fleet thin client — local shell, remote data #792 thin-client program; builds on Durable hub — reboot-survival for the canonical hub (blocked by #792) #1258 (durable hub), Fleet transport as a pluggable provider — ssh (default), tailscale (opt-in), direct #1260 (transport), Surface machine posture (hostname, hub reachability) in agent context #780 (posture), Fleet thin client — Slice 2: proxy /amicode/* to the host (host owns all state) #1262 (
/amicode/*proxy), ADR 0025 (host-owns-state). - Decomposes into five slice sub-issues: Fleet capability model + host-owned roster #1318 (capability model + roster) · Fleet enroll primitive (amico fleet enroll + join token) #1319 (enroll primitive) · /create-a-fleet orchestrator skill #1320 (
/create-a-fleetorchestrator) · Slimmed sidebar fleet section (read-only devices + health) #1321 (sidebar section) · Fleet Manager Work Column tab (consolidated management) #1322 (Fleet Manager tab).
Notes
- Cross-codebase seam: Fleet capability model + host-owned roster #1318/Fleet enroll primitive (amico fleet enroll + join token) #1319/Slimmed sidebar fleet section (read-only devices + health) #1321 are extension work (
packages/extension, vitest); Fleet Manager Work Column tab (consolidated management) #1322 is engine-overlay work (packages/app-bundle/overlay/packages/app, bun test, design tokens). The tab reaches the roster via the app's server SDK (amico.fetch('/amicode/roster')) and opens via the existing Work Column tab machinery; the sidebar's Manage affordance navigates to it across the same seam. - Slimmed sidebar fleet section (read-only devices + health) #1321 (sidebar) and Fleet Manager Work Column tab (consolidated management) #1322 (tab) are order-free siblings once Fleet capability model + host-owned roster #1318 lands (either order); each degrades honestly if the other is absent.
- The
compute"not yet wired" affordance is owned by Fleet Manager Work Column tab (consolidated management) #1322; theroaming→tailscaletransport default is applied by Fleet enroll primitive (amico fleet enroll + join token) #1319 (enroll) and prefilled by Fleet Manager Work Column tab (consolidated management) #1322's selector. - Non-goal for a future issue: the fleet-peer solve executor that would make
computelive (SSH-dispatch a solve to a compute-tagged machine + artifact sync-back + tier/warrant integration).
Activity
docs(adr): 0026 generalizable machine capabilities + host-owned roster
jeonghun-jj-lee commentedon Sep 20, 2026
Durable record: ADR 0026 —
docs/adr/0026-generalizable-machine-capabilities-and-host-owned-roster.md(committed tofeature/free-tier-fleet). Its Source cites this issue; this is the reverse edge.Fleet setup, generalizable machine roles & the Fleet Manager (#1316) (#…
docs(adr): 0026 generalizable machine capabilities + host-owned roster
Fleet setup, generalizable machine roles & the Fleet Manager (#1316) (#…