cocoonbox

Boundaries

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.

Horizontal: zero coupling

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:

Vertical: one interface per layer

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.

Authorization and credentials

Two invariants:

  1. No user can reach another user’s machine. Every path into a box (agent gRPC, MCP, the screen, state transfer, lifecycle calls) is authorized per user against box ownership at the edge or api, and nothing inside one box can reach another box: no shared secret that opens every box, no box-to-box network path, no storage prefix readable across users.
  2. Credentials stay as local as possible, never at rest in the VM. Order of preference: on the user’s Mac (Keychain) → host side, outside the VM (injected at egress, never readable back) → inside the VM only in memory for one request. A credential written to the VM disk ends up in its snapshots, checkpoints and state uploads, so it is treated as leaked to every future copy of that box.

Consequences:

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: