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 policy | Permissions and standing rules | |
|---|---|---|
| Governs | Calls to the gate: registered actions with validated input | Approvals, reviews, requests for information and publishes an agent raises |
| Matches on | Action, agent, environment and input values | Agent, kind of request and a risk floor |
| Can | Allow, refuse, or send to a person | Refuse, or require more people |
| Written with | The CLI or the REST API | Admin → 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
approverstakes. 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
| Call | Scope | Does |
|---|---|---|
POST /api/v1/policies | policies:write | Create 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/policies | policies:read | The policies, and which version of each is in force. |
POST /api/v1/policies/{id}/versions | policies:write | Store a complete new rule set as the next version. |
POST /api/v1/policies/{id}/activate | policies:write | Put a version in force, superseding the one that was. |
POST /api/v1/policies/simulate | policies:read | What 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 aproblemslist naming each fault.invalid_policyis different: the server could not read or store the policy, and it answers500. - 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.
requiredApprovalsis 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.