Approvals for agents¶
Every action an agent takes is checked by one permission engine. The same checks apply to the built-in assistant and to every API token. Actions taken directly by the signed-in user are not gated.
One of three effects is chosen for each action:
- Allow: the action is carried out now.
- Ask: the action is stored, an approval card is shown, and the action is carried out only after approval.
- Deny: the action is refused with an error.
Order of checks¶
- Scope. The token must hold the scope for the action, or the action is denied. The built-in assistant acts with your rights.
- Deny rules. Any matching Deny rule takes precedence.
- Allow and ask rules. A matching rule is looked for from the most specific subject to the least: one token or one chat, then Ambrose, then every agent. At the first level with a match, Ask takes precedence over Allow.
- Defaults. Without a matching rule, the built-in default applies (table below).
- Hard rules. In a few cases approval is always required, whatever the rules allow.
Hard rules¶
- The first message to a new address is always held for approval. This covers emails, invitation answers to an organiser you have never written to, and invitations to new guests. A contact group is expanded first, so each member counts on its own.
- A public recording link is always held for approval. Such a link is a send to anyone who has it.
- A bulk review card is always held for approval. A review of a whole list or search (
review_mail,/sweep) ends in one card, also when it was started by a routine. - A token's action on a whole list is always held for approval.
archiveormodify_conversationswith ascope(a view or a search) from an API token waits for a click, as only explicit ids are accepted from tokens by the REST API. After approval, the action is applied as that token: messages on accounts the token cannot see, and local-only messages when the token is not local-only, are left as they were. - Memories Ambrose writes after reading mail become suggestions or are held for approval, so no instructions can be planted in memory through an email.
- Text an agent writes after reading mail is held for approval. Once a turn of Ambrose has read email, including subjects, briefs, other text on your screen and the names of correspondents that match a name in your request, creating a task, changing a task's title or notes, and rewriting a brief are all held for approval. The same holds for every API token. When a rule allows the class for that agent, the action still goes through. A card of Ambrose's held this way, or because it hides mail after reading mail, is decided once: it has no Allow for this chat or Always allow, and a grant on another card does not decide it. A rule you add under Permissions still applies to later requests.
- Hiding mail after reading mail is held for approval. In such a turn, archiving, marking read, muting, snoozing, marking not important, removing a system label such as Inbox (
apply_labelwithremove), and moving mail to another tab (show_in_tab) are held for approval, so Ambrose cannot be told by an email to hide another one, such as a security alert. - Drafts. Your rules for
mail.draftapply to Ambrose and to tokens. A change to the recipients of a saved draft is always held for approval. After reading mail, or through a token, a draft to anyone outside the conversation it is a reply to, or with a Bcc, is held for approval. A meeting follow-up draft (draft_follow_up) is held the same way when its recipients include anyone who was not a guest of the meeting. - Notes. Text an agent writes into a note after reading other people's text is held for approval: a new note, a new item, an item's new text, a body, a section name, or a task made from an item. Checking and unchecking are never held. In such a turn, archiving a note and deleting items are held for approval too. Any read of a note counts, your own notes included, because a note can hold pasted mail. Writing from an errand of a standing order to a note other people share is always held for approval.
- Local-only content stays local-only. In a chat that has read local-only mail, the notes, lists and tables Ambrose makes are marked local-only. A new task, a task's new title or notes, a skill, a rewritten brief, and a draft that is not saved on a local-only account or as a reply to local-only mail are always held for approval, even when a rule allows them.
- Nothing is sent from background errands. Mail from an errand of a standing order is held for approval, even when a rule allows Ambrose to send.
- No remote images in agent text. Images in task notes, briefs, drafts, signatures and event descriptions that an agent writes are removed, and Ambrose keeps only links it found in the mail it read. In task notes, summaries and recording notes, images are shown as links and never loaded. In a draft an agent writes, a link whose text is not its own address gets the address beside it, for example "your bank portal (https://login.example.net/)", so you see where each link goes before you send.
Defaults¶
| Action class | Covers | Default |
|---|---|---|
mail.draft |
Create, change and delete drafts; rewrite a brief | allow |
mail.label |
Labels, archive, read, star, important, snooze, mute, category | allow |
mail.send |
Send a message or release a scheduled send | ask |
mail.trash |
Move to Trash, or out of it | ask |
mail.spam |
Report spam or not spam, block or unblock a sender | ask |
mail.delete |
Delete forever | ask |
mail.unsubscribe |
Unsubscribe from a list | ask |
mail.screener |
Allow, block or label a sender in the Screener | always ask |
rules.write |
Create, change, delete and apply filters | ask |
labels.write |
Create, rename, nest and recolour labels | ask (assistant: allow) |
labels.delete |
Delete labels | ask |
prefs.write |
Settings, inbox layouts, signatures and Ambrose profile | ask |
orders.write |
Create, change and delete standing orders and routines | ask |
agent.forwarded_instructions |
Act on a note you forward from one of your trusted addresses (Ambrose only). Set on the standing order Follow my forwarded notes, not on the Permissions page | ask |
memory.write |
Save and forget memories, create and change people and things | ask (assistant: allow) |
skills.write |
Create and change skills | ask |
calendar.create |
Create events | ask |
calendar.update |
Edit and move events | ask |
calendar.delete |
Delete events | ask |
calendar.rsvp |
Answer an invitation | ask |
calendar.invite |
Event changes that email guests | ask |
calendar.cancel |
Cancel or delete a meeting you organise that has guests | ask |
calendar.manage |
Create, rename, recolour and delete calendars | ask |
tasks.create |
Create tasks | allow |
tasks.update |
Edit and move tasks | allow |
tasks.complete |
Complete and reopen tasks | allow |
tasks.delete |
Delete tasks | ask |
tasks.lists |
Create and reorder task lists; renaming and deleting ask | allow |
tasks.bulk |
Bulk task actions and tag changes; bulk delete and tag delete ask | allow |
contacts.write |
Create, rename and delete contact groups, and change their members | ask |
speakr.link |
Link a recording to an event, conversation or task; map a speaker to a person | allow |
speakr.upload |
Send an email attachment to Speakr | ask |
speakr.write |
Change a recording's title, participants, tags or notes in Speakr | ask |
speakr.share |
Create a public recording link | always ask |
web.search |
Search the web and read a page from the results (web_search, read_page) |
allow |
notes.create |
Make notes and checklists | allow |
notes.edit |
Change a note: title, text, colour, pin, archive, labels, kind, links, a restored version; add, check, uncheck, edit, move and delete items; sections | allow |
notes.delete |
Move a note to Trash; clear a list's checked items | ask |
notes.labels |
Create, rename and recolour note labels; deleting one asks | ask (assistant: allow) |
tables.rows |
Add and change table rows | allow |
tables.rows_delete |
Delete table rows | ask |
tables.schema |
Create tables, add columns and save views; other column and view changes and a restore to a point in time ask | allow |
settings.write |
Accounts, identities, tokens and other account settings | deny |
Reading is not rule-gated. What an agent can read depends on its token's scopes, its accounts and the local-only flag.
Permission rules¶
Open Settings → Agents & API → Permissions to see the effect of every class and to add rules.

