9-Implementation: document the auth package (the role axis)

The role axis shipped in CB-548/CB-560/CB-561 but landed nowhere on this
page: Role, Principal, CallerResolver, Authz, AuditLog, MemberLifecycle
and MemberRegistry were all undocumented, and so was the decision table
that says an architect may send and a worker may not.

Also fix three stale facts found while writing it: the launcher package
is member, not worker; the daemon speaks herdr protocol 19 (0.8.0), not
14 (0.7.0); and there are fourteen packages, not ten. The package list
now says plainly which ones this page still does not cover.
Dai Ha
2026-08-15 04:43:44 +02:00
parent 05124a2c71
commit 7ceaad9b3c
+43 -9
@@ -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<br/>service core · rendezvous + held-reply inbox"]
inject["inject.Injector + StatusPoller<br/>single-writer, status-gated delivery"]
guard["guard.SubscriptionGuard<br/>boundary enforcement"]
ccl["worker.ClaudeCodeLauncher<br/>first Claude adapter · implements PeerLauncher"]
ccl["member.ClaudeCodeLauncher<br/>first Claude adapter · implements PeerLauncher"]
herdr["herdr.AgentControl / WorkspaceControl<br/>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<br/>+ assertPrimaryClean"] --> guard["SubscriptionGuard"]
guard --> herdr["connect UnixSocketHerdrClient<br/>→ AgentControl / WorkspaceControl"]
herdr --> ccl["worker.ClaudeCodeLauncher<br/>behind PeerLauncher SPI<br/>→ reapOrphanWorkers()"]
herdr --> ccl["member.ClaudeCodeLauncher<br/>behind PeerLauncher SPI<br/>→ reapOrphanWorkers()"]
ccl --> rv["Rendezvous → CompletionResolver<br/>→ Injector + StatusPoller"]
rv --> ms["MessageService"]
ms --> mcp["BridgeMcp<br/>(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