Docs How it works
Architecture
Four layers. Three ship today; the fourth doesn't exist yet. Every one of them runs in the host constle process, outside the agent.
On this page
The four layersLink to this section
| Layer | What it does | Where | Status |
|---|---|---|---|
| 4 · Commerce | Agents discovering and paying each other for work. Direction only; see Roadmap. | none yet | Planned |
| 3 · Communication | A2A: Ed25519-signed envelopes, host-side sign and verify, declared peers only. | internal/a2a/ |
Shipped |
| 2 · Identity & governance | did:key identity, the signed and hash-chained audit log, human gates, and the per-run / per-day USD ledger. |
internal/identity, audit, mcpgate, spending |
Shipped |
| 1 · Runtime & sandbox | Firecracker microVM or two-network Docker sandbox, no default route, Squid egress allowlist. | internal/sandbox/ |
Shipped |
Layer by layerLink to this section
Layer 1 · Runtime and sandboxLink to this section
A Firecracker microVM or a two-network Docker sandbox, in both cases with no default route. Egress traverses a Squid proxy that allowlists network.allowed_hosts by name, and a wall-clock kill switch stops the agent when limits.max_duration_seconds elapses. Both backends render their proxy policy from the same function, so Docker and Firecracker enforce one ruleset rather than two that drift. Details: Network isolation.
Layer 2 · Identity and governanceLink to this section
A W3C did:key identity whose private key never enters the sandbox (Identity); a JSONL audit log that is Ed25519-signed and hash-chained once a DID is declared (Audit log); human approval gates at the MCP proxy (Human gates); and a per-run and per-day USD ledger metered at the tool-call boundary (Spend caps). The daily ledger is durable across runs and keyed by DID, so renaming an agent does not reset its spend.
Layer 3 · CommunicationLink to this section
Agent-to-agent messages as Ed25519-signed envelopes, exchanged only with peers named in the manifest. The host signs and verifies; the sandbox performs no cryptography and never learns a peer's real endpoint. There is no discovery mechanism, by design: an agent can only talk to what its manifest declared before it started. Details: Agent-to-agent.
Layer 4 · CommerceLink to this section
Planned, with no code in the repository. Agents discovering and paying each other for work, once identity and spending enforcement are solid. A direction, not a scheduled milestone.
Where each rule is enforcedLink to this section
Every enforcement point sits outside the sandbox, at a chokepoint the agent's traffic must physically traverse (spec §2.3):
| Chokepoint | When | Enforces |
|---|---|---|
| Sandbox environment construction | before the agent exists | credentials |
| 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 |
| Supervisor process | per run | limits.max_duration_seconds, sandbox.memory_mb |
The first row is the only one that acts before the agent exists rather than while it runs: the environment is composed, the sandbox is started with it, and there is no later moment at which an agent could ask for more.
Constle assumes nothing inside the sandbox is trustworthy. A control that depended on the agent truthfully announcing its own behaviour would be reliable only while it was unnecessary. That is why some intuitively useful fields don't exist: there is no human gate on filesystem writes, because writes have no chokepoint outside the sandbox yet (spec §14.3).
Isolation levels and backendsLink to this section
sandbox.isolation is one of none, process, network or kernel. Each declared capability needs a minimum level, and the strongest of them is the capability floor. Omit isolation: and the runtime uses the floor; declare it and it may only be equal or stronger, or constle validate rejects the Agentfile, naming the capabilities that set the floor.
The resolved level is a minimum contract, not a preference. Each backend provides a fixed level:
| Docker | Firecracker | |
|---|---|---|
| Provides | network: separate process and network namespaces, shared host kernel |
kernel: a guest kernel behind KVM |
| Route out | Network created --internal; one on-link route, no default route |
No global address or ::/0 route; a per-run nftables table drops the tap |
| IPv6 | Off, via an explicit --ipv6=false |
Kernel link-local fe80:: only |
| Selected when | Auto-detected, or --backend=docker |
Auto-detected, --backend=firecracker, or required by isolation: kernel |
So isolation: kernel selects Firecracker, and when Firecracker is unusable on the machine (it needs KVM and root) the run aborts instead of continuing on Docker. The one way to run on a weaker boundary than the host can build is for an operator to name it:
constle run --accept-isolation=network agent.yamlThe run prints ISOLATION DOWNGRADE ACCEPTED and records both the requested and the achieved level in its run_started audit entry. The flag cannot waive the capability-floor refusal, which fires at validation, before any backend is chosen. Full rules: Field reference §6.1 and §8.
Life of an MCP tool callLink to this section
- The agent calls a name. It only knows
CONSTLE_MCP_<ID>_URL, which points at the gate. The real server URL stays on the host. - The gate reads the call. Only
POST,GETandDELETE, and a JSON-RPC body only onPOST. A body with duplicate or case-colliding keys, an ambiguous sub-path and a protocol upgrade are all refused. - Tool allowlist. A tool not listed in
mcp.servers[].toolsis refused at the gate (mcp_tool_blocked). - Human gate. If the tool name is in
require_approval_for, the call waits for the terminal or a signed webhook decision. - Forward and meter. The call goes to the declared endpoint only. The response is metered against the server's
pricing; over a cap, the run is killed. - Record. Each step lands in the signed, hash-chained audit log.
What never enters the sandboxLink to this section
| Stays on the host | Why |
|---|---|
~/.constle/identities/<name>/ |
The agent's private key, mode 0600. |
mcp.servers[].url |
Real MCP endpoints. The agent sees only the gate. |
a2a.peers[].endpoint |
Real peer endpoints. The agent can only name a peer. |
| Your shell's environment | Only the variables the Agentfile's credentials names are passed in. |
The url_secret_ref value |
The webhook URL lives in a host environment variable, not the Agentfile. |
~/.constle/webhook-keys/ |
The approver's signing key from constle webhook-keygen. It goes to whoever runs the decision endpoint, never to the agent. |
Why the direction of control mattersLink to this section
The agent runs in the sandbox, and the controls run in the host process; the two are not peers. What the agent sees is a set of per-run gate addresses.
A control the agent can reach is a control the agent can be talked into disabling. These controls are not reachable from inside the sandbox: not because the agent is trusted to leave them alone, but because there is no route, no key and no endpoint in there to act on.
A prompt injection that convinces the model to exfiltrate a document does not fail because the model reconsiders. It fails at the network layer, below the model, and the attempt is written to the audit log as a network_blocked event, which is how the operator finds out it happened at all.
Something wrong or unclear? Open an issue. The specifications on these pages are copies of spec/ in constle/constle.