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.
Architecture
Section titled “Architecture”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"]
The pieces
Section titled “The pieces”- 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.
Every run, four stages
Section titled “Every run, four stages”- Load.
base.sbx, whateverselects_on/--profile/requirespulls in from the root-owned policy root, then the workspace’s.wardcell.sbx. - Resolve. Merge into one policy, apply the layer rules, reject conflicts.
- Render. Compile the filesystem and process half to the host backend.
- Run. Exec the command inside it, with the proxy holding the network half.
Why the network half is a proxy
Section titled “Why the network half is a proxy”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.
Where a request can be refused
Section titled “Where a request can be refused”Layered, outermost first. Each one fails closed.
| Layer | Refuses |
|---|---|
| OS sandbox | anything not aimed at the proxy — no policy knowledge needed |
| DNS | a name no rule mentions |
| Connection gate | a non-HTTP stream (SSH, a database, …) to a host:port no rule grants |
| Address vetting | a name that resolves somewhere refused |
| Request gate | an HTTP(S) request no rule grants — logged with its full path |
| Body inspection | a 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.