Docs Getting started
Known limitations
Five gaps, all documented and deliberate rather than discovered later. Each one is a case where a manifest field looks stronger than the runtime currently is, and each is stated in the code at the point where it matters.
On this page
Important
Read these before you rely on anything else in these docs.
| # | Limitation | In short |
|---|---|---|
| 1 | Human gates match MCP tool names by exact string | No wildcards, no semantic matching, and no gating of plain HTTPS through allowed_hosts. |
| 2 | max_per_month_usd is parsed but not enforced |
Only max_per_run_usd and max_per_day_usd are. |
| 3 | Traffic through allowed_hosts is not metered |
Only the MCP gate meters cost. |
| 4 | A2A replay state is per machine | Durable across restarts, not shared between machines. |
| 5 | sandbox.network.egress has no consumer |
allowed_hosts is the entire network policy. |
1. Human gates match MCP tool names by exact string, and nothing elseLink to this section
human_gates.require_approval_for gates a call when an entry is a byte-exact, case-sensitive match for the params.name of a tools/call request on a server declared under mcp.servers. The tool name is the only protocol-level identifier the gate proxy sees, and exact match is the only mapping that is deterministic and auditable: there is no semantic matching, no prefix matching, no wildcards.
What this means for you: an entry like external_transfer gates nothing unless an MCP server actually exposes a tool named exactly external_transfer. Constle warns about every unmatched entry at both validate and run time, so an unenforceable gate is loud rather than silent, but it is still unenforceable. Human gates also do not apply to plain HTTPS traffic through allowed_hosts; the gate proxy only sees MCP.
Source: pkg/manifest/manifest.go (HumanGates.RequireApprovalFor, "MAPPING CONTRACT"), cmd/constle/gates.go.
2. max_per_month_usd is parsed but not enforcedLink to this section
The field is accepted by the parser and validated as a decimal amount. Nothing enforces it. Declaring it produces an explicit warning and no monthly ledger exists. max_per_run_usd and max_per_day_usd are enforced (the daily one durably, across runs, keyed by DID).
Source: pkg/manifest/manifest.go (Spending.MaxPerMonthUSD).
3. Traffic through allowed_hosts is not metered for spendingLink to this section
Cost is metered only at the MCP gate proxy, against the pricing block a server declares. Ordinary HTTPS to a host in network.allowed_hosts, including every direct call to an LLM API, is allowlisted, logged and not counted toward any spending cap.
This is a deliberate privacy trade-off, not an oversight: metering that traffic would require Constle to TLS-intercept the agent's connections and read their contents, and Constle refuses to do that. The consequence is real and you should size it: an agent that spends money over allowed_hosts rather than through a priced MCP server has no spending enforcement at all. That is exactly the case the quickstart's example hits, and why it prints NOT ENFORCED.
Source: pkg/manifest/manifest.go (Spending, "Enforcement scope"), internal/mcpgate/metering.go.
4. A2A replay state is per machine, not shared between machinesLink to this section
The A2A listener rejects duplicate msg_ids and envelopes whose timestamp drifts more than ±5 minutes from the local clock. The set of seen message IDs is durable: every accepted id is persisted under ~/.constle/a2a/replay/<did>/, so the check spans process restarts and concurrent runs of the same identity, and fails closed (a retryable 503) if that state cannot be read or written. What it does not span is machines: the state lives in the invoking user's home and is not replicated anywhere.
What this means: if you run the same identity as a listener on more than one machine, an envelope captured in flight can be replayed once per machine, provided each replay lands inside the 5-minute timestamp window. One listening machine per identity, the normal deployment, has no such exposure.
Why this differs from the README's short list
The README's one-line summary of this limitation still calls the replay guard "in-memory and per-run". The detailed section, the A2A specification and internal/a2a/replay_store.go all describe the durable, per-machine store above, and that is what the code does.
Source: internal/a2a/envelope.go (replayGuard), internal/a2a/replay_store.go.
5. sandbox.network.egress is declared but has no consumerLink to this section
The field parses, validates and defaults to restricted, and then nothing reads it. All egress enforcement is derived solely from network.allowed_hosts, which becomes the Squid dstdomain allowlist. An empty list denies everything.
So egress: open and egress: none both parse cleanly, change nothing about what the agent can reach, and still render as restricted in the run summary. This is the one gap in this list that is a declared policy which looks real and is not, which is precisely what the warnings in items 1 and 2 exist to prevent elsewhere. Fixing it means deciding what egress: open should do, not just what it should print. Until that decision is made, the display label is deliberately not derived from the field, because deriving it would make the output honest about a value the runtime still ignores.
Until then: treat allowed_hosts as the entire network policy. It is. An empty or absent allowed_hosts is your "deny all"; egress is documentation.
Source: cmd/constle/main.go (renderRunSummary, "KNOWN GAP"), recorded in #16.
Something wrong or unclear? Open an issue. The specifications on these pages are copies of spec/ in constle/constle.