Core concepts

Architecture

The complete Warden architecture: policy, planner, Executor, sandbox backends, Egress and receipts.

Maelys Warden is the product layer that composes three things: a portable decision about what a command may do, the enforcement of that decision by the Executor engine, and the evidence the run leaves behind.

Warden never decides which rights to grant. The host does. What Warden owns is the guarantee that the decision it was handed is the decision that gets enforced, and that the result can be checked afterwards.

That guarantee comes from keeping five responsibilities strictly separate. Each stage below receives a narrower, more concrete artefact than the one before it, and no stage can widen what an earlier one decided.

ARCHITECTUREFrom a policy source to a recorded execution
    • Policy sourceinline flags, JSON, builder API or Datalog
    • Canonical MIRthe portable decision and its decisionDigest
    • Sandbox policy resolutiontrusted HostContext, resolved SandboxPlan
    • Executorprepare, spawn, wait, stop, capability selection
    • Backendsseven OS mechanisms behind one contract
    • Egress and receiptsmediated connections and terminal evidence

The Backends stage stands for all seven implementations at once. As far as the pipeline is concerned they are interchangeable, so the overview keeps them in a single box and the section below opens it.

Three of those stage names are Warden vocabulary rather than general terms, so they are worth pinning down before anything else.

MIR is Warden's compiled, platform-independent form of a policy. Whatever the source — CLI flags, a JSON document, the C builder API or Datalog — it is lowered to the same canonical byte encoding. Because that encoding is canonical, hashing it yields a stable decisionDigest: the same policy always produces the same identity, on any host.

A backend is the concrete operating-system mechanism that actually blocks a forbidden read or connection: a sandbox profile, a set of Linux namespaces, or a virtual machine. It is the only thing in the pipeline that the kernel enforces.

A capability mask is how the two meet. A compiled plan declares the set of capabilities it requires — recursive filesystem rules, a network namespace, a bounded lifecycle — and every backend declares, through its probe(), the set it can guarantee on this host. Warden selects the strongest candidate whose set covers the requirement, and refuses the run when none does. There is no silent degradation to a weaker backend.

Component boundaries

The interesting part of this architecture is not what each component does, but what it is forbidden from doing. Every boundary below exists so that one component cannot quietly reinterpret another decision.

ComponentOwnsDeliberately does not own
Canonical MIRthe portable decision, its canonical encoding, its digesthost paths, process spawning
Sandbox policy resolutionresolving roots against a trusted HostContext, capability checkingprocesses, proxying
Executorprepare, spawn, wait, stop, backend selection, execution receiptthe Datalog policy language, DNS
Egressproxy authentication, DNS and pinning, the TCP allowlist, network receiptsoperating-system sandboxing
System primitivesreactor, sockets, clocks, buffers, idempotent shutdownpolicy, Bubblewrap
Warden facadecomposing the four layers above into one product decisiona new policy engine

Read the right-hand column first. MIR knows nothing about host paths, so a decision cannot smuggle in a filesystem location that only makes sense on one machine. Executor contains no Datalog and no DNS resolver, so it cannot reach a different conclusion than the policy layer did. Egress never opens a sandbox, so a network rule cannot loosen a filesystem rule. And the Warden facade composes, but never adds a sixth opinion about authority.

Libraries and their dependencies

The components above map onto a set of static libraries, some built in the Warden repository and some pulled in as pinned source dependencies. The linkage matters architecturally: a library that does not link against the policy engine provably cannot contain one.

LibraryImplementsLinks against / depends on
libmaelys-mir.acanonical MIR encoding and digestsexternal; built by Maelys Sandbox Policy
libmaelys-sandbox-policy.aroot resolution and capability checking, Sandbox Policy ABI 4external; pinned by adapter/MAELYS_SANDBOX_POLICY_PIN
libmaelys_sys.aclocks, descriptor ownership, CLOEXEC pipes and socketpairs, coalesced wakeupsexternal; pinned by adapter/MAELYS_SYSTEM_PIN
libmaelys_egress.athe Egress enforcement coreexternal; pinned by adapter/MAELYS_EGRESS_PIN
libmaelys_executor.athe Executor engine, ABI 9maelys-sys >= 0.4.0 only
libmaelys-netclient.achild-side fd 4 client, ABI 1maelys-sys >= 0.4.0 only
libmaelys-warden-policy-adapter.aSandbox Policy plan to ExecutorPlan lowering, ABI 6libmaelys_executor.a, libmaelys-sandbox-policy.a, libmaelys-mir.a
libmaelys_executor_egress_adapter.amediated-network lowering and the fd 4 broker, ABI 1libmaelys_executor.a, libmaelys_egress.a
libmaelys_mcp_executor_adapter.aoptional adapter for the mcp-runtime launcher ABI 6libmaelys_executor.a
libmaelys-warden.athe product facade, ABI 5the three adapters, libmaelys_executor.a, and maelys-sandbox-policy >= 0.3.0, maelys-egress >= 0.9.0, maelys-sys >= 0.4.0

