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.
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
- Policy source → Canonical MIR
- Canonical MIR → Sandbox policy resolution
- Sandbox policy resolution → Executor
- Executor → Backends
- Backends → Egress and receipts
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.
| Component | Owns | Deliberately does not own |
|---|---|---|
| Canonical MIR | the portable decision, its canonical encoding, its digest | host paths, process spawning |
| Sandbox policy resolution | resolving roots against a trusted HostContext, capability checking | processes, proxying |
| Executor | prepare, spawn, wait, stop, backend selection, execution receipt | the Datalog policy language, DNS |
| Egress | proxy authentication, DNS and pinning, the TCP allowlist, network receipts | operating-system sandboxing |
| System primitives | reactor, sockets, clocks, buffers, idempotent shutdown | policy, Bubblewrap |
| Warden facade | composing the four layers above into one product decision | a 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.
| Library | Implements | Links against / depends on |
|---|---|---|
libmaelys-mir.a | canonical MIR encoding and digests | external; built by Maelys Sandbox Policy |
libmaelys-sandbox-policy.a | root resolution and capability checking, Sandbox Policy ABI 4 | external; pinned by adapter/MAELYS_SANDBOX_POLICY_PIN |
libmaelys_sys.a | clocks, descriptor ownership, CLOEXEC pipes and socketpairs, coalesced wakeups | external; pinned by adapter/MAELYS_SYSTEM_PIN |
libmaelys_egress.a | the Egress enforcement core | external; pinned by adapter/MAELYS_EGRESS_PIN |
libmaelys_executor.a | the Executor engine, ABI 9 | maelys-sys >= 0.4.0 only |
libmaelys-netclient.a | child-side fd 4 client, ABI 1 | maelys-sys >= 0.4.0 only |
libmaelys-warden-policy-adapter.a | Sandbox Policy plan to ExecutorPlan lowering, ABI 6 | libmaelys_executor.a, libmaelys-sandbox-policy.a, libmaelys-mir.a |
libmaelys_executor_egress_adapter.a | mediated-network lowering and the fd 4 broker, ABI 1 | libmaelys_executor.a, libmaelys_egress.a |
libmaelys_mcp_executor_adapter.a | optional adapter for the mcp-runtime launcher ABI 6 | libmaelys_executor.a |
libmaelys-warden.a | the product facade, ABI 5 | the 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.
| Backend | Target kind | Isolation mechanism | Where the policy is enforced | Composite |
|---|---|---|---|---|
posix | local | none | nowhere; the process runs unconfined | no |
seatbelt | local | deny-default SBPL profile | host kernel, on the host process | no |
bubblewrap | local | Linux namespaces, mounts, seccomp | host kernel, on the host process | no |
apple-container | sealed OCI | lightweight Linux VM | the VM boundary and the OCI root | no |
apple-container-bubblewrap | sealed OCI | VM plus a Warden-owned guest Bubblewrap | the VM boundary, then inside the guest | yes |
krun | sealed OCI | libkrun microVM under a deny-default host Seatbelt profile | host Seatbelt, then the VM boundary | no |
krun-bubblewrap | sealed OCI | the same microVM plus an injected Warden Bubblewrap | host Seatbelt, the VM boundary, then inside the guest | yes |
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:
| Element | linux/arm64 target | linux/amd64 target |
|---|---|---|
| Warden and the macOS driver | native arm64 | native arm64 |
| guest Linux kernel | arm64 | arm64 |
| Bubblewrap and the Warden relay | static arm64 | static arm64 |
| OCI workload | native arm64 | amd64, 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:
fd4suits 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 withSCM_RIGHTS.proxysuitscurl,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:
| Element | Current cardinality | Shared between executions? |
|---|---|---|
the installed maelys-relay-bootstrap file | 1 per installation | yes, read-only |
| bootstrap/supervisor process | 1 per mediated spawn | no |
| L4 relay process | 1 per mediated spawn | no |
| loopback TCP listener | 1 per namespace | no; the same port number may be reused |
| Egress server + host thread | 1 per mediated spawn | no |
| directory, AF_UNIX socket and immutable principal | 1 set per mediated spawn | no |
| stream pair and two buffers | 1 set per proxied connection | no, 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.
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 JSONLThe 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.ais the mechanical API, agnostic of Sandbox and Egress. It also exposesTargetSpecandTrustedRunSpec, the multi-candidate planner and the four optional OCI backends: the two Apple candidates and the two Krun candidates.libmaelys-warden.ais the product facade: inline, JSON and MIR policy, composition, streams, lifecycle and versioned receipts.maelys-wardenuseslibmaelys-wardenexclusively, 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-workspaceand--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.