Skip to content

Entry Approval

enter_from says which zones may enter a zone. Approval decides whether a particular entry happens — this command, from this chain, in this directory, now.

It takes two halves, in two files, because they belong to two different people:

zones.toml — the policy says approval is needed
[zones.release]
profile = "release"
enter_from = ["work"]
requires_approval = true
/etc/wardcell/config.toml — the machine says how to ask
[plugin]
path = "/usr/local/libexec/wardcell/wardcell-plugin"

The policy root is the artifact an organisation distributes and versions; the chat integration or ticket system behind an approval is a property of the fleet it is deployed to. Split this way, one policy root serves machines wired to different approvers.

One plugin serves every gated zone — the question names its target, so a plugin that holds different zones to different rules does that itself. A machine not set up to ask does not start the run at all: a policy root with a gated zone and no plugin that implements approvals.zone fails at startup, rather than running and refusing every entry it was asked about.

The plugin is asked after enter_from has already allowed the entry. So it can refuse entries the policy permits, and it can never permit one the policy does not. A plugin that approves everything leaves the policy exactly as it was — which is what makes it safe to add to a zone that is already correct.

An approval that goes to a human comes back when it comes back, so a question and its answer are separate messages and nothing holds a process open in between. approval_timeout is optional:

[zones.release]
requires_approval = true
approval_timeout = "4h" # omit it and the entry waits as long as it takes

Omitted, the entry waits indefinitely. That is the right default for a release gate and a real cost to understand: the sandbox, its entry lock and the user’s terminal are held for the whole wait, and that sandbox cannot enter another zone meanwhile. Ctrl-C still works, and the wait ends immediately if the plugin crashes or the run ends.

An agent that needs a gated zone needs it more than once, and a person asked the same question six times stops reading it. So an approval says how far it reaches — the two buttons a chat prompt would offer:

oncethis entry only. The default
alwaysthis entry and every later entry into the same zone with a command line the approval covers, for the rest of the run. The plugin is not asked again

A standing approval is keyed on the target zone and on the words the command line must start with. By default those words are the whole command line that was approved, so approving npm publish into release does not approve npm install into release, and does not approve anything into staging.

The plugin may say it covers fewer words — ["npm", "install"] for an entry running npm install left-pad@1.1, so the next package is not another question. It can only shorten the command line it was asked about, never name a different one, so an approval can widen as far as “any npm command in this zone” and never reach a program nobody was asked about. A chat prompt with three buttons is the shape this is for — Approve once, Always npm install …, Always npm … — so what someone grants is what they were shown. Which split makes sense is a question about the tool being run, which is why wardcell does not guess it: the plugin says so in covers.

It is not keyed on which zone the request came from, because enter_from has already permitted every chain that can reach the zone — keying on that as well would narrow it to whichever escalation path happened to ask first. The directory cannot vary: it is where the run was started.

wardcell remembers it, and only for the life of the run. Nothing is written to disk, so a later run asks afresh and there is no stored permission to go stale. Every entry is still audited, with "standing": true on the lines that were not asked about, alongside the request_id, by, reference and covers of the approval they rest on.

There is no refusing “always”. A refusal that stood would turn one mistaken click into a run that cannot enter the zone at all, with nothing to undo it; a policy that should never allow an entry says so in enter_from, where it outlives the run.

$ wardcell zone enter release -- npm publish
error: entering zone 'release' was not approved: not during the release freeze

Exit code 1, the same as a policy denial — but the message is different on purpose. A denial means asking again will not help; a refusal means it might.

A plugin that will not start, a connection that drops, an unreadable answer, an approval_timeout that passes, or no plugin configured at all: all refuse.

This is the whole design. An approval gate that opened whenever the thing guarding it was broken would be worse than no gate, because the organisation would believe it had one.

An entry into a gated zone is an audit.decision event in the run’s log, recording what was asked and what came back alongside the decision:

{"ts":"2026-10-08T14:22:03.512Z","run":"9f2c…","level":"info","source":"supervisor","category":"audit","event":"audit.decision","msg":"release: allowed","data":{"attempt_id":0,"calling_zone":"work","chain":["work"],"target_zone":"release","command":["npm","publish"],"decision":"allowed","approval":{"request_id":"9f2c…-0","approved":true,"by":"alice@example.com","reason":null,"standing":false,"reference":"slack:C0ABCDEF/p1700"}}}

Read a run’s decisions on their own with wardcell logs show <id> --category audit.

The decision is allowed, denied (enter_from said no, and nothing was asked) or refused (the approval said no). reference is the plugin’s own name for the request — the thread or ticket an auditor would go and read. standing is true when the plugin was not asked at all, because an earlier approval in this run said it covers this zone and command; the other fields then repeat that approval, so the line is still attributable to the person who gave it. Where two standing approvals both reach an entry, it is recorded against the narrower one — “alice approved every npm install” is a truer account of why an entry happened than “bob approved everything”. For a zone that does not require approval, approval is null.

An approval plugin does not have to ask anyone — it has to decide, and a rule about office hours or a command allowlist needs no human in it at all.

Start from an example, in Rust or Go, which implements the protocol and leaves you one function. Approval Plugins is the protocol, the waiting rules and the failure modes in full.