How it works
Inside the agent, tools are handles, never raw secrets. When the model callscards.charge($48):
- The runtime shim sends the call to the Naïve gateway — not to the card processor.
- The gateway authenticates the agent profile’s scoped token and loads policy, budget, balance.
- Policy check: allowed? under cap? crosses the approval threshold?
- Allowed → the gateway (which holds the real issuing credential, never the agent) calls the processor, records the action, and decrements the budget.
- Needs approval → the action is parked, the approver is notified, and it returns
pending; it executes on approve. - Denied → one clean, structured denial, logged. No retry storm.
403 budget_exceeded; soft caps route to approval; exhausting a hard cap or the
company’s credit balance auto-stops the agent profile’s hosted runtime. Each agent profile
holds its own scoped key(s) — list / rotate / revoke them per agent profile
(/v1/agent-profiles/:id/keys) without touching the company key.
The cap binds on both transports: the scope it is enforced inside is bound around every
HTTP request and around every MCP tool call. What differs between the two is the
breadth of the primitive gate and of the capability/approval verdict — see
MCP binds the subject policy, but a much smaller kit gate.
Why it can’t be bypassed
The agent holds a per-agent profile scoped key (sealed to the agent profile’stenant_user at provision time), never the company key and never the underlying
provider credentials (card issuer, formation, KYC, email, SMS) — those live behind
the gateway in the encrypted vault. So the agent can only ever ask; the gateway
decides, executes, and records. revoke() suspends the agent profile, releases its
runtime slot, and cascades to sub-agents. Sub-agent kits are scoped-down
derivations of the parent (capabilities can only narrow; budget is shared), closing
the confused-deputy hole.
revoke() is absolute for anything that acts, and HTTP reads are the exception: a
revoked profile’s key is refused 403 agent_profile_revoked on every mutating HTTP
call and on every MCP tool call, but assertAgentProfileActive runs only for
MUTATING_METHODS, so a GET is decided as though the profile were active. Plan a
revocation as “stop it acting”, and revoke or rotate the key as well when you need it
to stop seeing.
Self-hosting the runtime does not bypass it, because governance is at the tool-call
boundary, not inside the runtime: a BYO-runtime agent holding a scoped key hits the
same subject policy as a Naïve-hosted one, on either transport. What it does not get on MCP is the full breadth of the kit’s primitive gate
— see below.
Where the kit binds — and the two places it does not
The AccountKit binds to the resolved subject of a request (subject resolution). It therefore governs:- callers on the per-agent-profile mounts (
/v1/users/:user_id/...), and - callers authenticating with a key sealed to a profile (
active_tenant_user_id).
The second place: MCP binds the subject policy, but a much smaller kit gate
The MCP dispatcher resolves the same subject the HTTP chain does, and — as of this revision — applies the same two subject-policy steps around every tool call: the revocation check and the agent-profile budget scope. What it does not apply at the same breadth is the kit’s primitive gate, and the governor’s capability/approval verdict reaches only the tools that ask for it.
Revoke is stricter here than on HTTP, deliberately. An HTTP request has a method, so
the chain can refuse writes and leave reads observable. A tool call has no method, and
readOnlyHint is declared for only 14 of 271 tools — a partition that would let 9
through and refuse 262 is a coin toss, not a rule. So a revoked or suspended profile is
refused every tool call on this transport. That is a tightening, and it means the
HTTP-side carve-out is the weaker of the two: revocation still does not gate HTTP
reads. A GET from a revoked profile’s key is never asked the question at all — it is
decided by the primitive gate as though the profile were active.
The primitive gate is where MCP is genuinely thinner: 222 of the 271 tools assert no
kit primitive at all, so switching a primitive off in the AccountKit stops 49 tools and
not the other 222. And agent({ can }) / limits.approve are enforced on MCP only
where a handler calls mcpGuard — the card, trading, formation, verification,
domain-purchase, phone/mobile provisioning, compute, browser-signup,
connections-connect and send-email verbs, plus the three destructive brain verbs. The
approval guarantee holds for the sensitive money-and-identity actions, but a capability
rule about, say, search or image generation is not consulted on this transport.
Which levers work on an open MCP session. The subject and the kit are re-read on
every call, so revoking the profile, switching a primitive off, or a capability deny all
take effect immediately — no reconnect needed. Revoking or rotating the key blocks new
sessions and any message that re-presents Authorization, but the AuthContext is
resolved once at SSE connect and cached for the life of the connection, so a client that
sends the header only on connect keeps its session until it disconnects. Tool listings
are a connect-time snapshot for the same reason: a revoked profile is still offered all
271 tools and refused every one of them — see the MCP tool list.
Before the one-tail dispatcher landed, the MCP layer applied neither the revocation
check nor the budget scope. If you are running an older build, treat revoke and
limits.budget as HTTP-only controls there.
How it maps to the platform
Native observability is required for governance, so the control plane already
records every tool call, spend, and decision — and exports it with no extra module.
Two planes, one policy
You declare one agent’scan / limits; the control plane routes it to two planes:
- Business-action governance (we own this) — budgets, spend caps (enforced at authorization), HITL approvals, capability allow/deny, audit, instant revoke — at the tool-call boundary, across the whole agent profile bundle. Caps and revoke bind on both transports; capability allow/deny and approvals bind on every HTTP primitive route and on the 28 guarded MCP tools.
- System governance — the runtime container’s network / filesystem / process isolation. A different layer with different failure modes; we do not claim to replace it.
Native observability + the native-vs-module rule
The control plane already records every tool call, spend, and decision — it has to, to govern. The rule for any cross-cutting concern: does the control plane need it to govern? Yes → native (tracing, secrets, identity). No → a module. Read the audit trail over the API: there is no telemetry exporter or third-party observability integration in the shipped platform today. What exists is the activity log (GET /v1/users/:user_id/logs, plus the
cross-tenant GET /v1/logs) and the SSE stream at GET /v1/events.