You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
Roster row: { machine_id, name, server_mode, capabilities[], sshAlias, transport, last_report, health }; health ∈ {reachable, degraded, down}; single-writer per row; schema-versioned.
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
The /amicode/*→host proxy + shouldProxyAmicodeToHost (server.ts:242-251, reserves /amicode/fleet/* local) and its relay test; the shared stub host (test/support/stub_hub.ts); the loopback mutation-guard routes (solver_mode.ts, connections.ts, bind_host.ts); the fleet projection schema + reader; the base-tier projection producer; ops/fleet-status.sh (the hardcoded device array it replaces); the accept-both data-plane mint (App-shelf: extension service serves the app bundle + proxies the engine (fork-cutover static and proxy slice) #822).
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 machineroamingorcompute, and the projection carries no roster — thedevices[]list is a hardcoded bash array inops/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.jsonon the Canonical Server. Each machine owns its own row (registry/heartbeat). Surface the fleet-wide roster from the host atGET /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/rosteris carried unchanged.fleet.jsonandServer modeare untouched.Scope — in: the capability vocabulary (known:
compute,roaming; plus free descriptive tags); the roster artifact + its schema; theGET /amicode/rosterread route + thePOST /amicode/rosterself-report write; the status job (ops/fleet-status.sh) reading the roster instead of a hardcoded list; theCONTEXT.mdCapability + Roster entries; flipping ADR 0026Statustoaccepted. · out: enroll (#1319), any UI (#1321/#1322), the fleet-peer executor,fleet.json/Server modechanges,amicissimochanges, 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/rosteris carried to clients unchanged; the host binds loopback so the write satisfies the mutation guard.Acceptance Criteria
{ machine_id, name, server_mode, capabilities[], sshAlias, transport, last_report, health }through the schema without loss, withhealth ∈ {reachable, degraded, down}.computeandroamingare recognized as known tags; an arbitrary descriptive tag is accepted and preserved verbatim.POST /amicode/rosterself-report updates only the reporting machine's row (a second machine's report does not mutate the first's row).GET /amicode/rosteron 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).ops/fleet-status.sh) derives its device list from the roster rows, not the hardcodedDEVICESarray.POST /amicode/rosteris refused; the write path calls thebind_hostloopback check like the other mutation routes.Testing Decisions
healthenum) alongside the schema suites (reuse-first).GET /amicode/rosterroute 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).solver_mode.ts/connections.ts).ops/fleet-status.shis 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
Server modeunchanged; capabilities are a separate open set.fleet.jsonis untouched (one-parser invariant, ADR 0023)./amicode/roster(proxied), NOT/amicode/fleet/roster(the reserved local honesty surface that 404s on clients).fleet.json, the row is the reconciled self-report.server_modeis a read-only mirror here (UI label "role");last_reportrenders as "last-seen".Data Contracts
{ machine_id, name, server_mode, capabilities[], sshAlias, transport, last_report, health };health ∈ {reachable, degraded, down}; single-writer per row; schema-versioned.GET /amicode/roster(read) +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 — NOT the ADR-0005 Fleet token.Constraints & Invariants
fleet.jsonparser; the guard-assert gate stays green.bind_hostcheck.Server moderemains the only serve-stance authority; a capability never changes guard/tunnel/hub behavior.Prior Art
/amicode/*→host proxy +shouldProxyAmicodeToHost(server.ts:242-251, reserves/amicode/fleet/*local) and its relay test; the shared stub host (test/support/stub_hub.ts); the loopback mutation-guard routes (solver_mode.ts,connections.ts,bind_host.ts); the fleet projection schema + reader; the base-tier projection producer;ops/fleet-status.sh(the hardcoded device array it replaces); the accept-both data-plane mint (App-shelf: extension service serves the app bundle + proxies the engine (fork-cutover static and proxy slice) #822).Source
docs/adr/0026-generalizable-machine-capabilities-and-host-owned-roster.mdonfeature/free-tier-fleet).