wiki: CB-401 Stage A — PeerLauncher SPI + ClaudeCodeLauncher rename

- 9-Implementation: new peer package section (PeerLauncher/PeerHandle/
  SpawnRequest/Capability); WorkerService->ClaudeCodeLauncher in component
  map, edge table, bootstrap wiring; nine->ten packages; refs main 3aa69a9.
  Capability noted as declare-only (verb-layer enforcement deferred).
- 1-Architecture: worker row notes swappable PeerLauncher adapter; core
  bus is peer-neutral.
- 8-Roadmap: Stage 4 relabeled 'Pluggable peers'; CB-401 Stage A complete
  (183 tests, main 3aa69a9); CB-402 Stage B + Stage C shown as future.

Reflects main @ 3aa69a9. Verified against source by primary.
Dai Ha
2026-07-18 15:00:06 +02:00
parent 8e5fd01ac9
commit 0a0f7d53d6
3 changed files with 53 additions and 23 deletions
+1 -1
@@ -102,7 +102,7 @@ agent traffic. This is a deliberate simplification with real payoffs:
| **`bridged` — SERVER face** | The gateway: **MCP server** (the contract every session mounts) + REST/SSE for non-Claude clients, over the **policy brain** — session tracker, subscription guard, reply rendezvous. | `bridge_send` · `bridge_reply` · `bridge_ask` · `bridge_status` · `bridge_spawn` · `bridge_list` · `bridge_stop` · `bridge_read`. |
| **`bridged` — CLIENT face** | Drives herdr: a **status-gated injector** (per-pane FIFO, delivers only when `agent_status ∈ {idle, blocked}`) over a **herdr socket client** (NDJSON, id-correlated, live event stream). | Single writer per pane → no injector-vs-injector races. |
| **herdr** | Agent multiplexer. Owns the PTYs, panes, persistence, and — crucially — **`agent_status_changed` events**. Claude sessions run here as panes. | Socket is **local-only**; young/single-dev → injector kept pluggable. |
| **Worker `claude`** | A *real* Claude Code process (inherits `CLAUDE.md`, hooks, skills, MCP), pointed at a different model. Recyclable, not immortal. | Only these carry `ANTHROPIC_BASE_URL`. |
| **Worker `claude`** | A *real* Claude Code process (inherits `CLAUDE.md`, hooks, skills, MCP), pointed at a different model. Recyclable, not immortal. | Only these carry `ANTHROPIC_BASE_URL`. Materialized by a swappable `PeerLauncher` adapter (`ClaudeCodeLauncher` today); the core bus is peer-neutral. |
| **Broker / queue** *(optional, internal)* | `bridged`-owned durability + cross-host transport, **below the gateway**. Enqueues async messages `bridged` will later inject. | Redis Streams / NATS JetStream, or an embedded queue for a single host. |
| **AgentAPI** *(fallback)* | Swappable injector behind the CLIENT face if herdr is unavailable. | Screen-stability heuristic instead of events — see [Approaches](3-Approaches). |
+22 -8
@@ -3,7 +3,7 @@
A **walking-skeleton-first** plan: Stage 1 delivers the flagship
[review scenario](7-Use-Cases#flagship--a-review-conversation-opus--gx00-reviewer) end-to-end
(thin but real — Opus gets a review back from a gx00 worker over MCP), then later stages add
reply fidelity, the subscription guard, lifecycle, async, and hardening. This re-scopes the
reply fidelity, the subscription guard, lifecycle, pluggable peers, and hardening. This re-scopes the
[Message Server](2-Message-Server#build-plan-milestones) M0–M4 milestones around the scenario,
so there's something usable after every stage.
@@ -20,8 +20,8 @@ gantt
Stage 2 — envelope + guard + reply :s2, after s1, 8d
section Scale
Stage 3 — lifecycle + discovery + fleet :s3, after s2, 10d
section Async
Stage 4 — async + durability + split :s4, after s3, 10d
section Pluggable peers
Stage 4 — PeerLauncher SPI Stage A :s4, after s3, 10d
section Harden
Stage 5 — auth · metrics · CI · systemd :s5, after s4, 8d
```
@@ -31,7 +31,7 @@ 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 — Async + split-host** | Detached + cross-host | `block:false` (detached dispatch) + idle-pane injection; internal queue (durability); split-host `Stop`-hook adapter that polls `bridged`. |
| **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. |
## Tech stack
@@ -47,7 +47,7 @@ Consolidated from [Message Server](2-Message-Server#proposed-tech-stack); the cc
| **Worker spawn** | **`ccs <profile> claude`** into a herdr pane | Profile = worker identity; no env-prefix. |
| **Subscription guard** | **`ccs env <profile>`** → resolved `base_url` host allowlist | Boundary check in ccs terms. |
| **Config** | **YAML via Jackson** + env | `bridged.yaml`: worker profiles, allowlist, bind addr, lifecycle knobs. |
| **Internal queue** *(Stage 4)* | **Redis Streams via Lettuce** (ack + visibility) | Below the gateway; in-JVM queue OK single-host. |
| **Internal queue** *(future)* | **Redis Streams via Lettuce** (ack + visibility) | Below the gateway; in-JVM queue OK single-host. |
| **Observability** | **SLF4J+Logback**; **Micrometer**→Prometheus `/metrics`; `/healthz` | Stage 5. |
| **Supervision** | systemd unit (`java -jar` or native-image), ordered after herdr | Stage 5. |
| **Testing** | **JUnit 5**; **mock UDS herdr** server; **REST contract tests per feature**; **contract test vs real herdr 0.7.0**; fake `ccs`/`claude` stubs | See [Testability](#testability--the-rest-api-is-the-contract-surface). |
@@ -116,9 +116,23 @@ Compact scope; expand into detailed tickets when a stage starts (as Stage 1 is b
|---|---|
| **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` `block:false` + `dispatch_id` · `CB-402` idle-pane injection delivery · `CB-403` internal queue (Redis Streams) · `CB-404` split-host `Stop`-hook adapter (polls `bridged`) · `CB-405` REST ingress for event-bus |
| **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 |
## CB-401 — Peer Launcher SPI (Stage 4)
Stage A has landed on main:
- ✅ **SPI extracted in-tree** (`dev.ltms.bridged.peer`): `PeerLauncher`, `PeerHandle`, `SpawnRequest`, `Capability`.
- ✅ **One adapter** — `worker.ClaudeCodeLauncher` implements `PeerLauncher`; it is the renamed/adapted `WorkerService` and still performs guard-checked spawn, orphan reap, and teardown.
- ✅ **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:** 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.
## Stage 1 — detailed tickets
**Goal:** Opus, from its own subscription session, mounts `bridged` and gets a code review
@@ -208,8 +222,8 @@ injects `ANTHROPIC_BASE_URL` there — guard-checked before the call — with no
own env untouched. Chosen over the pane + `send_text` fallback. herdr tracks each worker's Claude
**session UUID** (`agent.list`/`agent.get`), which grounds the ID contract. Teardown is
`pane.close` (no `agent.stop`).
**Built.** `AgentControl` (start/send/read/get/status/list/close) + `WorkerService` (builds env
from config, `assertWorker` **before** any herdr call) + REST `POST /workers`, `GET /agents`,
**Built.** `AgentControl` (start/send/read/get/status/list/close) + `ClaudeCodeLauncher` (builds env
from config, `assertWorker` **before** any herdr call, implements `PeerLauncher`) + REST `POST /workers`, `GET /agents`,
`DELETE /workers/{paneId}`.
**Acceptance (met).** Unit: `POST /workers` with an off-allowlist base_url → **403 and herdr is
never touched**; a good base_url → `agent.start` env carries `ANTHROPIC_BASE_URL`. Contract: a
+30 -14
@@ -3,7 +3,7 @@
> **Scope.** This is the *as-built* code map of the `bridged` module — the actual packages,
> classes, flows, and state machines in the source tree, as a companion to the design-level
> [1. Architecture](1-Architecture) and [2. Message Server](2-Message-Server). Every enum,
> constant, and route below was verified against source at commit `aa0cf81`.
> constant, and route below was verified against source at main `3aa69a9`.
`bridged` is a single-host Java 25 / Maven daemon: the sole gateway between an on-subscription
**primary** (Opus) and off-subscription **workers**, speaking to the **herdr** PTY manager
@@ -30,7 +30,7 @@ flowchart TB
msg["msg.MessageService + Rendezvous<br/>service core · per-session rendezvous"]
inject["inject.Injector + StatusPoller<br/>single-writer, status-gated delivery"]
guard["guard.SubscriptionGuard<br/>boundary enforcement"]
worksvc["worker.WorkerService<br/>spawn · reap · teardown"]
ccl["worker.ClaudeCodeLauncher<br/>first Claude adapter · implements PeerLauncher"]
herdr["herdr.AgentControl / WorkspaceControl<br/>JSON-RPC over UNIX socket"]
end
@@ -41,9 +41,9 @@ flowchart TB
worker -->|"bridge_reply / bridge_ask"| mcp
rest --> msg
mcp --> msg
mcp --> worksvc
worksvc --> guard
worksvc --> herdr
mcp --> ccl
ccl --> guard
ccl --> herdr
msg --> inject
inject --> herdr
herdr --> herdrd
@@ -56,11 +56,13 @@ flowchart TB
```
*Figure 1 — component layers. The service core (`msg`) is reached identically from either face;
delivery reaches workers only via `inject → herdr`. The subscription guard (amber) gates spawn.*
delivery reaches workers only via `inject → herdr`. Peer materialization is delegated to a
`PeerLauncher` adapter (`ClaudeCodeLauncher` today); the subscription guard (amber) lives inside
the adapter and gates spawn.*
## Package & class reference
Nine packages under `dev.ltms.bridged`. Below, per layer: the classes, their kind, and their
Ten packages under `dev.ltms.bridged`. Below, per layer: the classes, their kind, and their
role. Method signatures are abbreviated; see source for full contracts.
### `herdr` — the wire layer (11 classes)
@@ -117,6 +119,19 @@ Owns the forward rendezvous (`bridge_send` → `bridge_reply`) and the reverse r
`TIMED_OUT_WORKING`, `TIMED_OUT_QUEUED`, `BUSY`, `STALE_TURN`. `AskOutcome`: `ANSWERED`,
`NO_WAITER`, `TIMED_OUT`. `Phase`: `PENDING`, `DONE`, `FAILED`.
### `peer` — the launcher SPI (4 classes)
The seam that keeps the core peer-neutral. The bus delegates spawn/teardown to a launcher
implementation while the core owns transport, session lifecycle, and routing. The first adapter
is `worker.ClaudeCodeLauncher`; future adapters (e.g. Codex) implement the same SPI.
| Class | Kind | Role |
|---|---|---|
| `PeerLauncher` | interface | SPI for materializing a connected peer: `spawn`, `stop`, `effectiveCwd`, `parityOverlay`, `profiles`, `defaultProfile`, `list`, `reapOrphanWorkers`, `capabilities`. |
| `PeerHandle` | interface | Opaque handle returned by `spawn`. `id()` is the registry/routing key; `terminalId()` is the transport-level session id (herdr terminal UUID today). |
| `SpawnRequest` | record | Spawn parameters: `profileName`, `requestedCwd`, `callerCwd`. A null/blank profile means "use the default"; a null/blank cwd means "inherit from config or caller". |
| `Capability` | enum | Declared launcher capabilities: `MID_TURN_ASK`, `SELF_PR`, `WORKTREE`, `ORPHAN_REAP`. Stage A only *declares* them (advisory); verb-layer enforcement — a verb against a peer lacking a capability returning a clean "unsupported" rather than crashing — is planned, not yet wired. |
### `mcp` — the MCP north face (6 classes)
Exposes `bridged` as a Streamable-HTTP MCP endpoint and resolves caller identity from the
@@ -142,7 +157,7 @@ transport context.
| Class | Kind | Role |
|---|---|---|
| `rest.BridgedApp` | class | Javalin routes; validates bodies, maps `Outcome` → HTTP status. |
| `worker.WorkerService` | class | Guard-checked spawn, orphan-pane reaping at boot, teardown (`spawn`, `reapOrphanWorkers`, `stop`, `list`, `profiles`). |
| `worker.ClaudeCodeLauncher` | class | Guard-checked spawn, orphan-pane reaping at boot, teardown. The first-class `PeerLauncher` adapter for Claude Code over herdr (`spawn`, `reapOrphanWorkers`, `stop`, `list`, `profiles`, `capabilities`). |
| `guard.SubscriptionGuard` | class | Host-allowlist + primary-cleanliness enforcement (`assertWorker`, `assertPrimaryClean`). |
| `guard.GuardException` | class | Thrown on any subscription-boundary violation. |
| `config.BridgedConfig` | record | YAML config with defaults; single legacy worker or named `workers` map (`load`, `workerProfiles`, `defaultProfile`). |
@@ -172,8 +187,8 @@ transport context.
flowchart LR
cfg["load config<br/>+ assertPrimaryClean"] --> guard["SubscriptionGuard"]
guard --> herdr["connect UnixSocketHerdrClient<br/>→ AgentControl / WorkspaceControl"]
herdr --> wsvc["WorkerService<br/>→ reapOrphanWorkers()"]
wsvc --> rv["Rendezvous → CompletionResolver<br/>→ Injector + StatusPoller"]
herdr --> ccl["worker.ClaudeCodeLauncher<br/>behind PeerLauncher SPI<br/>→ reapOrphanWorkers()"]
ccl --> rv["Rendezvous → CompletionResolver<br/>→ Injector + StatusPoller"]
rv --> ms["MessageService"]
ms --> mcp["BridgeMcp<br/>(connection identity)"]
ms --> app["BridgedApp<br/>(start Javalin)"]
@@ -181,7 +196,8 @@ flowchart LR
```
*Figure 2 — startup wiring in `Bridged.main`. The guard asserts the primary env is clean before
anything else; `reapOrphanWorkers()` clears stale panes from a prior daemon restart.*
anything else; `PeerLauncher` is wired as an interface, with `ClaudeCodeLauncher` as the first
adapter; `reapOrphanWorkers()` clears stale panes from a prior daemon restart.*
## Flow: forward rendezvous (`bridge_send` → `bridge_reply`)
@@ -323,7 +339,7 @@ N+1's waiter (CB-116 safety).*
## Subscription boundary
The one non-negotiable invariant, enforced in code. `SubscriptionGuard.assertWorker(baseUrl)` runs
in `WorkerService.spawn()` **before any herdr call**: the worker's `ANTHROPIC_BASE_URL` host must
in `ClaudeCodeLauncher.spawn()` **before any herdr call**: the worker's `ANTHROPIC_BASE_URL` host must
be on the allowlist (Stage-1: `gx00.gw`, `ollama.ltms.dev`). `assertPrimaryClean` (called at
startup) hard-stops if the primary env carries any `ANTHROPIC_BASE_URL`. Spawn injects
`ANTHROPIC_BASE_URL`/`ANTHROPIC_MODEL`/`CLAUDE_CONFIG_DIR`/`ANTHROPIC_AUTH_TOKEN` into the
@@ -353,5 +369,5 @@ startup) hard-stops if the primary env carries any `ANTHROPIC_BASE_URL`. Spawn i
- [8. Roadmap](8-Roadmap) — stages, tickets, and the feature ⇄ endpoint ⇄ test map.
---
*Generated from a fan-out code audit (one worker per layer) and verified against source at
`aa0cf81`.*
*Generated from a fan-out code audit (one worker per layer) and verified against source at main
`3aa69a9`.*