Agent policy

The rules your organisation writes about agents: gate policy for registered actions, and per-agent permissions and standing rules for requests.

What an agent may do without asking, and who must agree when it does ask, is decided by rules your organisation writes, not by the agent. There are two sets, because they govern two different things:

Gate policyPermissions and standing rules
GovernsCalls to the gate: registered actions with validated inputApprovals, reviews, requests for information and publishes an agent raises
Matches onAction, agent, environment and input valuesAgent, kind of request and a risk floor
CanAllow, refuse, or send to a personRefuse, or require more people
Written withThe CLI or the REST APIAdmin → Agents and Admin → Policies

Gate policy

A gate policy is a named set of rules. Each rule has a name, an effect of allow, deny, require_human, a priority (lower decides first; 100 when omitted), a match, an optional explanation shown to whoever was stopped, and for require_human optional params.

deliverd.policy.json

{
  "name": "Refunds",
  "rules": [
    {
      "name": "Large refunds need two people",
      "priority": 10,
      "effect": "require_human",
      "match": {
        "actions": ["finance.refund"],
        "input": { "amount": { "gt": 1000 } }
      },
      "params": { "requiredApprovals": 2, "approvers": ["finance-leads"] },
      "explanation": "Over £1000 two finance leads sign it off."
    },
    {
      "name": "Small refunds go through",
      "priority": 20,
      "effect": "allow",
      "match": {
        "actions": ["finance.refund"],
        "input": { "amount": { "lte": 50 } }
      }
    }
  ]
}
match.actions
Action ids, exact (finance.refund) or a family (finance.*). A family also catches an action added to it later.
match.agentIds
Only requests from these agents.
match.environments
Only requests declaring one of these environments, so a rule can tell staging from production.
match.input
Conditions on the action's input, keyed by field, each one of eq, ne, gt, gte, lt, lte, in. Structured rather than parsed from strings like "> 1000", whose failures would be silent.
params.requiredApprovals
How many people must agree.
params.approvers
Who decides: people, groups or workspaces, in the same words an approval's approvers takes.
params.expiresInMinutes
How long the resulting approval stays open.
params.escalateAfterMinutes
When an unanswered request is widened.

The first decisive rule by priority wins, so an allow above a deny is how an exception is written. Where several rules ask for a person, the tightest constraint applies and their named approvers are combined. No matching rule means a person decides.

From a terminal

deliverd actions push --file deliverd.actions.json
deliverd policy push --file deliverd.policy.json --activate
deliverd policy simulate --action finance.refund \
  --input '{"amount":1240}'

actions push registers every action in the file and is meant to run on deploy. policy push stores the file as a new version and only puts it in force with --activate; without it the version is a draft, so the sequence that reviews well is push, simulate, activate. deliverd policy list shows each policy and the version in force, and deliverd policy activate puts a stored version in force. See CLI.

Over REST

CallScopeDoes
POST /api/v1/policiespolicies:writeCreate a policy with its first version; activate: true puts it in force. A second policy of the same name is 409 duplicate.
GET /api/v1/policiespolicies:readThe policies, and which version of each is in force.
POST /api/v1/policies/{id}/versionspolicies:writeStore a complete new rule set as the next version.
POST /api/v1/policies/{id}/activatepolicies:writePut a version in force, superseding the one that was.
POST /api/v1/policies/simulatepolicies:readWhat a request would meet, recording nothing.

What will surprise you

Two more scopes, and one of them does not decide
actions:write registers an action, policies:write authors a rule set, and both are already on a user key. The scope is not the check, though: the service reads your role on every call, so a member's key carrying policies:write is refused exactly as if it did not carry it. Owner or administrator, or a 403.
A rule naming a field no action declares is refused when you write it
Not at evaluation, where it would simply match nothing and leave an administrator believing a control exists. The policy is checked against the registered actions it applies to, and the error names the field. This is the reason actions carry schemas at all.
Simulate across policies, not within one
POST /v1/policies/simulate takes a request you describe and runs the real evaluation over every active version at once, because that is what a real call meets: the first decisive rule by priority wins, and it may well belong to a different policy. A per-policy simulator would report your rule firing when a higher-priority deny elsewhere is what actually answers.
An agent key is refused on both
The mirror of the gate above. An agent may submit to a rule and may not write one, and it may not name the actions the rules are written against either — an agent that can mint finance.refund_v2 has named its way around every rule written for refunds.
  • A version that has been in force can never be edited. Change the rules by storing a new version; every decision records the version it was made under.
  • A badly written rule set is 400 invalid_rules, with a problems list naming each fault. invalid_policy is different: the server could not read or store the policy, and it answers 500.
  • There is no MCP tool for any of this, on purpose. An agent that could write its own rules would be deciding for itself whether it needs a person.

Agent permissions

Each agent identity has four permissions — publish, collect, review and approve — saying which of those it may ask a person for at all. They are separate from its key's scopes: a scope is what a key may call, a permission is what the agent behind it may put in front of a person, and an administrator can withdraw one on the agent's page under Admin → Agents without reissuing the key.

A permission nobody has set means allowed. A withdrawn one is refused with 403 action_not_permitted. Withdrawing permissions is how to stop an agent; deleting the agent instead makes every call its key makes fail with 404 agent_not_found.

Standing rules

Standing rules, under Admin → Policies, decide how a request is handled before anybody sees it. Each matches on an agent, a kind of request and a risk floor — all optional, each meaning "any" when unset — and does one of two things:

Escalate
More people must agree, and optionally named approvers are added to whoever the requester asked. Added, never substituted. An escalation can carry a deadline between five minutes and fourteen days: it then waits, and brings its approvers in only once the request has gone that long unanswered.
Refuse
The request is refused with 403 refused_by_rule, carrying the rule's explanation.
  • Refuse beats escalate, whatever order the rules were written in. Among escalations the strictest count wins and the named approvers are combined.
  • Automatic widening happens once per request, whatever rules match.
  • requiredApprovals is fixed when the request is made, so a rule changed while people are deciding cannot move the bar under them.
  • One rejection settles a request, however many approvals it needed.