v0.5.0 · Open source · Apache 2.0

Rules your agent can’t turn off.

One YAML policy. Network, spend, approvals and identity, enforced from outside the agent, where a compromised agent can’t reach them.

curl -fsSL https://constle.dev/install | sh

The installer checks the release’s SHA-256 before unpacking. How releases are verified

01Network isolation

One gate. No way around it.

No default route, IPv4 or IPv6. Declared hosts pass. Everything else gets a 403, and a line in the log.

Example from the README: a request to api.groq.com, a declared host, is allowed. A request to evil.example.com is refused with 403 Forbidden. Both land in the audit log.

Try to get out.

Seven ways an agent might reach the internet. The results are the README’s.

The full network model
request
CONNECT api.groq.com:443
result
TCP_TUNNEL/200
audit
network_allowed
why
Listed in allowed_hosts. A tunnel may only go to 443.
AWhy the proxy can’t be skipped

On Docker the agent’s network is created --internal: one on-link route and no default route. IPv6 is off with an explicit --ipv6=false, so this doesn’t depend on the host’s Docker defaults. DNS doesn’t resolve inside the sandbox either. The Firecracker guest gets only a link-local fe80:: address, and a per-run nftables inet table covers both address families.

BWhy raw IPs lose

The allowlist is a dstdomain ACL with reverse lookups off. An IP literal is admitted only if that same literal was listed, never by resolving back to a name. A PTR record is written by whoever owns the address, not by whoever wrote your allowlist.

CWhere it stops: DNS

For public addresses the proxy trusts DNS. If a declared hostname resolves to a public address an attacker controls, the allowlist lets it through. DNS can’t aim a declared name back inside: loopback, link-local, metadata and private destinations are refused on the resolved address.

02Product

Watch one run, end to end.

The policy, the run, the human sign-off, the evidence.

Illustrative run. Prompts, flags and event names are Constle’s own.

invoice-processor.yamlAgentfile
identity:  name: invoice-processor  did: did:key:z6Mk…4doKsandbox:  isolation: kernel  network:    allowed_hosts:  # the whole policy      - api.groq.comspending:  max_per_run_usd: "0.50"  max_per_day_usd: "5.00"human_gates:  enabled: true  require_approval_for: [pay_invoice]  approver_pubkey: did:key:z6Mk…Ap7w  notify:    - channel: webhook      url_secret_ref: HUMAN_GATE_WEBHOOK_URL  on_timeout: abort
01 Policy · one YAML file
~/agents — constle runzsh
$ constle run invoice-processor.yamlconstle v0.5.0  ✓ Agentfile valid     network:   restricted → api.groq.com     spending:  run≤$0.50 · day≤$5.00  ✓ backend: firecracker  ✓ sandbox started (run_id: 76935e132f9be8e9)  ┌─ agent output ────────────────────────  │ https://api.groq.com/      CONNECT allowed  │ https://evil.example.com/  CONNECT refused 403  └───────────────────────────────────────⏸  human gate: agent "invoice-processor" wants to call MCP tool "pay_invoice" on server "accounting"   subject: sha256:4b3f…e91a   approve? [a]pprove / [d]eny (timeout 300s → abort):✓ run finished    exit=0
02 Run · one allowed, one refused
human gate · signed decisionwebhook
  1. POST$HUMAN_GATE_WEBHOOK_URL2xx
    { "request_id": "hg_1864a3f2c07d9e10",
      "tool_call": { "name": "pay_invoice", … },
      "subject_digest": "sha256:4b3f…e91a" }
  2. GET…/hg_1864a3f2c07d9e10/decision200
    { "decision": "approved",
      "signature": "z3xQb…c7f1" }
  • signature · approver_pubkey
  • request_id matches this gate
  • subject_digest matches the call
  • decision == "approved"

forwarded · gate_approved · decided_by: webhook

03 Approve · terminal or signed webhook
invoice-processor-2026-08-08.jsonl
#eventdetailsig
01run_startedfirecracker✓
02network_allowedapi.groq.com · 200✓
03network_blockedevil.example.com · 403✓
04gate_triggeredpay_invoice✓
05gate_approveddecided_by: webhook✓
06tool_callaccounting/pay_invoice✓
07run_finishedexit=0✓

