Skip to content

Fleet capability model + host-owned roster #1318

Description

@jeonghun-jj-lee

Important

Problem — The fleet has no generalizable role axis and no fleet-wide device roster. A machine's role is only its serve-stance (Server mode); there is no way to tag a machine roaming or compute, and the projection carries no roster — the devices[] list is a hardcoded bash array in ops/fleet-status.sh. Without this spine, nothing downstream (enroll, the orchestrator, the UI) has a roster to write or read.
Approach — Add an orthogonal, open capability-tag set per machine and a host-owned roster.json on the Canonical Server. Each machine owns its own row (registry/heartbeat). Surface the fleet-wide roster from the host at GET /amicode/roster (read) + POST /amicode/roster (self-report write) — deliberately OUTSIDE /amicode/fleet/*, which is the client's local honesty surface the #1262 proxy refuses to proxy; every other /amicode/* path IS proxied to the host, so /amicode/roster is carried unchanged. fleet.json and Server mode are untouched.
Scope — in: the capability vocabulary (known: compute, roaming; plus free descriptive tags); the roster artifact + its schema; the GET /amicode/roster read route + the POST /amicode/roster self-report write; the status job (ops/fleet-status.sh) reading the roster instead of a hardcoded list; the CONTEXT.md Capability + Roster entries; flipping ADR 0026 Status to accepted. · out: enroll (#1319), any UI (#1321/#1322), the fleet-peer executor, fleet.json/Server mode changes, amicissimo changes, changing the /amicode/* proxy.
Assumptions — the landed /amicode/*→host proxy (#1262) proxies every /amicode/* path EXCEPT the reserved /amicode/fleet/* (verified: shouldProxyAmicodeToHost, server.ts:250-251), so a route at /amicode/roster is carried to clients unchanged; the host binds loopback so the write satisfies the mutation guard.

Acceptance Criteria

  • A roster row round-trips { machine_id, name, server_mode, capabilities[], sshAlias, transport, last_report, health } through the schema without loss, with health ∈ {reachable, degraded, down}.
  • compute and roaming are recognized as known tags; an arbitrary descriptive tag is accepted and preserved verbatim.
  • A POST /amicode/roster self-report updates only the reporting machine's row (a second machine's report does not mutate the first's row).
  • GET /amicode/roster on the host returns the fleet-wide roster; a client reaching it through the /amicode/*→host proxy receives the byte-identical roster (asserted against the stub host).
  • The reachability status job (ops/fleet-status.sh) derives its device list from the roster rows, not the hardcoded DEVICES array.
  • An unauthenticated (no data-plane credential) POST /amicode/roster is refused; the write path calls the bind_host loopback check like the other mutation routes.

Testing Decisions

  • Roster schema: a round-trip + reject-malformed test (incl. the health enum) alongside the schema suites (reuse-first).
  • Proxy surfacing: add a GET /amicode/roster route to the shared stub host (test/support/stub_hub.ts), then assert it proxies byte-identically in client mode (the /amicode/* proxy relay test is the pattern).
  • Single-writer + auth: a test that a self-report touches only its own row, and that a credential-less write is refused (mirrors the loopback-mutation-guard route tests, e.g. solver_mode.ts/connections.ts).
  • Status job: ops/fleet-status.sh is a shell script — assert via a dry-run/shellcheck-level test that it reads roster rows (lighter than a unit test; the hardcoded array is removed).

Key Decisions

  • Two axes: Server mode unchanged; capabilities are a separate open set.
  • The roster is amicode-owned on the Canonical Server; fleet.json is untouched (one-parser invariant, ADR 0023).
  • Roster surfaced at /amicode/roster (proxied), NOT /amicode/fleet/roster (the reserved local honesty surface that 404s on clients).
  • Each machine owns its row; the authoritative per-machine role stays its own fleet.json, the row is the reconciled self-report. server_mode is a read-only mirror here (UI label "role"); last_report renders as "last-seen".

Data Contracts

Constraints & Invariants

  • One topology reader (ADR 0023): the roster is not a second fleet.json parser; the guard-assert gate stays green.
  • Loopback bind + mutation refusal (ADR 0002/0005): the self-report write rides the authed data plane to a loopback host and calls the bind_host check.
  • Server mode remains the only serve-stance authority; a capability never changes guard/tunnel/hub behavior.

Prior Art

Source

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions