Profile policy

A ceiling over every agent on this machine. An agent narrows within it and can never widen it — so a profile that says "ask me before any shell command" means every agent asks, including one you installed from the registry and never read.

// profile.config.ts
export default defineProfile({
    policy: {
        tools: {
            search: "escalate",     // ask before any @axon/search tool
        },
        process: {
            run: "escalate",        // every one-shot command asks
            spawn: false,           // long-lived children: off entirely
        },
    },
})

That search: "escalate" is what the rule looks like from the other side. The agent calls a search tool, the call suspends, and you answer — whichever agent it was, whether or not its own config had anything to say about searching:

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

Answer allow always and a grant is written, so that call stops asking. Answer deny — or write false instead of "escalate" — and the agent is told the call was blocked:

A denied call shown as CAPSULE_POLICY_DENIED: search.web denied by policy, with the agent explaining it cannot reach live web results and offering another route

It sits at the top level, beside extensions and settings — never inside them. settings is the set of keys the terminal acts on; policy is read by the runtime. A policy block nested under settings is silently ignored rather than honoured, because a boundary that moves depending on where you wrote it is not a boundary.

Why the profile, not the agent

An agent's own policy block is a statement about what that agent needs. That is the right layer for "this agent only reads ./src" — the author knows. It is the wrong layer for "nothing on my laptop touches ~/.ssh", because it depends on every agent author agreeing with you, forever, including in agents you install without reading.

The ceiling is resolved in the kernel, at the one seam every consumer passes through: the TUI, axon dev, axon run in a script, a test harness. A ceiling applied only on the terminal's spawn path would be advisory, and the CLI is exactly where someone scripting would step around it without meaning to.

The two layers

Both layers keep their rules. They are not merged into one — the pair is carried down and evaluated per call, so a verdict can always name which layer produced it.

That split matters more than it looks. Merging two glob rules is where a ceiling springs a leak: a profile allowing git status unioned with an agent allowing git push would permit a command that neither layer permits alone. Composing the shape and deferring the verdict is what makes that impossible.

The stricter layer wins, ranked deny > escalate > allow. On a tie the profile is reported as the source, because telling you the agent denied something your profile also denies sends you to the wrong file to fix it.

Profile saysAgent saysResult
falseanythingdenied — a bare profile denial is final
"escalate"anything but falseasks — the agent cannot skip the prompt
trueits own rulethe agent's rule stands
(nothing)its own rulethe agent's rule stands
glob ruleglob ruleboth evaluated; stricter verdict wins

A profile grant is a ceiling, not a grant: true removes a constraint you might have imposed, it does not hand an agent a capability its own policy withholds.

Rule shapes

Any capability takes one of four forms:

tools: {
    github: true,           // allow
    payments: false,        // deny — final if written at the profile
    search: "escalate",     // ask, every time
    shell: {                // glob rule
        allow:    ["git *"],
        escalate: ["docker *"],
        deny:     ["rm -rf *"],
    },
}

Inside a glob rule, deny beats escalate beats allow regardless of the order you write them. A consequence worth knowing: an escalate: ["**"] catch-all alongside an allowlist makes the allowlist unreachable — everything matches the catch-all first. If you mean "ask me about all of it", write "escalate" on its own.

A glob rule that is declared but matches nothing is a denial, not silence. Naming what is permitted and hitting none of it is an answer.

Rules key on the namespace a module registers its tools under, not on individual functions — search: "escalate" covers every tool @axon/search brings.

fs, limits and isolation

Three keys behave differently, because they are OS-layer facts rather than per-call decisions:

fs and limits are enforced by bind mounts and cgroups, which the mediator never sees and which cannot express an intersection of globs. When the profile declares either, the profile's value is taken outright — a ceiling a bind mount ignores is not a ceiling.

isolation is a tier, not a rule, so it resolves as a maximum: none < auto < hardened. A profile asking for hardened cannot be loosened by an agent, but an agent may harden beyond its profile.

Declaring any of fs, network or limits at the profile also opts every agent on the machine into rootless confinement, even one that declares no policy of its own. Setting nothing at all means full machine access — the deliberate default, so that a new user is not fighting a sandbox before they have an agent worth confining.

When the config is broken

A profile that fails to parse yields no ceiling rather than refusing to boot. It fails open, matching the rest of the profile: the config loader reports the breakage where you can see it, and a ceiling that made a typo unbootable would strand you inside the very file you need to edit.

Worth knowing if you rely on the ceiling for something that matters — it is a safety rail for your own machine, not an adversarial sandbox. The confinement layer is what contains a hostile agent; see policy for what the capsule enforces.

See also

policy — the agent-level block, and what each key means.

Kernel & Policy — how enforcement works, and where the mediator sits.

profile.config.ts — the rest of the file.