A rule has a subject (every agent, Ambrose, one chat, or one token), an action class (or a wildcard such as mail.*), an effect and optional constraints. A rule applies only when all its constraints match, and it can expire. A wildcard rule (* or agent.*) never sets the standing order Follow my forwarded notes to Allow: only a rule that names agent.forwarded_instructions does. A broad Deny rule still stops it.
| Constraint | Applies to | Matches when |
|---|---|---|
| Replies only | mail.send |
the message is a reply inside an existing conversation, to its participants |
| Known recipients only | mail.send |
every recipient has written with you before |
| Domains | mail.send |
Allow: every recipient is in a listed domain. Deny or ask: any recipient is. |
| Recipients | mail.send |
Allow: every recipient is listed. Deny or ask: any is. |
| Identities | mail.send |
the From address is one of these |
| Max per hour, max per day | mail.send |
fewer sends than the limit so far |
| Accounts | mail.* |
the messages come from these accounts |
| Labels | mail.label |
only these labels change |
| Calendars | calendar.* |
the event is in one of these calendars |
| Lists | tasks.* |
the task is in one of these lists |
| Speakr account, tags | speakr.* |
the recording belongs to this account or has these tags |
| Notes | notes.* |
the note written is one of these |
| Labels | notes.* |
Allow: every one of your labels on the note is listed. Deny or ask: any is. |
Always allow… on an approval card creates a rule narrowed to the calendar, list, note, recipients' domain or labels of that request.
A token's send policy (see Tokens and scopes) works as an Allow rule for mail.send at that token's level. Rules still apply: a Deny takes precedence, and the first message is still held for approval.
The send gate¶
Every email an agent sends is checked at the send gate:
- The
sendscope is required. Without it,send_draftis refused and the draft is kept. - The permission rules are applied. By default every send is held for approval.
- The first message to a new address is always held for approval. Only mail you wrote or approved counts as having written to an address; automatic replies, invitation answers and unsubscribe messages do not.
- An allowed send is held for a window before it is sent, 60 seconds by default (
MAILR_AGENT_SEND_HOLD_SECONDS), and can be cancelled during that time. - A notification is shown on the web and on the phone when a message is waiting for approval.
Each recipient must be a plain address (name@example.com). An address with a display name, a comment, angle brackets or more than one @ is refused with HTTP 400; put the name in the recipient's name field. Addresses are matched and sent in lower case, with international domain names in their ASCII form.
A send may name a draft_id only to send that draft: when an agent's message differs from the stored draft, the draft is kept and not replaced.
Response to a gated call¶
A gated call returns HTTP 202 (or, from a tool, a pending_approval result):
{
"status": "pending_approval",
"approval_id": 6158241357902263,
"approval_url": "https://mail.example.com/approvals/aSChI8VY0d"
}
The approval_url leads to the approvals panel in Mailr, with that request outlined for a decision. After the decision, the outcome is shown there instead. Push notifications for approvals open the same link.
With public ids on (MAILR_PUBLIC_IDS=true), approval ids are opaque, like conversation and message ids: approval_id is a 16-digit number and the link has its short form (a…). Use the ids exactly as they are returned. Either form is accepted by GET /api/v1/approvals/{aid}. An old numeric id from before the switch is still accepted while that approval is pending. See Security.
Nothing has happened yet, so the agent should report this to the user and check back later:
get_approval_status(orGET /api/v1/approvals/{aid}) returnspending,approved,executing,executed,denied,expiredorfailed, with the result once the action ran.get_outbox_statusreturns a sent message's state:queued,awaiting_approval,sent,cancelled,rejected,failedoruncertain.uncertainmeans the connection to the mail server dropped after the message was handed over, so it may have gone out; it is never sent again on its own, and the user decides. An agent should say a message was sent only once its state issent.
An agent must never retry a call to get around an approval. Approved actions are carried out once, with the requesting token's limits.
Pending approvals expire after 24 hours by default (MAILR_APPROVAL_TTL_HOURS).
An approval card shows everything that will be carried out: the whole text of an email, a note, a skill, a signature or a standing order's instructions, and every recipient and guest. Long text is folded behind Show more. Invitation mail is sent only to the guests listed on the card; a guest added to the event elsewhere in the meantime is sent no mail from that change.
Deciding¶
Only the signed-in user can decide. Use the card in the chat, the bell at the top of every page, Settings → Agents & API → Approvals, or the notification on your phone. See Approvals for the choices on a card.
A grant on one card ("scope": "session" or "always") also decides the other waiting requests its new rule covers. They are the same agent's requests of the same class, with the rule's constraints met and no hard rule that asks. A card that always asks, or one of Ambrose's held by a hard rule, takes no grant: session and always on it are refused with 400, and its grants field is false. Each is given the same decision, decided_by rule:<id>, and is carried out on its own. rule_id and also_decided ({"ids": [...], "count": n}) are added to the decide response. With POST /api/v1/approvals/grants/{rule_id}/undo, the rule is removed and the requests decided by the rule that have not started are put back to pending; they are listed in the response under reverted, and those already running or done under kept. On each card, also_covers is the number of other waiting requests each grant would decide: session, always and always_derived (narrowed to the request).
A review card or a cleanup plan can be approved with some conversations left out: add except_ids (conversation ids) to an approve decision. Only those conversations are removed from what the card showed; nothing else is added. The card's preview then has left_out, the number left out.
A review card can also be approved with your own move for some conversations: add changes, an object from conversation id to a move, to an approve decision, for example {"changes": {"4503599627370497": "trash", "4503599627370498": "label:12", "4503599627370499": "keep"}}. A move is trash, archive, read, keep (the same as except_ids) or label:<label id> with one of your own labels. Only conversations the card showed can be changed: those in a group of the plan (review.groups[].senders[].conversations), and the rows in review.kept and review.unsure. Any other id, another move or a label that is not yours is refused with 400, and the card stays waiting. The plan and the card are rebuilt from the card and your changes; the preview then has changed (the number of conversations you moved) and a line "N changed by you", and the conversations you kept are in review.kept with by_you.
Each sender on a review card lists its conversations under conversations (conversation_id, subject, date), and review.labels lists the labels the review could add (id, name).
A filter or a standing order for new mail that an agent creates or changes (manage_filter, manage_order) is checked against the user's mail of the last 6 months before it asks. The card's preview has match (count, capped, days, newest_subject, newest_date) and the display a Matches now field. The tool result has matches_existing (emails, period, newest_date, without the subject) and, when nothing matches, check_search: the agent checks the search with the user before they approve.
POST /api/v1/approvals/decide-many with {"ids": [...], "decision": "approve"} decides several waiting requests from one requester (one chat, one token, or Ambrose) at once. The web app's Allow all button is this call. Each request is decided as if its own card had been tapped once: no rule is stored, and nothing else is covered. Requests from different requesters are refused with 400. A request that is no longer waiting is skipped. In the response, the decided cards are listed under decided, the skipped ones under skipped ({"id", "status"}), with count. As for every decision, a signed-in session is required; an agent token is refused with 403.