Capsule & Policy

When the agent decides to call a tool, execution crosses a process boundary into a separate subprocess — the capsule. The tool runs there. The result comes back over an IPC channel. The agent process never executes tool code directly.

This is not a guardrail layered on top of the architecture. It is the architecture.

Why a separate process

Two reasons.

Tool failures are contained. A tool that throws, leaks memory, calls process.exit(), or crashes entirely exits the capsule subprocess — not the agent. The runtime detects the failure, surfaces it as a structured error, and the agent continues. A broken tool cannot take down the agent that called it.

Policy enforcement is structural. Policy is checked inside the capsule before each tool function executes. There is no code path from the agent to the filesystem, network, or shell that bypasses this gate. The model cannot override its own constraints — not because it's been instructed not to, but because it has no mechanism to do so. The agent process simply doesn't have access to those primitives.

How policy gets there

Policy is declared in axon.config.ts and serialized into the capsule subprocess environment at boot time, before any tool code loads. There is no runtime communication channel for policy — the capsule owns it from the moment it starts.

export default defineAgent({
    policy: {
        fs: {
            read:  ["./src/**", "./package.json"],
            write: ["./src/**"],
            deny:  [".env", "**/node_modules/**"],
        },
        network: {
            allow: ["api.github.com"],
        },
        proc: {
            allow: ["git *", "bun test", "bun run *"],
            deny:  ["git push --force*"],
        },
    },
})

A call that violates policy is rejected before the function runs. The agent receives a structured error — not the policy rules — and adapts from there.

No policy block means unrestricted access, not denied access. No fs block: the capsule can read and write anything. No network block: unrestricted outbound. This is intentional for local development. For anything deployed or published, declare explicit rules.

Per-invocation narrowing

The base policy in axon.config.ts applies to every invocation. Individual calls can narrow it further — useful when a script or route handles less-trusted input.

const result = await axon.request({
    prompt,
    policy: {
        fs: { read: false, write: false },
        network: { allow: ["api.stripe.com"] },
        proc: { allow: [] },
    },
})

Narrowing can only restrict. A call cannot grant access to something the base policy already denies. The narrowed policy is cleared when the invocation completes.

Escalation

For calls that require a runtime decision — where the right answer depends on context rather than a static rule — the escalate value pauses execution and surfaces the call for human review.

export default defineAgent({
    policy: {
        proc: {
            allow: ["git *"],
            escalate: ["git push*"],
        },
    },
})

When a call matches an escalate rule, the TUI shows the call details — function, module, arguments — and waits for approval or denial. If approved, the call proceeds. If denied, the agent receives a rejection and continues from there.

In headless execution with no TUI attached, an unresolved escalation fails closed. The call is denied automatically after the escalation timeout (default 30 seconds).

What the agent sees

When a tool call is blocked, the agent receives a structured error indicating the call was rejected by policy. It does not see the policy rules themselves. A well-written agent will explain what it tried to do and either attempt a different approach or tell you it needs broader access.

The capsule's full behaviour — what it can do, what it can't, what requires escalation — is declared in source and committed to git. Anyone reading the agent folder can see exactly what the agent is allowed to do.

What this does and does not protect against

Protects against:

  • Tool failures crashing the agent runtime
  • The model calling filesystem, network, or shell APIs outside declared policy
  • A bad tool implementation calling process.exit() and killing the agent
  • Per-invocation policy violations in routes handling untrusted input

Does not protect against:

  • A tool with legitimate network access exfiltrating data through an allowed endpoint. Policy gates which APIs can be called, not what data flows through them.
  • Syscall-level escapes. The capsule is a Bun subprocess — it runs with the same OS permissions as the user who started it. There are no kernel namespaces, no seccomp filters, no filesystem mount isolation. This is not a sandbox in the container or VM sense.
  • Resource exhaustion before limits trigger. Memory watchdog and resource counters are best-effort, not hard kernel limits.

The capsule provides a meaningful, practical execution boundary for the common threat model — unintended tool behaviour, policy violations, and failure isolation. It is not designed to contain a deliberately malicious subprocess. Trust your tool implementations.

For the full policy field reference, see Policy.