Two rows carry most of the design. libmaelys_executor.a requires nothing but maelys-sys: it has no link edge to Sandbox Policy, to MIR or to Egress, which is what makes the phrase policy-agnostic engine a build-time fact rather than a convention. Conversely libmaelys-warden.a is the only library that pulls all three worlds together, which is exactly why the facade is where composition lives.

The four external libraries are not vendored copies. Each is a pinned source dependency recorded as a commit in adapter/MAELYS_*_PIN, built from a sibling checkout, so the version composed into a release is pinned rather than discovered. mcp-runtime is pinned the same way, but the relationship there is behavioural only: nothing in Warden links against it.

Backends behind one Executor

Executor exposes one backend contract and registers every implementation that its host can support. The choice among them is a planning decision, not a runtime fallback: maelys_executor_create_default() discovers the optional drivers, the planner then compares required capabilities against advertised ones, and an ineligible candidate is rejected before prepare rather than downgraded during the run.

The split that organises all seven is the kind of target being executed.

Without --image, the Warden and CLI facade builds a local target and can reach posix, seatbelt or bubblewrap. The Apple and Krun branches require an OCI TrustedRunSpec, which the CLI has built since 0.19.0 from --image REFERENCE@sha256:DIGEST. A local host command is never implicitly converted into an image.

BackendTarget kindIsolation mechanismWhere the policy is enforcedComposite
posixlocalnonenowhere; the process runs unconfinedno
seatbeltlocaldeny-default SBPL profilehost kernel, on the host processno
bubblewraplocalLinux namespaces, mounts, seccomphost kernel, on the host processno
apple-containersealed OCIlightweight Linux VMthe VM boundary and the OCI rootno
apple-container-bubblewrapsealed OCIVM plus a Warden-owned guest Bubblewrapthe VM boundary, then inside the guestyes
krunsealed OCIlibkrun microVM under a deny-default host Seatbelt profilehost Seatbelt, then the VM boundaryno
krun-bubblewrapsealed OCIthe same microVM plus an injected Warden Bubblewraphost Seatbelt, the VM boundary, then inside the guestyes

Composite means the candidate is prepared and selected as a single unit that stacks two mechanisms. It is not a late fallback from the simple candidate: if the Warden-owned guest bundle is missing or mutated, the composite is withdrawn or the artefact fails, and the simple candidate is never substituted for it.

This table compares only what is architecturally distinct. For the exact filesystem, network, I/O and lifecycle guarantees each backend advertises, see the normative backend capability matrix and the Apple Container guide.

Workload platform and TCB platform

The OCI platform belongs to the TargetSpec and describes the workload. It does not ask for the trusted Warden components to be recompiled for the same architecture. The cross-platform Apple execution shipped since 0.19.0 keeps a single static arm64 guest TCB:

Elementlinux/arm64 targetlinux/amd64 target
Warden and the macOS drivernative arm64native arm64
guest Linux kernelarm64arm64
Bubblewrap and the Warden relaystatic arm64static arm64
OCI workloadnative arm64amd64, translated by Rosetta

Platform compatibility is therefore a planner predicate in its own right, distinct from MIR capabilities: a candidate can advertise every capability the plan needs and still be rejected because it cannot run that workload architecture. The detailed plan, the probes and the physical gates are in apple-container-1.3-plan.md.

Two network frontends, one decision

A mediated destination is one the workload may not open directly: the connection is proxied, authorised and recorded by Egress instead. The mediated MIR carries the TCP allowlist, and the trusted HostContext then chooses an explicit mediator. Both native frontends terminate in Egress:

  • fd4 suits Maelys-aware clients. They request a destination on the control channel; the adapter translates that request into the native Egress connector and hands back the relayed stream with SCM_RIGHTS.
  • proxy suits curl, git, npm, Codex and Claude Code unmodified. Under Bubblewrap the workload only ever sees an L4 loopback relay inside its own namespace. That relay connects to the private AF_UNIX listener of Egress; it parses nothing and decides nothing. Under Seatbelt no relay is needed: one SBPL rule admits only the loopback TCP port that Egress opened for this execution, with the matching IPv6 port reserved inertly.

There is no longer any implicit conversion between these two identities. The Sandbox-Executor adapter lowers maelys-egress/fd4-v1 to NETWORK_EXTERNAL_CHANNEL and maelys-egress/proxy-v1 to NETWORK_MEDIATED_PROXY. In both cases Egress is the single engine for authorisation, connection, relaying, observation and receipts. An unknown name is refused.

