Roadmap: Stage 5 hardening as-built; Stage 3/4 status corrected
Stage 5 (CB-501..505) is complete and documented as-built, including the
finding that drove it: bridged had exactly one security control, the loopback
bind, and any caller that was not a recognised worker pane was treated as the
PRIMARY. CB-501 inverts that default (ANONYMOUS is the fallback) and refuses to
start on a non-loopback bind under loopback-trust.
Also records that CB-505 must enforce on BOTH entry paths: BridgeMcp calls the
service layer directly and /mcp is a raw servlet that never traverses Javalin's
before filter, so the wiki's long-standing "MCP is a thin adapter over the REST
core" is not literally true at code level.
Corrections to stale rows:
- Stage 4: CB-402 landed (ded226a), was still listed as future. Flagged as
code-complete but NOT live-dogfooded — the one known-unverified item.
- Stage 3: CB-301-ext / CB-302 / CB-304 marked shipped; CB-302's "STATE.md"
framing superseded by the worker-opened-PR checkpoint that actually shipped.
- Test count 147 -> 307, and clarified that figure excludes contract tests.
+57
-11
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user