diff --git a/CONTEXT.md b/CONTEXT.md index 418ec45..8ad2f03 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -56,6 +56,36 @@ _Avoid_: Armonia (the whole stack), workspace, folder The scheduled agentic-work context — a registered set of jobs that run unattended on a schedule, each producing staged, reviewable output rather than direct changes. Already live as the team-facing nightly Slack automation in `harmoniqs/amico` (morning brief, per-member briefs, EOD check-in, news posts) on GitHub Actions; the unified context grows it to all scheduled agentic work, including local repo/Julia-heavy jobs. Subsumes the dream cycle: `dream-reflect` runs as one job within Notturno. One night's run of the registered jobs is "tonight's notturno". _Avoid_: cron, scheduler (as concept names), night shift +### Fleet & serving + +**Server mode**: +The per-machine stance for where Sessions are served from, in three values. `standalone` — this machine spawns and owns its own chat server (the default; the only mode that ever spawns). `server` — this machine runs the Canonical Server as a system service and the panel attaches to it. `client` — this machine never serves; the panel attaches to the Canonical Server through a Managed Tunnel. Machine-scoped, never synced: a synced "client" landing on the server would silence the fleet. +_Avoid_: profile, spawn vs attach (as concept names) + +**Canonical Server**: +The one chat server that owns the fleet's Session store — the single writer every panel attaches to. Runs as a system service on the machine in `server` Server mode, available headless (no editor required). Only one may exist per Fleet. +_Avoid_: master, primary, host + +**Fleet**: +The user's machines acting as one logical studio: exactly one Canonical Server plus zero-or-more clients, all attaching to the same Session store. +_Avoid_: mesh, cluster + +**Local fallback**: +The deliberate, user-invoked escape hatch: a `client` machine temporarily serving itself locally (a `standalone` spawn) while the Canonical Server is unreachable. Explicit about its trade-off — fleet history returns on reconnect, and Sessions made during fallback merge back into the Canonical Server on rejoin. Never silent: an active Local fallback is a first-class, visible state. +_Avoid_: offline mode, degraded mode + +**Rejoin**: +The closing half of Local fallback: on reconnect, the client ships its local Session shard to the Canonical Server, which merges it as the single writer (id-guarded inserts, strictly-newer-wins per row, event-position guard, schema-drift column mapping). After Rejoin, fleet history is whole again — nothing strands on the client. +_Avoid_: sync (bidirectional connotation), upload + +**Fleet token**: +The shared secret authenticating a client to the Canonical Server's data routes — minted when the fleet server is enabled, stored at 0600, handed to clients during the ssh-based setup flow. The sibling of the per-boot server password (ADR 0002): that guards a spawned server its extension owns; this guards the service no extension spawns. +_Avoid_: API key, password + +**Managed Tunnel**: +The self-healing SSH local-forward a `client` uses to reach the Canonical Server — one component with two launchers. The extension spawns and supervises it for interactive panels (reconnect with backoff, address candidates probed LAN-before-overlay, health surfaced in the status bar); a headless launcher (`amico fleet tunnel`) serves panel-less consumers such as scheduled jobs. Failures are always visible to its consumer — never an invisible external service. +_Avoid_: port forward (as a concept name), launchd tunnel + ### Orthogonal axes **Entitlement**: diff --git a/docs/adr/0005-managed-fleet.md b/docs/adr/0005-managed-fleet.md new file mode 100644 index 0000000..a4fc2a1 --- /dev/null +++ b/docs/adr/0005-managed-fleet.md @@ -0,0 +1,68 @@ +# Managed Fleet: one canonical server, clients attach, nothing silently forks + +Status: draft (2026-08-07) + +A user's machines form one logical studio — one canonical chat server, many panels — but +until now that topology lived entirely in ops duct tape: a hand-written ssh config, a +hand-rolled launchd tunnel agent, and an extension whose only behavior was to spawn a local +server, so the "client attaches through the tunnel" design worked *by accident* (the spawn +lost a port race and the health probe rode the tunnel). On 2026-08-07 this failed three +times in one day on one fleet: a silent three-week fork (279 sessions invisible to the +canonical store), a fork reborn during a tunnel reconnect gap, and a panel stranded dead +after a tunnel outage with no self-recovery. Each failure mode was detected by a human +running diagnostics by hand; the product saw nothing. + +This ADR makes the fleet a product feature: explicit **Server mode** (`standalone` / +`server` / `client`), the **Canonical Server** as a product-managed system service, the +**Managed Tunnel** as a self-healing extension-owned component, an identity handshake and +**Fleet token** on the server, and **Local fallback** with **Rejoin** merge as the +user-facing repair hatch. Terms per `CONTEXT.md` (Fleet & serving). + +**Why:** the deciding requirement is not elegance but repairability for an open-source +user: when the fleet breaks, the user must be able to keep working (Local fallback) and +later rejoin without losing sessions — and the product must never, under any network +condition, silently serve a forked store. Weighing the alternatives: Remote-SSH moves the +user's whole editing context to the server (rejected as the primary pattern — "chat on the +laptop about the laptop's files" breaks); ops-only hardening keeps the fragility (rejected +— we spent a day chasing it); spawning on the server machine from its editor ties fleet +liveness to an editor's lifetime (rejected — the couch scenario). The server-side merge and +identity routes follow ADR 0002's seam precedent: the fork server owns what it owns, with +CLI/headless parity. + +**Conditions of merge (the hardening grafts):** the never-fork invariant is enforced by +construction (client/server modes contain no spawn path) and pinned by tests; machine-scoped +settings (`scope: machine`) so Settings Sync can never carry `client` onto the server or leak +the token into a synced file; the merge policy is locked by golden-shard fixtures recovered +from the 2026-08-07 incident (id-guarded inserts, strictly-newer-wins per row with +`directory` excluded, event `(aggregate_id, seq)` position guard, column-name mapping for +schema drift, single FK-off transaction, refuse-and-preserve on unmappable drift); identity +handshake verified before every attach (role + fleet id), mismatch refused with actionable +copy; loopback-only binding with mutation routes refusing otherwise (ADR 0002 graft +persists); the service binary lives at a stable path with the upgrade choreography +(replace → restart → health probe) owned by the extension. + +**Flip condition:** revisit the Managed Tunnel + identity ownership if opencode upstream +gains a native remote-attach capability with identity and auth (we would adopt rather than +maintain ours); revisit the Rejoin merge toward park-local if cross-version schema drift +produces merges we cannot verify — the invariant is "never lose, never silently corrupt", +and a merge we cannot verify violates it. + +**Accepted costs:** the vendored fork's route surface grows again (identity + rejoin) — +ADR 0002's highest-conflict-on-rebase spot gets busier; the rejoin merge is the most +intricate component and its correctness budget is paid in fixtures and tests, not in review +cleverness; the service installer is per-platform (launchd now, systemd when a Linux server +appears); headless fleet consumers wait for the tunnel's second launcher. + +**Considered:** Remote-SSH pivot (zero fleet code, wrong workflow); ops-only hardening +(zero build, keeps the fragility); two modes spawn/attach (server behaviors smear into +spawn); a named Fleet object with membership (heavier than a studio needs); CLI-daemon +tunnel (invisible failures); editor-owned canonical server (fleet dies with the editor); +mDNS auto-discovery setup (LAN-only, extra surface — ssh bootstrap with manual fallback +chosen); anonymous loopback auth (SSH as the boundary — rejected per ADR 0002's threat +model; the fleet token is the per-boot password's sibling for a service no extension +spawns). + +**Prior art / source:** the 2026-08-07 incident and recovery (fixtures at +`~/.amico/fleet-recovery/2026-08-07/`); harmoniqs/amicode#279 (attach-only mode, filed from +the incident); the fleet playbook in the Amico skill set; ADR 0001 (at-rest secret +discipline), ADR 0002 (server-route seam, per-boot password, loopback graft).