CB-500 §11: resolve distributed-sandbox topology (gateway-per-host × local sandboxes)
Clarifies the open fork from §4/§9: Development A (sandbox launcher) and CB-308 (per-host federation) COMPOSE — each host runs a bridged gateway whose launcher spawns agents into that host's LOCAL sandboxes; the broker moves messages + presence, never keystrokes. The forcing fact: delivery is herdr keystroke-injection into a locally-owned PTY, so "a sandboxed agent on another host" ≡ "a sandbox spawned by that host's gateway" (a remote container with no local herdr can't be injected into). Rules out a central daemon reaching remote PTYs. Adds Figure 11 (composed topology) + Figure 12 (remote-delegation sequence: the local?inject:publish fork with a sandboxed far side — both injection points stay local, only the middle hop crosses the broker), the two forced reachability changes (host-routable mcpUrl; PTY in the local gateway's herdr), and a maps-to-existing-seams table (CB-308 gateway × Dev-A launcher, CB-117 reap, CB-303 container lifecycle, CB-308 #5 trust). No new pillars. Both diagrams mmdc-validated; §4/§9 updated to point at §11.
This commit is contained in:
@@ -160,7 +160,9 @@ CB-306 readiness gate); only the launcher's `buildLaunch` differs — exactly th
|
||||
**Deltas:** a new `kind: sandbox` adapter (extends the same `HerdrPeerLauncher`/`PeerLauncher` base);
|
||||
a spawn-target/role dimension on `SpawnRequest` and the `Worker` profile; optionally a `SANDBOX`
|
||||
`Capability`. Per-role = two profiles → two images; `CompositePeerLauncher` already routes them. If a
|
||||
sandbox is a *separate host/container*, it reuses CB-308's global id + per-host gateway wholesale.
|
||||
sandbox is a *separate host/container*, it reuses CB-308's global id + per-host gateway wholesale —
|
||||
**the distributed case is resolved in §11: a sandbox on another host is one spawned by that host's
|
||||
gateway, because herdr keystroke-injection needs a locally-owned PTY.**
|
||||
|
||||
## 5. Development B — Main-agent pairs
|
||||
|
||||
@@ -336,8 +338,9 @@ flowchart LR
|
||||
|
||||
## 9. Open questions (to resolve at ticket-split)
|
||||
|
||||
- **Sandbox mechanism:** container (`docker exec`) vs devcontainer vs a remote host (then it *is*
|
||||
CB-308). How the role→image mapping is expressed on the profile.
|
||||
- **Sandbox mechanism:** container (`docker exec`) vs devcontainer — how the role→image mapping is
|
||||
expressed on the profile. *(Topology **resolved** in §11: distributed = gateway-per-host × local
|
||||
sandboxes; the remaining choice is only the local launch mechanism, not the shape.)*
|
||||
- **Pair semantics:** are the two mains fully symmetric peers, or is one a co-primary that may also
|
||||
delegate? Affects how `PrimaryRegistry` and the "orchestration tools" identity relax.
|
||||
- **Orchestrator drivenness:** the mains become programmatically spawned/resumed — does the human
|
||||
@@ -355,3 +358,103 @@ role/spawn-target), **the CB-308 substrate** (likely already its own ticket), **
|
||||
pair), and **C** (orchestrator tier + context scoping) — with the identity clause (§7) as an
|
||||
acceptance criterion on **A** specifically. Sequence per §8; nothing here is a new pillar, so each
|
||||
ticket is an extension of an existing pattern (CB-402 for A, CB-308 for B/C).
|
||||
|
||||
## 11. Distributed sandboxes — the resolved topology
|
||||
|
||||
The follow-up question — *"clarify the architecture when we have distributed agents in sandboxes"* —
|
||||
resolves the fork left open in §4 and §9. **Decision: Development A (sandbox launcher) and CB-308
|
||||
(per-host federation) *compose*, not compete — each host runs a `bridged` gateway whose launcher
|
||||
spawns agents into that host's *local* sandboxes.** A sandbox is never reached across the network; it
|
||||
is reached by the gateway sitting next to it.
|
||||
|
||||
### 11.1 The one fact that fixes the shape
|
||||
|
||||
The bus delivers a turn by **herdr keystroke-injection** — `Injector → AgentControl.send` writes into
|
||||
a PTY that its **local** herdr owns. The broker moves *messages and presence*, **never keystrokes**.
|
||||
So an agent's PTY must live in a herdr that *some* `bridged` instance drives locally: a remote
|
||||
container with no local herdr **cannot be injected into**. That rules out a central daemon reaching
|
||||
remote PTYs, and collapses the design to a single identity:
|
||||
|
||||
> **"a sandboxed agent on another host" ≡ "a sandbox spawned by that host's gateway."**
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph hostA["HOST A — gateway"]
|
||||
mA["main / orchestrator<br/>MCP client → LOCAL gateway"]
|
||||
gA["bridged A<br/>herdr + CompositePeerLauncher<br/>(incl. SandboxLauncher)"]
|
||||
cBEa["sandbox: backend<br/>(local container)"]
|
||||
cFEa["sandbox: frontend<br/>(local container)"]
|
||||
mA --- gA
|
||||
gA -->|"spawn (docker/devcontainer)<br/>→ PTY in A's herdr"| cBEa
|
||||
gA --> cFEa
|
||||
end
|
||||
subgraph broker["BROKER (AMQP) — CB-307/308 fabric"]
|
||||
inbox["agent.ID.inbox queues"]
|
||||
roster["roster.* (federated presence)"]
|
||||
end
|
||||
subgraph hostB["HOST B — gateway"]
|
||||
gB["bridged B<br/>herdr + SandboxLauncher"]
|
||||
cBEb["sandbox: backend<br/>(local container)"]
|
||||
gB -->|"spawn → PTY in B's herdr"| cBEb
|
||||
end
|
||||
gA <-->|"messages + presence<br/>(NOT keystrokes)"| inbox
|
||||
gB <-->|"messages + presence"| inbox
|
||||
gA --- roster
|
||||
gB --- roster
|
||||
classDef line fill:#2b6cb0,stroke:#1a365d,color:#ffffff;
|
||||
class inbox,roster line
|
||||
```
|
||||
|
||||
*Figure 11 — the composed topology. Each gateway owns its local herdr and runs a `SandboxLauncher`
|
||||
(the §4 adapter) that spawns role-specific containers **on its own host**; the broker (blue) carries
|
||||
only messages + roster between gateways. Keystroke-injection stays strictly local to each gateway.*
|
||||
|
||||
### 11.2 How a delegation reaches a sandboxed agent on another host
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant MA as main (host A)
|
||||
participant GA as gateway A
|
||||
participant BR as broker
|
||||
participant GB as gateway B
|
||||
participant SB as sandbox agent (host B, container)
|
||||
MA->>GA: bridge_send(globalId on B, msg)
|
||||
GA->>GA: directory lookup - is globalId local? NO
|
||||
GA->>BR: publish agent.ID.inbox (durable)
|
||||
BR->>GB: route to the owning gateway
|
||||
GB->>SB: inject via B's LOCAL herdr (keystrokes)
|
||||
Note over GB,SB: SandboxLauncher already spawned the container -<br/>its PTY is in B's herdr, CB-306 readiness passed
|
||||
SB-->>GB: bridge_reply (to B's LOCAL MCP endpoint)
|
||||
GB->>BR: publish primary-bound (durable, msg id)
|
||||
BR->>GA: route back to A
|
||||
Note over GA: held until the main pulls (the main is a client)
|
||||
MA->>GA: poll / blocking send resolves
|
||||
GA-->>MA: reply
|
||||
```
|
||||
|
||||
*Figure 12 — the `local ? inject : publish` fork (CB-308 §3.2) with a sandboxed far side. Only the
|
||||
**middle** hop crosses the network via the broker; **both** injection points (into the sandbox on B,
|
||||
and the drain-nudge back into the main on A) are local herdr writes. This is CB-308's routing rule
|
||||
unchanged — the sandbox is transparent to it.*
|
||||
|
||||
### 11.3 Two reachability changes any sandbox forces
|
||||
|
||||
| Change | Today | Under sandboxes |
|
||||
|---|---|---|
|
||||
| **`mcpUrl`** | `http://127.0.0.1:8765/mcp` (loopback) | must be **host-routable from inside the container** (e.g. `host.docker.internal` or the gateway's LAN IP) — the worker connects to **its own gateway's** MCP, never a remote one. |
|
||||
| **PTY ownership** | pane in the daemon's herdr | pane is the **container's** attached PTY, in the **local** gateway's herdr (via `docker exec`/devcontainer) — non-negotiable per §11.1. |
|
||||
|
||||
### 11.4 Everything maps to an existing seam (nothing new invented)
|
||||
|
||||
| Concern | Provided by |
|
||||
|---|---|
|
||||
| Per-host gateway (owns local herdr + sessions) | **CB-308** (today's `bridged`, evolved) |
|
||||
| Spawn into a local sandbox / role→image | **Development A** `SandboxLauncher` (§4), routed by `CompositePeerLauncher` |
|
||||
| Addressing a remote sandboxed agent | **CB-308** global id + federated roster (host + role as metadata) |
|
||||
| Orphan reap after a gateway restart | **CB-117** per-gateway, summed by the composite — each reaps only its **local** herdr |
|
||||
| Container up/down | tied to **CB-303** session lifecycle — `SandboxLauncher.stop` tears the container down with the pane |
|
||||
| Cross-gateway spawn/send trust | **CB-308 item #5** / CB-401 Stage-C — each gateway edge is a trust boundary |
|
||||
|
||||
*The net: distributed sandboxes add **zero** new pillars — they are `CB-308 gateway × Development-A
|
||||
launcher` at every host, with the §7 ownership line (peer owns the image; the bridge only launches
|
||||
into it) holding at each gateway.*
|
||||
|
||||
Reference in New Issue
Block a user