cocoonbox follows the layering of cocoon and projecteru2/core: components never couple sideways, and each one depends only on the layer below it through a published interface.
The four components share nothing except published contracts.
| Component | Language | Talks to others only through |
|---|---|---|
macos/ app |
Swift | proto/ gRPC via the edge; RFB bytes inside Session.Screen |
api/ control plane + edge |
Go | proto/ (serves Boxes, proxies agent RPCs and MCP as opaque bytes) |
agent/ in-box agent |
Rust | proto/ gRPC and the MCP contract, served on 7443 |
image/ guest |
shell | docs/contracts.md (users, sockets, ports, env) |
Rules:
shuttle-desk-core (loomhand/shuttle crates/desk-core, a git dependency pinned to a shuttle tag) is the one shared library: the toolhost wire and the desk call arguments of shuttle’s desk wire, which shuttle serves and the toolhost relays (contracts.md, Toolhost wire). It holds contract code only.user has sudo and could forge it: the share page bridge parses the RFB stream and drops what a viewer may not send or get, so it stays view-only and never gets the box’s clipboard (api/edge/screen.go, api/edge/rfb.go).main first (proto/, docs/contracts.md), then each component adopts it.Each component is a stack; a layer calls only the interface of the layer directly below.
api
gRPC services (Boxes) · edge (gRPC + MCP proxy)
│ machines.Machines: lifecycle (Ensure / Get / List / Hibernate / Delete / Watch / Changes),
│ Address, SetMeta, Deliver, SetCredentials, SetEgress
│ machines.Tenants: PutTenant / DeleteTenant
▼
backend: sandboxd (claims and tenants on the cbsbx instance, one client)
The service layer owns users, ownership, operations and their event logs, and names every key it installs. A backend owns only machine lifecycle and tenants and returns an address; it gets secrets as named values to install and never sees proto types or which product credential a value is. The edge depends on an address resolver, never on a backend.
agent
transports: gRPC (Agents, Session) · MCP (/mcp)
│ both call the same tool dispatcher; neither implements tools
▼
loop (mode A only) ── Model interface (chat wire → gw URL)
│ Tools interface
▼
toolhost RPC (unix socket, user `user`)
▼
executors: shell · files · CDP browser · shuttle (desk and computer verbs, runs)
Mode A and mode B differ only at the top layer. Nothing below the dispatcher knows which mode issued a call.
app
SwiftUI views → CocoonBoxKit (gRPC client, connection, events) → swift-rfb (RFB over any byte stream)
swift-rfb is a separate MIT library with no knowledge of cocoonbox.
infra
cocoonbox consumes cocoonstack mechanisms (cocoon, sandboxd, gw) through their public APIs and CLIs only. Anything cocoonbox-specific (users, tiers, idle policy, credentials) stays in cocoonbox; a missing primitive goes upstream as a mechanism, never as cocoonbox logic.
Two invariants:
Consequences:
guest: false) or a broker on the Mac, not through a file in the VM.user has passwordless sudo since desktop v10. The in-box split between user, cbagent and root no longer protects anything from the desktop user or from the tools agents run as user: they can read agent.env, /var/lib/cocoonbox (agent.db) and the identity file, and replace the agent binary. Both invariants still hold, because neither relies on that split:
CB-OUTPUT). Root in the guest can rewrite the guest firewall or the identity file, which affects only its own box. The identity file holds only the edge’s public key, and the edge never sends one box’s credential to another box.agent.env holds the placeholder. The state key and the presigned URLs arrive per request and cover only the box’s own state.user; using them needs a path to another box’s traffic, which the host blocks. v12 has no sshd and no host keys.