diff --git a/8-Roadmap.md b/8-Roadmap.md index 6633126..dfe1c21 100644 --- a/8-Roadmap.md +++ b/8-Roadmap.md @@ -31,8 +31,8 @@ gantt | **1 — Walking skeleton** | One review, happy path | Opus mounts `bridged` (MCP), calls `bridge_send` with a diff, gets a review back from a real `ccs gx00-vllm claude` worker. Single hardcoded profile, same host, no guard/lifecycle. | | **2 — Contract + guard** | Trust the reply, trust the boundary | Structured [envelope](7-Use-Cases#mechanism-4--the-id-contract-envelope) + worker `bridge_reply`; reply rendezvous; subscription guard via `ccs env`; reviewer skill. | | **3 — Lifecycle + discovery** | Reuse, recycle, choose | Session manager (spawn/reuse/recycle Ralph loop, `idle_ttl`); `bridge_list` roster+live; multiple profiles. (`bridge_ask` landed early in Stage 2.) | -| **4 — Pluggable peers** | PeerLauncher SPI + first adapter | Extract `PeerLauncher` SPI in-tree; one adapter (`ClaudeCodeLauncher`); core depends on the interface; behaviour-preserving. Stage B (CB-402, 2nd adapter e.g. Codex) and Stage C (dynamic external plugin loading) are future work. | -| **5 — Harden** | Production shape | Auth/TLS, `/metrics` + `/healthz`, mock-socket CI, systemd unit, per-session authz + audit. | +| **4 — Pluggable peers** | PeerLauncher SPI + two adapters | ✅ Extract `PeerLauncher` SPI in-tree; `ClaudeCodeLauncher` (Stage A) **and** `OpenCodeLauncher` (Stage B, CB-402) routed by a `kind:` discriminator through `CompositePeerLauncher`. Stage C (dynamic external plugin loading) remains future work, gated by a trust/capability model. | +| **5 — Harden** | Production shape | ✅ Bearer auth + fail-fast exposure guard, `/metrics` + `/healthz`, mock-socket CI on the Gitea runner, launchd + systemd units, per-session authz + audit log. TLS deliberately terminates at a reverse proxy, not in the daemon. | ## Tech stack @@ -115,9 +115,9 @@ Compact scope; expand into detailed tickets when a stage starts (as Stage 1 is b | Stage | Tickets | |---|---| | **2** | `CB-201` envelope schema + codec · `CB-202` worker `bridge_reply` tool + reviewer skill · `CB-203` reply rendezvous (corr match; resolve on reply *or* the `working→idle` edge) · `CB-204` subscription guard via `ccs env` + allowlist · `CB-205` blocked-worker path (`bridge_ask`) | -| **3** | `CB-301` session manager (spawn/reuse/recycle) · `CB-302` Ralph checkpoint (`STATE.md` + commit) · `CB-303` `idle_ttl`/`context_cap`/drain · `CB-304` `bridge_list` roster+live · `CB-305` multi-profile routing (`role@profile`) | -| **4** | `CB-401` ✅ PeerLauncher SPI Stage A — extracted in-tree, one adapter (`ClaudeCodeLauncher`), core uses `PeerLauncher` interface, main @ `3aa69a9`, 183 tests green. `CB-402` 2nd coding-agent adapter (e.g. Codex) — Stage B, future. Stage C — dynamic external plugin loading, future, gated by trust/capability model. | -| **5** | `CB-501` bearer auth + TLS · `CB-502` `/metrics` + `/healthz` · `CB-503` mock-socket CI · `CB-504` systemd unit + ordered start · `CB-505` per-session authz + audit log | +| **3** | `CB-301` ✅ session manager (spawn/reuse/recycle) · `CB-301-ext` ✅ per-worker git worktree + config-parity overlay (`97ecc71`) · `CB-302` ✅ worker checkpoint — **shipped as commit→push→**_**worker-opened PR**_ (`64e70ef`: repo-scoped forge-token injection + the implementer skill), which **supersedes** this row's original `STATE.md`-file framing; see `docs/Worker-Git-Workflow.md` · `CB-303` ✅ `idle_ttl`/`context_cap`/drain · `CB-304` ✅ `bridge_list` roster+live (`9fe04bf`) · `CB-305` ✅ multi-profile routing | +| **4** | `CB-401` ✅ PeerLauncher SPI Stage A — extracted in-tree, one adapter (`ClaudeCodeLauncher`), core uses the `PeerLauncher` interface, main @ `3aa69a9`. `CB-402` ✅ **Stage B landed** (`ded226a`) — `OpenCodeLauncher` as the SPI-proving second adapter: shares none of Claude's private seams (no `ANTHROPIC_BASE_URL`, no `SubscriptionGuard`), mounts the bridge MCP via a generated `OPENCODE_CONFIG`, and is routed by `kind:` through `CompositePeerLauncher`. ⚠️ **Code-complete but not yet live-dogfooded** — see the note below. Stage C — dynamic external plugin loading, future, gated by trust/capability model. | +| **5** | ✅ **All landed.** `CB-501` bearer auth + non-loopback-bind fail-fast (TLS at a proxy, not in-JVM — see `docs/CB-5xx-Hardening.md` D3) · `CB-502` `/metrics` (zero-dependency Prometheus renderer, D4) + `/healthz` · `CB-503` mock-socket CI (`.gitea/workflows/ci.yml`) · `CB-504` launchd agent + systemd unit + herdr-socket startup wait · `CB-505` per-session authz table + audit log | ## CB-401 — Peer Launcher SPI (Stage 4) @@ -128,10 +128,56 @@ Stage A has landed on main: - ✅ **Core decoupled** — `session.SessionManager` now depends on the `PeerLauncher` interface and keys its registry on `PeerHandle.id()` (equal to herdr `paneId` for the Claude adapter, so no value change). - ✅ **Behaviour-preserving** — full green gate on main @ `3aa69a9`, **183 tests**. -**Future sub-stages (not started):** +**Stage B / CB-402 — landed on main (`ded226a`), with one caveat:** -- **Stage B / CB-402:** a second in-tree coding-agent adapter (e.g. Codex) to prove the SPI holds. -- **Stage C:** dynamic external plugin loading (`ServiceLoader`/jar discovery). This is gated by a trust/capability model — a launcher runs at daemon privilege and can inject env/tokens into peers, so third-party plugins are not enabled without that model. +- ✅ `HerdrPeerLauncher` base extracted; `OpenCodeLauncher` implements the three divergent hooks. +- ✅ `kind:` discriminator on worker profiles; `CompositePeerLauncher` routes spawn/stop/reap/list by kind. +- ✅ The SPI is proven provider-neutral: opencode uses **none** of Claude Code's private launch seams. +- ⚠️ **Not live-dogfooded.** Every prior ticket (CB-306/307/108) was gated on a live run; this one + merged with the dogfood deferred ("needs a running-daemon restart + a resolved Gemini provider"). + `opencode` is not installed on the dev host and the provider question + (CB-402 §7 Q1) is still open. **This is the one known-unverified item carried into the + cross-host stage** — gitea issue #7 stays open until the §5 checklist runs. + +**Stage C (future):** dynamic external plugin loading (`ServiceLoader`/jar discovery). Gated by a trust/capability model — a launcher runs at daemon privilege and can inject env/tokens into peers, so third-party plugins are not enabled without that model. + +## CB-5xx — Stage 5 hardening (as-built) + +The single-host close-out, done **before** cross-host rather than after, because CB-308's own +gating concern is the trust model and it inherits whatever identity shape lands here. Full design +and decision record: **`docs/CB-5xx-Hardening.md`**. + +The finding that shaped the stage: `bridged` had **exactly one security control — the loopback +bind**. `ConnectionIdentity` resolves a worker from its connection (unforgeable), but *any* caller +that was not a recognised worker pane — including, had the bind ever widened, an arbitrary remote +client — was treated as **the primary**, the most privileged role on the bus. CB-501 inverts that +default: `ANONYMOUS` is now the fallback and `PRIMARY` must be established. + +- **CB-501 ✅ auth.** `auth.mode: loopback-trust` (default, the historical behaviour named honestly) + or `token` (bearer required of every non-worker caller). Worker identity is *never* token-gated, + so enabling auth cannot lock the fleet out of `bridge_reply`. Constant-time token comparison. + **The highest-value line in the stage is a startup check:** a non-loopback `bind.host` under + `loopback-trust` now *refuses to start* rather than silently promoting every reachable client to + primary. TLS terminates at a reverse proxy by design (D3), not in the JVM. +- **CB-502 ✅ `/metrics`.** Zero new dependencies — a ~150-line Prometheus text renderer instead of + Micrometer (D4), because this pom already hand-reconciles Jackson 2/3 and a Jetty BOM and carries + four accepted-CVE advisories, and the project's mandated dependency CVE gate could not be run. + Instrumented at `MessageService` — the single funnel both surfaces share. +- **CB-503 ✅ CI.** `.gitea/workflows/ci.yml` on the (already-running) Gitea Actions runner. Needs + no contract-test flag: the pom's `default-excludes` profile already excludes `@Tag("contract")`, + so a plain `mvn -B clean install` *is* the mock-socket surface. +- **CB-504 ✅ supervision.** launchd agent (the real target — this host is macOS, there is no + systemd) **and** a systemd unit for the Linux gateways CB-308 adds. Ordering directives are + advisory; the actual fix is that bridged now **waits up to 30s for the herdr socket and then + serves degraded** instead of crashing into a restart loop on a boot-order race. +- **CB-505 ✅ authz + audit.** The role table enforced on **both** entry paths — and that plural is + the point. The wiki has long described MCP as "a thin adapter over the REST core"; at code level + it is not. `BridgeMcp` calls the service layer *directly*, and `/mcp` is a raw servlet on Jetty's + context handler that never passes through Javalin's `before` filter. Enforcing only at REST would + have left `/mcp` wide open. The load-bearing rule is *own-session-only*: a worker may reply or ask + only as itself (already structurally true over MCP, newly true over REST, which had simply trusted + the session id in the URL path). Audit lines are JSON to a dedicated appender and **never carry + message content**. ## Delivery reliability & multi-host (CB-306 – CB-308) @@ -213,9 +259,9 @@ comes in Stage 2). This is the thinnest end-to-end vertical slice. **Definition of done for the stage:** `CB-107` demo passes. -> **Build status — Stage 1 COMPLETE** (in `bridged/`, Maven · Java 25 · **147 tests green — 141 -> unit/acceptance + 6 live-herdr contract**; the suite has grown with the Stage 2 work noted -> below). The `CB-107` end-to-end demo gate passes and the +> **Build status — Stage 1 COMPLETE** (in `bridged/`, Maven · Java 25 · **307 unit/acceptance tests +> green** as of the CB-5xx close-out; the live-herdr and broker contract tests run separately via +> `mvn test -Pcontract`). The `CB-107` end-to-end demo gate passes and the > bridge is **dogfooded**: an Opus primary delegates real tasks to off-subscription workers that > reply through it (delegated code reviews have produced committed bug fixes). > ✅ **CB-101** herdr client — connection-per-call, contract-tested vs live 0.7.0.