Skip to content

How It Works

wardcell turns .sbx fragments into one resolved policy, compiles its filesystem half to the host sandbox, and holds its network half in a local proxy — so the backend and the proxy enforce the same policy and cannot disagree.

flowchart TB
  frags[".sbx fragments<br/>base · selected · workspace"] --> engine["policy engine<br/>merge · validate · check"]
  engine --> policy["resolved policy<br/>one canonical IR"]
  policy --> backend["OS backend<br/>Seatbelt / Bubblewrap"]
  policy --> proxy["wardcell-proxy<br/>network gate, on loopback"]
  backend --> cmd["your command<br/>sandboxed process"]
  cmd -->|all egress| proxy
  proxy -->|allowed| net["granted hosts"]
  proxy <-.->|credentials · approvals · discovery| plugin["plugin — optional<br/>your identity + approval systems"]
  • Policy engine. Loads base.sbx, the fragments a run selects, and the workspace .wardcell.sbx, and merges them into one resolved policy — rejecting conflicts with a fragment name and a line number before anything runs.
  • OS backend. Compiles the filesystem and process half of that policy to the host — Seatbelt on macOS, Bubblewrap on Linux — and launches your command as a sandboxed process inside it.
  • Proxy. Runs on loopback, outside the sandbox, and is the only way out. It gates every connection and request against the same resolved policy, and terminates TLS with a per-run CA so it can see hostnames, HTTP methods and paths, and GraphQL operations.
  • Plugin (optional). One program you supply. The proxy and the zone supervisor ask it for credentials to attach, for a human (or standing) approval, and to resolve service names — the seam where wardcell meets your own identity and approval systems. Nothing inside the sandbox ever sees it.
  1. Load. base.sbx, whatever selects_on / --profile / requires pulls in from the root-owned policy root, then the workspace’s .wardcell.sbx.
  2. Resolve. Merge into one policy, apply the layer rules, reject conflicts.
  3. Render. Compile the filesystem and process half to the host backend.
  4. Run. Exec the command inside it, with the proxy holding the network half.

A kernel-level host allowlist does not work. Kernels filter addresses, not names — and one address serves thousands of names behind a CDN, addresses change between DNS lookups, and a redirect moves you to a host nobody checked.

So wardcell routes egress through a proxy that sees names and gates on the name. That is also what makes the finer rules possible: once the proxy terminates TLS, it can gate an individual HTTP method and path, or a single GraphQL mutation.

Layered, outermost first. Each one fails closed.

LayerRefuses
OS sandboxanything not aimed at the proxy — no policy knowledge needed
DNSa name no rule mentions
Connection gatea non-HTTP stream (SSH, a database, …) to a host:port no rule grants
Address vettinga name that resolves somewhere refused
Request gatean HTTP(S) request no rule grants — logged with its full path
Body inspectiona GraphQL operation outside allow_mutations

The proxy, the DNS resolver, the per-run CA and (with zones) the supervisor all run outside the sandbox; the sandboxed process gets a loopback address and nothing else. On Linux that is literal — a network namespace with no route out but a bind-mounted socket. On macOS it is a Seatbelt rule denying all outbound except the proxy, or a system extension that hands the sandbox’s flows to wardcell whether or not the program cooperates. See macOS and Linux.