diff --git a/9-Implementation.md b/9-Implementation.md index fc1e7bf..9d74506 100644 --- a/9-Implementation.md +++ b/9-Implementation.md @@ -9,7 +9,7 @@ `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 -(protocol 14, herdr 0.7.0) over a Unix-domain socket. It presents two equivalent faces — a REST +(protocol 19, herdr 0.8.0 — ported in CB-521) over a Unix-domain socket. It presents two equivalent faces — a REST server (the testability seam) and an MCP server — over one shared service core. ## Component map @@ -32,7 +32,7 @@ flowchart TB msg["msg.MessageService + Rendezvous + ReplyInbox
service core · rendezvous + held-reply inbox"] inject["inject.Injector + StatusPoller
single-writer, status-gated delivery"] guard["guard.SubscriptionGuard
boundary enforcement"] - ccl["worker.ClaudeCodeLauncher
first Claude adapter · implements PeerLauncher"] + ccl["member.ClaudeCodeLauncher
first Claude adapter · implements PeerLauncher"] herdr["herdr.AgentControl / WorkspaceControl
JSON-RPC over UNIX socket"] end @@ -64,8 +64,11 @@ the adapter and gates spawn.* ## Package & class reference -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. +Fourteen packages under `dev.ltms.bridged`: `auth`, `config`, `guard`, `herdr`, `inject`, `lead`, +`mcp`, `member`, `metrics`, `msg`, `peer`, `placement`, `rest`, `session`. Below, per layer: the +classes, their kind, and their role. Method signatures are abbreviated; see source for full +contracts. The sections below do not yet cover every package — `auth`, `herdr`, `inject`, `msg`, +`peer`, `mcp` and the edge are written up; `lead`, `metrics`, `placement` and `session` are not. ### `herdr` — the wire layer (11 classes) @@ -127,11 +130,42 @@ holds a worker's terminal reply when *no* forward send is open (instead of dropp `TIMED_OUT_WORKING`, `TIMED_OUT_QUEUED`, `BUSY`, `STALE_TURN`. `AskOutcome`: `ANSWERED`, `NO_WAITER`, `TIMED_OUT`. `Phase`: `PENDING`, `DONE`, `FAILED`. +### `auth` — who is calling, and what may they do (7 classes) + +The role axis. Every request arrives on a connection, and this package turns that connection into +a `Principal` and then decides. Identity is **never** taken from a tool argument, so a caller +cannot claim to be someone else. + +| Class | Kind | Role | +|---|---|---| +| `Role` | enum | `PRIMARY` (a lead orchestrator), `WORKER` (a spawned member with its own pane), `ARCHITECT` (a spawned member bound to a configured slot). Anything unrecognised is anonymous, not a role. | +| `Principal` | record | `(role, terminal, pid, name)` plus the factories `anonymous`, `primary`, `leader`, `worker`, `architect`. Predicates: `isPrimary`, `isWorker`, `isArchitect`, `isSpawnedMember` (worker **or** architect — CB-560), `isAnonymous`, `ownsSession`. | +| `CallerResolver` | class | Connection → `Principal`. Since CB-561 the **only** public way to build one is `withLeadsAndMembers(identity, tokenMode, token, leadTerminals, memberRegistry)`; the older map- and supplier-form constructors were deleted because they produced a resolver that could never return `ARCHITECT`, which failed silently. The remaining constructors are package-private and exist for tests. | +| `Authz` | class | The one decision table, `permits(caller, action, targetSession)`. | +| `AuditLog` | class | `allowed` / `denied` / `failed` — one line per decision, so a refusal is visible rather than a mystery. | +| `MemberLifecycle` | interface | `acquired(role, profile, terminal)` / `released(terminal)`. The seam the session lifecycle calls; a no-op default keeps tests free of the registry. | +| `MemberRegistry` | class | Flattens every `fleet:` pool into slots keyed `architect:opus`, and owns the live `terminal → slot` bindings. `acquired` binds **only** when `role == ARCHITECT`, to the first free slot with a matching profile; `bind`/`unbind` are compare-safe, so a stale unbind cannot remove a replacement. | + +**The decision table** (`Authz.Action` → who): + +| Action | Permitted to | +|---|---| +| `SPAWN`, `STOP`, `DRAIN` | the primary only | +| `SEND` | the primary **or** an architect | +| `REPLY`, `ASK` | the caller that owns the target session — only ever itself | +| `READ`, `METRICS` | primary, worker, or architect | + +**Two consequences worth stating.** First, a member's role reaches the principal only through +`MemberRegistry`, and only architects bind; a developer and a reviewer are both `Role.WORKER` at +this layer, and the difference between them lives in the roster (`bridge_list`), not in the +principal. Second, `SEND` is the one action an architect gains over a worker, which is what lets +two architects talk to each other without the lead relaying every message. + ### `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. +is `member.ClaudeCodeLauncher`; future adapters (e.g. Codex) implement the same SPI. | Class | Kind | Role | |---|---|---| @@ -166,12 +200,12 @@ transport context. **CB-307 scope:** only a *terminal* `bridge_reply` with no op `bridge_ask` (interactive; the worker blocks and can't consume a late answer) and the injector's completion/failure fallbacks (they target a *captured* waiter, CB-116) are **never** queued. -### `rest` · `worker` · `guard` · `config` — the edge (6 classes) +### `rest` · `member` · `lead` · `guard` · `config` — the edge | Class | Kind | Role | |---|---|---| | `rest.BridgedApp` | class | Javalin routes; validates bodies, maps `Outcome` → HTTP status. | -| `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`). | +| `member.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`). | @@ -202,7 +236,7 @@ completion/failure fallbacks (they target a *captured* waiter, CB-116) are **nev flowchart LR cfg["load config
+ assertPrimaryClean"] --> guard["SubscriptionGuard"] guard --> herdr["connect UnixSocketHerdrClient
→ AgentControl / WorkspaceControl"] - herdr --> ccl["worker.ClaudeCodeLauncher
behind PeerLauncher SPI
→ reapOrphanWorkers()"] + herdr --> ccl["member.ClaudeCodeLauncher
behind PeerLauncher SPI
→ reapOrphanWorkers()"] ccl --> rv["Rendezvous → CompletionResolver
→ Injector + StatusPoller"] rv --> ms["MessageService"] ms --> mcp["BridgeMcp
(connection identity)"] @@ -395,7 +429,7 @@ startup) hard-stops if the primary env carries any `ANTHROPIC_BASE_URL`. Spawn i turn per session (coalesced via `openAsksBySession`). - **`mcp`** — identity resolution shells out to `lsof` once per worker-identity request; shared services own their own concurrency. -- **`worker`** — `AtomicLong` name sequence + per-process nonce avoid herdr name collisions +- **`member`** — `AtomicLong` name sequence + per-process nonce avoid herdr name collisions without locks; the orphan reaper is best-effort and never aborts startup. ## Related pages