How it works
Three layers, none inside the agent.
All three layers are on the host, outside the sandbox. The agent gets a sandbox, a proxy and its run’s gates. While the sandbox holds, the rules are out of the agent’s reach.
-
3Communication
A2A messages go in Ed25519-signed envelopes, which the host signs and verifies, to declared peers only. The sandbox does no cryptography and is never given a peer’s real endpoint. There is no discovery mechanism, by design.
internal/a2a/
-
2Identity & governance
did:keyidentity, the audit log (signed and hash-chained once the agent has an identity), human gates at the MCP gate proxy, and the per-run / per-day USD ledger.internal/identity · audit · mcpgate · spending
-
1Runtime & sandbox
A Firecracker microVM or a two-network Docker sandbox, and a Squid egress allowlist.
constlestops the run atmax_duration_seconds.memory_mbis the container’s memory limit (Docker) or the microVM’s memory size (Firecracker).internal/sandbox/
01What it enforces
Enforced below the model.
Eight capabilities, each with how it works and where it stops.
| Capability | How | Where it stops | |
|---|---|---|---|
| C.01 | Sandboxed execution Firecracker microVM or a two-network Docker sandbox. |
isolation: is a minimum. kernel selects Firecracker and fails closed if it’s missing. Declaring external_transfer, delete_records or spawn_subagent raises the floor to kernel. Otherwise Docker runs, and Firecracker only when forced with --backend=firecracker. |
--accept-isolation=<level> lets an operator knowingly run one run on a weaker boundary. It is named and recorded. Docs ↗ |
| C.02 | Network egress Egress through the proxy meets a Squid allowlist. A raw IP is denied unless that exact address is itself listed. |
Name-based dstdomain matching with reverse lookups off. Loopback, link-local, metadata and private ranges refused on the resolved address. CONNECT only to 443. |
DNS for public addresses is trusted, so a declared name that resolves to an attacker’s public IP gets through. An allowed host’s server can answer for other names it hosts. Traffic to hosts you allow isn’t gated. If an allowed host shares a server with an MCP server, a request can reach that MCP server past the gate. Docs ↗ |
| C.03 | Spending limits Per-run and per-day USD caps you set, on priced calls that pass through the gate. |
Spending is a meter, not a bill. It is the usage each priced server reports, multiplied by the price you declare in pricing. The daily ledger is durable and keyed by DID. A priced result with no usage value stops the run. |
Costs are counted after each call, so a run can pass its cap before it is stopped, and each overlapping run of one identity can pass the daily cap by one call. Traffic through allowed_hosts (direct LLM calls included) is not metered. max_per_month_usd is not enforced, and validate warns about it. Methods listed under methods on a priced server go through unmetered, and error replies aren’t charged. Docs ↗ |
| C.04 | Human gates Named MCP tool calls that pass through the gate wait for a human, at the terminal or through a signed webhook. |
A protocol-aware gate proxy pauses matching tools/calls. A webhook decision must be Ed25519-signed by approver_pubkey. A decision that fails verification can only deny. |
Exact tool-name match only. A terminal approval is unsigned. With no decision, on_timeout decides (default abort). Gates are off until human_gates.enabled: true. A line typed ahead at the terminal can answer a later prompt. Docs ↗ |
| C.05 | Audit log One JSONL file per agent per UTC day, signed and hash-chained when an identity is declared. |
With identity.did set, every entry is Ed25519-signed. constle audit verify reports the first edited line and re-verifies recorded gate decisions. |
An intact chain shows no entry was changed, inserted or reordered. It can’t show that none were cut from the end, and it doesn’t make the account true. Docs ↗ |
| C.06 | Cryptographic identity
|
Key at ~/.constle/identities/<name>/, mode 0600. constle run fails closed if the declared DID has no local key. |
With an identity, identity.owner must match the owner the local identity was created with. It binds nothing else. Docs ↗ |
| C.07 | Max duration The agent is killed when its time is up. |
constle stops the run at max_duration_seconds and records terminated_by_limit. |
Wall-clock only. memory_mb is the container’s memory limit (Docker) or the microVM’s memory size (Firecracker). sandbox.disk_mb is declared, not applied, and validate says so. Docs ↗ |
| C.08 | Agent-to-agent messaging Signed envelopes, to declared peers only. |
The host signs and verifies. The sandbox does no cryptography and is never given a peer’s real endpoint. There is no discovery, by design. | Replay state lives in the invoking user’s home directory, so an identity listening from several home directories can be replayed once per home directory within ±5 minutes. Docs ↗ |
02Where each rule is enforced
Enforcement sits on the host, where traffic meets the proxy and the gates.
Constle assumes nothing inside the sandbox is trustworthy. A control that relied on the agent reporting its own behaviour would only work when it wasn’t needed.
| Chokepoint | When | Enforces | beforerun startmax_duration_seconds |
|---|---|---|---|
| Sandbox environment construction | before the agent exists | credentials, sandbox.memory_mbsandbox.memory_mb is the container’s memory limit (Docker) or the microVM’s memory size (Firecracker), set when the sandbox is created. | |
| Squid egress proxy | per run | sandbox.network.allowed_hosts | |
| MCP gate proxy | per run | mcp.servers[].tools, human_gates.*, spending.* metering | |
| A2A gate + host listener | per run | a2a.peers authorization, envelope signing and verification | |
constle on the host | per run | Stops the run at limits.max_duration_seconds. |
The first row acts before the agent starts. The environment is composed and the sandbox is started with it, so there’s no later moment to ask for more.
03The isolation floor
Capabilities set the floor.
Each declared capability needs a minimum isolation level. A declared level may only strengthen that floor, never weaken it. Try it.
capabilities is not
It is not a sandbox permission system. Declaring read_file grants nothing and omitting it removes nothing, because the image and mounts decide file access. Capabilities are self-asserted, so an agent that omits external_transfer and declares isolation: none is not raised to kernel and runs on Docker (network isolation). Capabilities naming irreversible actions are reported by validate as needing approval, but that is advice. Only human_gates.require_approval_for gates anything.
04Backends
Two backends share one ruleset.
Both render their proxy policy from the same function, so Docker and Firecracker enforce the same allowlist.
| Docker | Firecracker | |
|---|---|---|
| Boundary | Two-network container sandbox | microVM, with a kernel of its own |
| Route out | Network created --internal, with one on-link route and no default route | A default route only to the TAP gateway on the host (a /30, no forwarding or NAT). The per-run nftables inet table accepts only the proxy and gate ports and drops everything else from the tap. |
| IPv6 | Off, via explicit --ipv6=false | Kernel link-local fe80:: only |
| Selected when | the default, or --backend=docker | chosen when the isolation contract needs kernel (isolation: kernel, or a capability whose floor is kernel), or forced with --backend=firecracker |
| Image | The image is run as named | A guest root filesystem prepared for that image, images/<image>.ext4. An image with none is refused. |
On Docker, the agent’s network is created with the isolated-gateway option, and a Docker Engine that can’t provide it is refused.
05Tool calls
Life of an MCP tool call.
1
The agent calls a name
Constle gives it only
CONSTLE_MCP_<ID>_URL, which points to the gate. Constle never passes the real server URL into the sandbox.2
The gate reads the call
The gate accepts only
POST,GETandDELETE, and only for the declared endpoint itself, so a request for any path below it is refused. A body with duplicate or case-colliding keys is refused, and so is a protocol upgrade.3
Tool allowlist
When a server declares
tools, a call to any other tool is refused. An empty list allows none, and no list allows all. Tool calls go through the tool checks, while the handshake,ping,tools/list, notifications and responses pass. Resource, prompt, completion and logging requests pass only if the server’smethodslist names them, and any other request the agent sends is refused. What the server sends, its own requests included, is relayed without method checks.4
Human gate
If gates are on (
human_gates.enabled: true) and the tool name is inrequire_approval_for, the call waits for the terminal or, when a notify webhook URL resolves, a signed decision.5
Forward and meter
It is forwarded to the declared endpoint only, and the response is metered against
pricing. The call that crosses the cap is counted, and the run is stopped.6
Record
Gate decisions, tool calls and limit hits are written as they happen. Network events are copied from the proxy’s log when the run ends, and the run’s last moments can be missing. With
identity.didset, every entry is signed and hash-chained.
06The Agentfile
One YAML file, with no rule language.
A declared protection must never look real when it isn’t. Choose a line.
agent.yaml, an exampleits lines labelled
07Host-only
What never enters the sandbox.
~/.constle/identities/<name>/- The agent’s private key, mode 0600.
mcp.servers[].url- Real MCP endpoints. Constle passes the agent only the gate’s address.
a2a.peers[].endpoint- Real peer endpoints. The agent can only name a peer.
your shell’s environment- Nothing from it enters except the variables the Agentfile names under
credentials. Those do enter, as environment variables the agent can read. url_secret_ref- The webhook URL lives in a host env var, not the Agentfile.
~/.constle/webhook-keys/- The approver’s signing key from
constle webhook-keygen. Constle never passes it into the sandbox, and you give it to whoever runs the decision endpoint.
08Compared
Four places a rule can live.
A check that runs in the agent’s process, or a rule the model reads in its prompt, is exposed to the same injected text that steers the agent. Constle’s checks run on the host, outside the sandbox.
| reaches the line: it holdsstops short: in parta stub: it doesn’t | Rules in the system prompt | A guard library in the agent’s process | A hosted gateway in someone else’s cloud | Constle: outside the agent, on your machine |
|---|---|---|---|---|
| Where the rule lives | In the model’s context, as text. | In the agent’s own process. | On a gateway someone else runs, in their cloud. | On your machine, around the sandbox. |
| Can injected text talk it out of the rule? | Yes. A rule written in its prompt is more text it can be argued out of. | It is exposed to the same injected text that steers the agent. | It depends on what it judges. A destination can’t be argued with, but content can. | No. The agent can be fooled into trying anything, but what it’s allowed to do doesn’t change while it runs. |
| Does it hold if the agent’s process is compromised? | No. It lives inside the agent. | No. A check inside its own process goes down with it. | For the traffic that is sent through it. | Yes, while the sandbox holds. On Docker that’s the host kernel, and on Firecracker it’s a kernel of its own. |
| Does the traffic have to pass it? | No. Nothing passes through a prompt. | Only the calls the agent makes through it. | Only what the agent is configured to send there. | It enforces at the proxy and the tool gate, for the traffic that passes them. A request to a declared MCP server can go around the gate inside an allowed host’s tunnel (limitation 2). |
| What can you check afterwards? | What the agent chose to record. | Logs written by the process that was steered. | The records the gateway keeps, on its side. | With an identity, a log signed with the agent’s own key. constle audit verify checks it offline and names any line that was changed. |
| What it costs you | Nothing to run. | A dependency in your agent’s code. | Your agent’s traffic goes through a third party’s servers. | Docker or a Firecracker microVM on your own machine. It covers what passes through its proxy and gates, and API keys you declare are passed into the sandbox. The current source and its signed releases aren’t public yet. |
Approaches, not products: each column is a place a rule can live.
In-agent guardrails judge content, meaning what the model is asked and what it says, while Constle governs reach: where the agent can connect, which tools it can call, what calls to priced servers may cost, and what gets recorded. Neither replaces the other.