$ constle audit verify ✓ 7 entries · signatures valid · chain intact

04 Prove · signed, hash-chained

03What it enforces

Enforced below the model.

Nine capabilities. Eight ship today. Open a tile for the mechanism and where it stops.

C.01 Shipped

Sandboxed execution

Firecracker microVM or a two-network Docker sandbox. No default gateway.

Mechanism · where it stops
How
Backend auto-detected, or forced with --backend. isolation: is a minimum: kernel selects Firecracker and fails closed if it’s missing.
Stops at
--accept-isolation=<level> lets an operator knowingly put one run on a weaker boundary. It is named and recorded.

C.02 Shipped

Network egress

Every packet goes through a Squid allowlist. Raw IPs are denied.

Mechanism · where it stops
How
Name-based dstdomain matching with reverse lookups off. Loopback, link-local, metadata and private ranges are refused on the resolved address. CONNECT only to 443.
Stops at
DNS for public addresses is trusted: a declared name that resolves to an attacker’s public IP gets through.

C.03 Shipped

Spending limits

Hard per-run and per-day USD caps, metered at the MCP gate.

Mechanism · where it stops
How
Each priced server’s response is metered against its declared pricing. The daily ledger is durable and keyed by DID. A missing usage value kills the run.
Stops at
Traffic through allowed_hosts (direct LLM calls included) is not metered. max_per_month_usd is not enforced.

C.04 Shipped

Human gates

Named MCP tool calls wait for a human: the terminal, or a signed webhook.

Mechanism · where it stops
How
A protocol-aware gate proxy pauses matching tools/calls. A webhook decision must be Ed25519-signed by approver_pubkey; if anything fails to verify, it can only deny.
Stops at
Exact tool-name match only. A terminal approval is unsigned. With no decision, on_timeout decides (default abort).

C.05 Shipped

Audit log

JSONL per agent per UTC day. Signed and hash-chained.

Mechanism · where it stops
How
With identity.did set, every entry is Ed25519-signed. constle audit verify reports the first edited line and re-verifies recorded gate decisions.
Stops at
An intact chain proves the runtime’s account is unaltered, not that it is true.

C.06 Shipped

Cryptographic identity

W3C did:key. The private key never enters the sandbox.

Mechanism · where it stops
How
Key at ~/.constle/identities/<name>/, mode 0600. constle run fails closed if the declared DID has no local key.
Stops at
identity.owner is a label checked for equality, not a cryptographic binding to a person.

C.07 Shipped

Max duration

The agent is stopped when its time is up.

Mechanism · where it stops
How
The host supervisor stops the sandbox at limits.max_duration_seconds and records terminated_by_limit.
Stops at
Wall-clock only. Memory is capped by sandbox.memory_mb; sandbox.disk_mb is declared, not applied.

C.08 Shipped

Agent-to-agent messaging

Signed envelopes, to declared peers only.

Mechanism · where it stops
How
The host signs and verifies. The sandbox does no cryptography and never sees a peer’s real endpoint. There is no discovery, by design.
Stops at
Replay state is per machine: one identity listening on several machines can be replayed once per machine within ±5 minutes.

C.09 Not built

Agent commerce

Agents discovering and paying each other. Exploring, once identity and spending are solid.

04Architecture

The rules live outside the agent.

Every layer runs in the host constle process. Keys, real MCP URLs and peer endpoints never enter the sandbox.

Chokepoints, isolation levels, backends

Layer 2 · Identity & governance

did:key identity, the signed hash-chained audit log, human gates, and the per-run / per-day USD ledger.

internal/identity · audit · mcpgate · spending

05The Agentfile

One YAML file. Every field labelled.

A declared protection must never look real when it isn’t. Tap a line.

Full field reference on docs.constle.dev

06Quickstart

Sixty seconds to a sandboxed agent.

Verified end to end on Linux + Docker against v0.5.0.

Full quickstart & CLI reference
curl -fsSL https://constle.dev/install | sh

or build it (Go 1.26+)

git clone https://github.com/constle/constle && cd constle && go build -o constle ./cmd/constle