Across a VM boundary, SCM_RIGHTS cannot carry a descriptor from the host kernel into the guest kernel. The Apple and Krun backends therefore ship the proxy frontend only: a guest L4 relay, then the Containerization UDS or vsock transport for Apple and AF_VSOCK 4520 for Krun, and finally Egress on the host. A workload fd 4 frontend across a VM remains unshipped.

The struck-out box does not exist in the code any more: a low-level maelys_executor_spawn() call carrying EXTERNAL_CHANNEL but no trusted channel is refused.

The Egress sequence under Bubblewrap

Every mediated spawn currently owns one Egress server and one host thread. Inside that server a reactor multiplexes the connections, so there is no thread per socket. This per-execution isolation is what prevents two runs from accidentally sharing a principal or a policy generation. A shared daemon remains a future option, not a dependency of the core.

The relay: one shared file, private processes

maelys-relay-bootstrap is an executable binary, not a library and not a file descriptor. An installation contains exactly one such file, in libexec. Executor bind-mounts that same file read-only into each Bubblewrap namespace; it does not copy it per launch.

At runtime, by contrast, nothing is shared between two workloads:

ElementCurrent cardinalityShared between executions?
the installed maelys-relay-bootstrap file1 per installationyes, read-only
bootstrap/supervisor process1 per mediated spawnno
L4 relay process1 per mediated spawnno
loopback TCP listener1 per namespaceno; the same port number may be reused
Egress server + host thread1 per mediated spawnno
directory, AF_UNIX socket and immutable principal1 set per mediated spawnno
stream pair and two buffers1 set per proxied connectionno, multiplexed by the relay

The bootstrap starts its relay process first, waits for the listener to be ready, then starts the workload, forwards signals and waits for it to finish. The single relay of that execution accepts several connections and multiplexes them in one maelys-system loop. For each accepted connection it opens a separate AF_UNIX stream to the Egress server of that same execution.

Both relay processes execute the same installed file, much as two curl processes can execute the same /usr/bin/curl, but their state, listeners, principals and connections are isolated. The proxy URLs for these AF_UNIX paths contain no userinfo. The Seatbelt TCP frontend and the fd 4 frontend keep their explicit authentication instead.

The relay uses several internal descriptors — the loopback listener, client sockets, the AF_UNIX streams to Egress and a readiness channel — but "the relay" is not a descriptor. The fd4 frontend is entirely different: it starts no relay, and fd 4 of the workload is then a Maelys-aware datagram control channel translated into the native Egress connector. In proxy mode the workload does not get that channel at all. The descriptor numbered 4 that Bubblewrap uses temporarily to deliver the private environment is consumed before the workload runs and is not the fd4 API.

Digests and receipts

A receipt is the record a finished run leaves behind: which decision was enforced, which backend enforced it, and how the run ended. It is written to the files you asked for and never mixed into the workload's own output.

CODE
JSON/builder --> MIR digest --> decisionDigest
                         SandboxPlan
                              |
                              v
                  ExecutorPlan digest --> planDigest
                              |
                 backend + timestamps + outcome
                              v
       receipt v1 (local CLI compatibility), v3 (RunSpec ABI 8)

       receipt v2 stays frozen for historical ABI 7 consumers

Egress policy digest + destination + bytes + result
                              v
                    network receipts JSONL

The two digests answer different questions. decisionDigest identifies the policy you authored; planDigest identifies the mechanical plan Executor actually lowered it to. Keeping them separate is what lets you prove both that the right policy was used and that it was not rewritten on the way down.

A receipt never contains the proxy credential, the private AF_UNIX path or an environment secret. Workload stdout stays separate: the CLI writes receipts only into the files it was asked for. See receipts for the field level detail.

Low-level API and product API

Warden ships two C surfaces on purpose, because an integrator embedding a mechanical launcher and an integrator embedding the product have genuinely different needs.

  • libmaelys-executor.a is the mechanical API, agnostic of Sandbox and Egress. It also exposes TargetSpec and TrustedRunSpec, the multi-candidate planner and the four optional OCI backends: the two Apple candidates and the two Krun candidates.
  • libmaelys-warden.a is the product facade: inline, JSON and MIR policy, composition, streams, lifecycle and versioned receipts.
  • maelys-warden uses libmaelys-warden exclusively, and the SDKs wrap the same CLI contract for their first version. That facade runs local targets and, since 0.19.0, the OCI targets requested through --image, --platform, --guest-workspace and --guest-cwd. The Executor RunSpec API remains available for mechanical mappings an integrator has reviewed.

This is why mcp-runtime can keep linking only the ABI 6 launcher adapter for plans that need no mediation. An MCP host that requests a mediated destination must compose the Egress lifecycle explicitly; the pure adapter refuses such a plan fail-closed. An orchestrator or a standalone tool can pick the full facade directly.

The workload descriptor space, the host driver descriptor space and the common Apple and Krun MWDP/1 protocol shipped since 0.20.0 are covered separately in warden-driver-protocol.md.