Kernel & Policy

An agent runs as its own OS process, and everything it does happens inside that one box. The kernel — the trusted core of the runtime — owns the machine-facing operations. Agent code runs in the capsule, the mediated scope inside the same process: every tool call and every model-emitted block passes through the kernel, which checks it against your policy before anything executes.

That is the security model in one sentence: the capsule can request; only the kernel can take. The model is treated as untrusted — full stop.

Most "agent security" is a system prompt asking the model to behave — instructions it can ignore, forget, or be talked out of. Axon's is not. Your policy is enforced by the runtime on every platform, and on Linux by the operating system itself. The model can want whatever it wants; the privileges are not there.

Two layers, one policy

The same policy block drives both. They differ in what they can express and how absolutely they hold.

The mediator runs everywhere. It gates every declared tool call and every process.run/process.spawn by glob, returning typed denials, raising escalations, and emitting a span for each decision. It is the layer that can ask you — an OS wall cannot pause and request approval.

OS confinement runs on Linux, and it is the wall. fs becomes mount-namespace bind mounts, so a path you did not grant does not exist inside the box — a forbidden read fails as "no such file", not "permission denied". net becomes a network namespace with a default-drop nftables ruleset, so a raw socket to an ungranted address is dropped by the kernel rather than by a check in front of it. limits becomes cgroup caps. env becomes an environment built from nothing rather than inherited. Where the two overlap, the OS layer is the truth and the mediator is the polite error in front of it.

The box wraps the whole agent process and everything it spawns:

systemd-run --scope <limits> -- bwrap <box> -- bun run <agent>

The cgroup is outermost, so resource limits cover the process tree — a subprocess cannot multiply the budget by spawning helpers. Bubblewrap holds the filesystem, pid and network isolation.

macOS and Windows get the mediator only; the namespaces do not exist there, so fs, net, env and limits are unenforced. That is a real reduction rather than a formality. If you want the full posture, run on Linux — your cloud deployment already does.

Policy is what you tell the kernel to allow

Declared once in axon.config.ts. The kernel applies it at every enforcement layer available on the host system.

export default defineAgent({
    policy: {
        isolation: "auto",
        fs: {
            read:  ["./src", "./package.json"],
            write: ["./output"],
        },
        net: {
            allow: ["api.github.com:443"],
            dns:   "allowlist",
        },
        shell: {
            allow: ["git", "bun"],
            args:  { git: { deny: ["push --force*"] } },
            raw:   false,
            spawn: false,
        },
        env: { allow: ["GITHUB_TOKEN"] },
        tools: {
            fs: { read: true, remove: false },
            "*": "escalate",
        },
        limits: { memory: "2G", cpu: "100%", wall: "30m" },
    },
})

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

No policy block means unrestricted access, not denied access. That is deliberate for local development: an agent on your own machine, with no policy, is a personal tool with your privileges. For anything deployed or published, declare explicit rules.

Setting any fs, net, env or limits rule turns the OS box on by default — isolation becomes "auto" rather than "none". You rarely set it by hand.

Per-invocation narrowing

The base policy applies to every invocation. Individual calls can narrow it further — the standard move for routes handling untrusted input:

const result = await axon.request({
    prompt,
    policy: {
        fs:    { read: [], write: [] },
        shell: false,
    },
})

Narrowing can only restrict. A call can never grant access beyond the base policy, and the narrowed policy clears when the invocation completes.

Escalation

For calls that need a human decision, escalate pauses execution and surfaces the call:

policy: {
    shell: {
        allow: ["git"],
        args:  { git: { escalate: ["push*"] } },
    },
}

The TUI shows the call, the arguments it was made with, and waits:

The terminal suspended on a search.web call, showing the query and three choices: allow once, allow always, and deny

Approving once settles this call; approving always writes a grant so the same call stops asking. Headless, with no TUI attached, an unresolved escalation fails closed after the timeout (default 30 seconds).

What the agent sees

A blocked call comes back as a structured policy error — the agent doesn't see the rules themselves. A well-written agent explains what it tried and either takes another approach or tells you it needs broader access.

A denied call shown inline as CAPSULE_POLICY_DENIED: search.web denied by policy, followed by the agent explaining it cannot reach live web results and asking how else it can help

The denial is an ordinary error the agent reasons about, not a crash — which is why the conversation continues rather than ending on a stack trace.

What this does and does not protect against

Protects against:

  • Agent-emitted code touching the filesystem, network, environment, or shell outside declared policy
  • Per-invocation policy violations in routes handling untrusted input
  • Runaway resource use — cgroup caps cover the process tree, so spawning helpers cannot multiply the budget
  • On Linux: even a mediator bypass hits the OS wall. A path outside fs does not exist inside the box, and a host outside net is unreachable at the namespace level

Does not protect against:

  • A tool with legitimate network access exfiltrating data through an allowed endpoint. Policy gates which destinations can be reached, not what flows through them.
  • A tool crashing the agent. Tools run in the agent's own process, so a tool that calls process.exit() ends the agent — the blast radius is one agent and one conversation, and the supervisor records it, but the process does go.
  • Deliberate exploitation of the runtime itself. Trust your tool implementations.

For the full policy field reference, see Policy.