CLI

  • run <agentfile>run an agent in a sandbox

    --backend=docker|firecracker forces a backend. --accept-isolation=none|process|network accepts a weaker boundary for this run, and says so.

  • validate <agentfile>check without running

    Prints the resolved isolation and warns about every declared control that won’t be enforced.

  • initscaffold a starter Agentfile

    Writes agent.yaml with sane defaults in the current directory.

  • identity create <name>make an agent DID

    Ed25519 keypair as did:key. --owner=<email> binds a label. identity show prints the DID and key location.

  • webhook-keygen <name>approver keypair for gates

    Not an agent identity: it authenticates the human approving. Paste the DID into human_gates.approver_pubkey.

  • audit verify <logfile>signatures + hash chain

    Re-verifies recorded gate decisions too. --agentfile pins both trust anchors; --did and --approver-pubkey pin them one at a time.

  • ps · stop <run-id>list and stop runs

    Lists running and recent Constle-managed agents; stops one by run ID.

  • versionprint the version

    Bare constle shows the startup screen; --no-animation or CONSTLE_ANIMATION=never skips it.

07Honest by default

Pre-1.0. Five known gaps.

Each is a place where a field looks stronger than the runtime is. Stated here, and in the code.

  1. 1Human gates match tool names exactly

    An entry gates a call only when it byte-exactly matches an MCP tool’s params.name. No wildcards, no semantics. Gates don’t apply to plain HTTPS via allowed_hosts. Unmatched entries are warned about at validate and run.

    pkg/manifest/manifest.go · cmd/constle/gates.go

  2. 2max_per_month_usd isn’t enforced

    Parsed and validated, then ignored with a warning. Per-run and per-day caps are enforced; the daily one durably, keyed by DID.

    pkg/manifest/manifest.go

  3. 3allowed_hosts traffic isn’t metered

    Only the MCP gate meters cost. Metering direct LLM calls would mean TLS-intercepting the agent, and Constle refuses to. An agent that spends over allowed_hosts has no spend enforcement.

    internal/mcpgate/metering.go

  4. 4A2A replay state is per machine

    Seen message IDs persist under ~/.constle/a2a/replay/<did>/ across restarts, and fail closed. They aren’t shared between machines, so one identity listening on several machines can be replayed once per machine within the ±5-minute window.

    internal/a2a/envelope.go · replay_store.go

  5. 5sandbox.network.egress has no consumer

    It parses and defaults to restricted, and nothing reads it. allowed_hosts is the entire network policy; an empty list denies everything.

    cmd/constle/main.go · issue #16

Full explanations with source references

What Constle is not

  • An agent frameworkIt governs the environment, not the logic. LangGraph, CrewAI or hand-rolled code run inside unchanged.
  • A cloud providerIt installs on your infrastructure, any cloud or on-premise.
  • A monitoring overlayIsolation stops exfiltration even if the model is fully compromised.
  • FinishedThe gaps are listed up front rather than found later.
  • A closed platformApache 2.0. The Agentfile format is open and independently auditable.

08Open source

Built in the open, in milestones.

Early and solo-maintained. Every issue and PR gets read.

Shipped

  • Firecracker & Docker sandboxes, proxy-only egress
  • did:key identity, signed hash-chained audit
  • Human gates with an abort-on-timeout default
  • Per-run and per-day spend enforcement
  • Signed A2A to declared peers
  • CLI: run, validate, ps, stop, init, identity, audit verify, webhook-keygen

In progress

  • Deeper conformance testing
  • A field-by-field spec of what each key enforces versus declares
  • Hardening the paths that touch real infrastructure

Exploring

  • Agent-to-agent commerce, once identity and spending are solid. A direction, not a date.

Contribute

git clone https://github.com/constle/constle && cd constle && go test ./...

Most useful now: bug reports with a failing test, a gVisor backend, example agents, spec-vs-parser gaps.

CONTRIBUTING.md

Verify a release

The release workflow signs checksums.txt with cosign keyless, pinned to that workflow. The installer checks the SHA-256 every time, and the signature when cosign is on your PATH.

Verification commands

Report a vulnerability

Please don’t open a public issue. Use GitHub Security Advisories; disclosure is coordinated.

Report privately

Declare it. Enforce it. Prove it.

Install the CLI, validate the example Agentfile, run it in a sandbox.