# 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.

Source: https://immiscible.fly.dev/docs/concepts/mandates

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

```json
{
  "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`).

> **Tip**
> Name the domains. A mandate with no domains means any destination, and once untrusted content is in the agent's session the Rule of Two then asks a person before each call. Listing domains turns those questions into clean refusals for anywhere else.

## 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_broaden` and a `reason` such 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 with `confirmBroaden: true` to save it; the answer then carries the same sentence as `warning`, 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`:

```json
{
  "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`](https://immiscible.fly.dev/docs/api/get-api-w-wid-proposals.md) lists them, and [`POST .../proposals/:pid/confirm`](https://immiscible.fly.dev/docs/api/post-api-w-wid-proposals-pid-confirm.md) 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.com` is 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](https://immiscible.fly.dev/docs/api/authentication.md#service-tokens) 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`](https://immiscible.fly.dev/docs/api/get-api-w-wid-mandates.md) | list |
| `POST` | [`/api/w/:wid/mandates`](https://immiscible.fly.dev/docs/api/post-api-w-wid-mandates.md) | create, signed on creation |
| `POST` | [`/api/w/:wid/mandates/:mid/revoke`](https://immiscible.fly.dev/docs/api/post-api-w-wid-mandates-mid-revoke.md) | revoke |
| `GET` | [`/api/w/:wid/mandate-templates`](https://immiscible.fly.dev/docs/api/get-api-w-wid-mandate-templates.md) | the templates |
