Concepts
Mandates
A mandate is a standing authority granted to one agent. It is written for a person to read and enforced by a machine, and it is signed, so nobody can edit it in the database without the signature failing.
The same object you see in the console is the one every action is checked against. An agent with no mandate for an action type gets deny with no_mandate: there is no default allowance.
| Kind | Governs | Example |
|---|---|---|
payment | spending money | supplier invoices up to £2,000 each, £10,000 a month |
data | releasing fields from the vault | address and email to *.nhs.uk for booking |
action | anything else consequential | tool.call and email.send only to acme.com |
#The object
{
"id": "mdt_91c3e0b2",
"agentId": "agt_4f2c91a7",
"kind": "payment",
"title": "Weekly groceries",
"status": "active",
"expiresAt": null,
"createdBy": "you@example.com",
"signature": "base64url Ed25519 over the canonical mandate",
"currency": "GBP",
"perTransaction": 15000,
"perPeriod": 60000,
"period": "week",
"merchants": { "allow": ["market.example", "grocer.example"], "block": [] },
"categories": ["groceries"],
"approveAbove": 8000,
"newMerchant": "approve"
}status is active, revoked or expired. Amounts are minor units of currency. A change is a new signature and a ledger record naming who made it.
#Payment mandates
| Field | Meaning |
|---|---|
currency | the only currency covered; anything else is currency_mismatch, a deny |
perTransaction | the most one action may spend |
perPeriod, period | the most all actions under this mandate may spend per day, week or month |
merchants.allow | domains the agent may pay without further question |
merchants.block | domains it may never pay |
approveAbove | ask a person above this amount, even inside the limits |
newMerchant | for a merchant not allow-listed and never seen: approve (ask), allow or deny |
Breaching perTransaction or perPeriod is a deny, not a question. To be asked instead, set approveAbove below the limit and keep the limit as the ceiling nobody can talk past.
The workspace also keeps one allowance and one never-pay list across every mandate, under Settings: a payee on the never-pay list is refused whichever mandate an agent cites.
#Data mandates and the vault
The vault holds a person’s fields (name, address, email, passport number) sealed at rest. An agent never holds them: it asks for a release, and on allow the values come back once, in released, for that recipient only.
| Field | Meaning |
|---|---|
fields | vault fields this agent may release, for example address, email |
recipients | domains it may release them to; *.nhs.uk covers subdomains |
purposes | why, for example booking; recorded with each release |
A release outside recipients is recipient_not_allowed, a deny. Restricted fields (passport, national_id, bank_account, card, health) always need a person unless the mandate names that field and that recipient. A mandate for passport to * does not count.
#Action mandates
| Field | Meaning |
|---|---|
actions | action types this agent may take: tool.call, email.send, calendar.write, account.change or your own |
domains | where those actions may reach, for example github.com, acme.com |
newDomain | approve: a destination not in domains asks a person; deny: it is refused. Left out, a list of domains is closed and no list means any destination. An agent added as Something else starts with tool.* and newDomain: approve: anything that reaches another site asks a person |
readOnly | allow: a provably read-only tool call (Claude Code’s Read, Glob, Grep and LS, or one Bash command such as ls, cat, pwd, which, git status, git diff or git log with plain arguments: no pipes, redirection, subshells or separators, and no secrets file) goes ahead without a person even while the agent is new, and is still decided and recorded; ask, or left out: the agent’s standing decides, so a new agent asks. Anything that writes, runs code or reaches another site is judged as before. Something else and Coding agent start with allow |
localWrites | allow-after-intern: once the agent is past its intern stage, a tool call that edits a file or runs a test or build inside its project goes ahead without a person; an intern still asks before every write. ask, or left out: a person decides, at any standing. allow is the older word for allow-after-intern and means the same, because the intern standing asks before anything that is not read-only either way. Something else (General tasks) and Coding agent start with allow-after-intern; to have a person approve every edit and test run, replace the rule with one that sets ask. “Inside its project” is read from the project the hook sends (Claude Code’s CLAUDE_PROJECT_DIR, or the working directory) and the paths in the call; a call that does not show it stays inside, such as one from an older hook that does not send the project, asks. Whatever this says, and at every standing, these ask: a destructive command (rm, git push --force, git reset --hard, git clean, a history rewrite such as git rebase, recursive chmod or chown, dd, mkfs), any git push, a deploy (fly deploy, vercel, kubectl apply, terraform apply and the like), a publish (npm publish, twine upload), a package install from the network (npm install, pip install, npx, or piping a download into a shell), anything run with sudo, anything that names a path outside the project, and a change to the agent’s own hook and settings (.claude/settings.json), .mcp.json or git’s hooks and config. A command that cannot be read with confidence asks too, and a secrets file or the environment leaving the machine is refused |
Three things hold under every action rule, whatever it says and however trusted the agent:
- A destructive command asks a person:
rm,git push --force,git reset --hard,git clean, piping a download into a shell (curl ... | sh), writing over a file outside the project (~/.bashrc,~/.ssh,/etc,.git/hooks), dropping a database table, publishing a package, and the like (destructive_command). - A command that cannot be read with confidence asks a person: one too long to be sent whole, one spelt in escape codes or built from a variable, or one carrying invisible characters (
unverifiable_command). - A secrets file (
.env, keys,~/.aws,~/.ssh) or the environment leaving the machine is refused (secrets_leaving).
Every site a command names is checked against domains, not only the first, and a command that uses the network without naming where (git push origin, ssh host) asks a person under a rule that names domains (unnamed_destination).
#Rules add up
An action needs to fit only one of an agent’s mandates, so the broadest one sets the limit. Two things keep a broad mandate from quietly undoing a narrow one:
- A payment to a merchant that a mandate allow-lists is judged under that mandate, and an action a mandate names exactly (
tool.call) is judged ahead of a wildcard (tool.*). - Creating a mandate that allows anywhere (an action mandate with no domains, or a payment mandate with no merchant list that allows new merchants) beside a narrower one for the same agent is refused with
409 confirm_broadenand areasonsuch as “This rule allows more than an existing rule for this agent: tool calls to any domain (Coding agent allows only github.com, registry.npmjs.org)”. Send it again withconfirmBroaden: trueto save it; the answer then carries the same sentence aswarning, and the ledger record says which mandates it widens.
#A rule for your own agent
Nobody widens the authority of an agent that acts for them on their own say-so. In a workspace with another owner, a mandate you write for an agent that acts for you, and that gives it anything its current mandates do not (a higher limit per payment or per period, a new kind of authority or action, a merchant, recipient, field or domain it cannot reach now, or no approval line where it has one), is not saved. The answer is 403 second_owner_required:
{
"error": {
"type": "second_owner_required",
"message": "This agent acts for you, and this rule gives it more than it has now (up to £50,000 a payment, more than the £150 it has now), so another owner of this workspace confirms it. It has been sent to them as a proposal.",
"proposalId": "prp_6f1c2a9e",
"why": ["up to £50,000 a payment, more than the £150 it has now"]
}
}The proposal waits for another owner for seven days: GET /api/w/$IMMISCIBLE_WORKSPACE/proposals lists them, and POST .../proposals/:pid/confirm saves the mandate exactly as it was asked for, with both names on the record; .../dismiss drops it. Whoever proposed it cannot confirm it. Raising the workspace’s burst lines (agentVelocity in the settings) works the same way.
In the same workspace, the person an agent acts for never approves its payments either: someone else does, whatever the mandate’s approval line.
#What a mandate cannot do
A mandate grants authority. It cannot remove the floors under it.
- It cannot switch off the Rule of Two: untrusted content driving a payment or data release with an external effect needs a person, however generous the mandate.
- It cannot permit a lookalike domain.
arnazon.comis denied even if someone allow-listed it. - It cannot outlive the kill switch. A frozen agent is denied everything.
- It cannot lift the agent above its tier. An intern asks about everything that matters whatever its mandates say.
#Templates
The console offers templates (groceries, subscriptions and bills, travel, data sharing). Each is a starting point; every field is editable before saving. A service token registering agents as code may only attach mandates from templates: a machine never writes a custom spec.
#Revoking and expiry
Revocation is immediate: the next request under the mandate is no_mandate. A mandate with expiresAt stops on its own at that moment. Neither cancels an action already allowed and settled; they stop the next one.
#API
| Method | Path | |
|---|---|---|
GET | /api/w/:wid/mandates | list |
POST | /api/w/:wid/mandates | create, signed on creation |
POST | /api/w/:wid/mandates/:mid/revoke | revoke |
GET | /api/w/:wid/mandate-templates | the templates |