Documentation
Policy reference
A MailPolicy is a declarative document attached to a mailbox. It is evaluated server-side on every inbound message, before your agent code runs. It enforces sender allowlists, DKIM/SPF verification, content guards, rate limits, and per-sender capability scoping.
A policy is set with client.setPolicy(mailboxId, policy) (or PUT /v1/mailboxes/{id}/policy) and evaluated automatically by the platform. The exact evaluation order is documented in Evaluation order.
Schema overview
interface MailPolicy {
defaultAction: "bounce" | "drop";
senders: SenderRule[];
contentGuards?: ContentGuard[];
auditLog: AuditConfig;
}
interface SenderRule {
match: SenderMatch;
capabilities: string[];
rateLimit?: RateLimit;
tokenBudget?: TokenBudget;
}The wire format uses camelCase. The Python SDK maps these to snake_case field names internally.
MailPolicy
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
defaultAction |
"bounce" | "drop" |
yes | — | one of two literals | Action taken for senders that match no rule. See DefaultAction. |
senders |
SenderRule[] |
yes | [] |
top-to-bottom evaluation; first match wins | Ordered list of sender rules. |
contentGuards |
ContentGuard[] |
no | [] |
applied after a sender rule matches | Regex-based body filters. |
auditLog |
AuditConfig |
yes | — | retentionDays ≥ 1 |
Audit-log configuration. |
SenderRule
A SenderRule matches a sender (or a class of senders) and grants them a list of capabilities, optionally bounded by rate and token limits.
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
match |
SenderMatch |
yes | — | at least one of address/domain |
Who this rule applies to. |
capabilities |
string[] |
yes | [] |
arbitrary strings; see Capability | What tools/actions the agent may invoke for this sender. |
rateLimit |
RateLimit |
no | unbounded | independent per-hour/per-day | Cap on inbound messages from this sender. |
tokenBudget |
TokenBudget |
no | unbounded | per-thread and per-day are independent | Cap on LLM tokens spent on this sender. |
Rules are evaluated in order; the first rule whose match matches the inbound sender wins. Subsequent rules are not evaluated.
SenderMatch
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
address |
string |
no | — | full email address; case-insensitive | Exact-match an address. |
domain |
string |
no | — | bare domain; case-insensitive | Match any address at this domain. |
requireDkim |
boolean |
no | false |
— | Reject if DKIM did not pass. |
requireSpf |
boolean |
no | false |
— | Reject if SPF did not pass. |
If both address and domain are set, the sender must match the address (the domain is then redundant). If neither is set, the rule matches every sender — useful as a catch-all at the bottom of the list.
Matching is case-insensitive in both directions.
Capability
capabilities is an array of arbitrary strings. The platform does not enforce a fixed enum; the strings are passed verbatim to your agent runtime, which decides what they mean. The reference integration uses these four:
| Value | Meaning in the reference integration |
|---|---|
read_calendar |
Agent may inspect the user's calendar. |
propose_meeting |
Agent may suggest a meeting time. |
confirm_meeting |
Agent may book a meeting. |
ingest_conflict_notice |
Agent may process a conflict-detection bot's notification. |
Other capability strings work fine; the platform only warns if it has not seen them before.
RateLimit
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
perHour |
integer |
no | unbounded | ≥ 1 if set | Inbound messages from this sender per UTC hour. |
perDay |
integer |
no | unbounded | ≥ 1 if set | Inbound messages from this sender per UTC day. |
Both limits are enforced independently if both are set. Windows are tumbling (UTC hour boundary, UTC day boundary).
When a limit is exceeded, the message is rejected with audit outcome rate_limited.
TokenBudget
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
perThread |
integer |
no | unbounded | ≥ 1 if set | Total LLM tokens this sender may consume per email thread. |
perDay |
integer |
no | unbounded | ≥ 1 if set | Total LLM tokens this sender may consume per UTC day. |
Token budgets are enforced after the agent runs; the agent reports its own token usage back to the platform. When a budget is exhausted, the message is rejected with audit outcome budget_exhausted.
ContentGuard
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
reject |
string |
yes | — | regex source; ECMAScript syntax with inline flags (e.g. (?i)foo) |
Pattern that, if it matches the body, rejects the message. |
reason |
string |
yes | — | non-empty | Reason recorded on the audit entry and embedded in any bounce notification. |
Content guards are applied after sender matching. They run against the message's text body. Regexes use ECMAScript syntax; for case-insensitive matching, prefix with (?i).
When a guard matches, the message is rejected with audit outcome rejected_at_content_guard.
AuditConfig
| Field | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
retentionDays |
integer |
yes | — | ≥ 1 | How long audit entries are kept before deletion. |
includeBodyHash |
boolean |
no | false |
— | If true, every audit entry includes a SHA-256 of the inbound body bytes. |
See Audit log for what is recorded and how it is queried.
DefaultAction
The defaultAction field on MailPolicy controls what happens to senders that do not match any rule.
| Value | Behaviour |
|---|---|
bounce |
Send a notification email to the sender explaining the rejection. The original message is not delivered to the agent. |
drop |
Silently discard the message. No notification sent; the audit log records the rejection. |
bounce is the friendlier default for legitimate human senders; drop is appropriate for bot/spam-prone mailboxes.
Evaluation order
For every inbound message, the platform evaluates the policy in this exact order. The first failing step rejects the message; no later step runs.
- Sender rule matching. Walk
senderstop-to-bottom. The firstSenderMatchthat matches the inbound sender wins. If none match, reject with outcomerejected_at_policyand applydefaultAction. - Verification. If the matched rule sets
requireDkimorrequireSpf, check the inbound message's DKIM/SPF verdicts. On failure, reject with outcomerejected_at_verification. - Content guards. Walk
contentGuards. If anyrejectregex matches the body, reject with outcomerejected_at_content_guardand the rule'sreason. - Rate limits. If the matched rule has a
rateLimit, increment the per-hour and per-day counters for the sender. If either exceeds the limit, reject with outcomerate_limited. - Token budget gate. If the matched rule has a
tokenBudget, check the already-consumed counters for this thread and this sender's UTC day. If the latest already-consumed total exceeds the budget, reject with outcomebudget_exhausted. (Token budgets are enforced retrospectively: the gate prevents the next message after the budget is exhausted, not the one that exhausted it.) - Capability scoping. The matched rule's
capabilitiesare attached to the inbound event and passed to the agent runtime.
Bounces are sent only when defaultAction == "bounce" AND the rejection happens. Verification and content-guard rejections are accompanied by a bounce notification with a specific reason.
Examples
Single-user scheduling agent
The reference integration's policy. One named boss, the boss's domain as a backup, no one else.
const policy: MailPolicy = {
defaultAction: "bounce",
senders: [
{
match: { address: "boss@acme.com" },
capabilities: ["read_calendar", "propose_meeting", "confirm_meeting"],
rateLimit: { perHour: 30 },
tokenBudget: { perThread: 8000, perDay: 100_000 },
},
{
match: { domain: "acme.com", requireDkim: true },
capabilities: ["read_calendar"],
rateLimit: { perHour: 10 },
},
],
contentGuards: [
{ reject: "(?i)wire transfer", reason: "phishing-likely keyword" },
],
auditLog: { retentionDays: 30, includeBodyHash: true },
};Customer-support triage with three sender tiers
A support agent with VIP, paying-customer, and free-tier sender bands.
const policy: MailPolicy = {
defaultAction: "drop",
senders: [
{
match: { domain: "vip-customer.com", requireDkim: true },
capabilities: ["read_account", "create_ticket", "escalate_immediate"],
rateLimit: { perHour: 100 },
},
{
match: { domain: "paying-customer.com", requireDkim: true },
capabilities: ["read_account", "create_ticket"],
rateLimit: { perHour: 30 },
tokenBudget: { perDay: 200_000 },
},
{
match: {},
capabilities: ["create_ticket"],
rateLimit: { perHour: 5 },
tokenBudget: { perThread: 2000 },
},
],
auditLog: { retentionDays: 90, includeBodyHash: true },
};Internal-tool agent restricted to one corporate domain
A dev-ops bot that only ever talks to one company's mailservers.
const policy: MailPolicy = {
defaultAction: "drop",
senders: [
{
match: { domain: "acme-corp.com", requireDkim: true, requireSpf: true },
capabilities: ["deploy", "rollback", "read_status"],
rateLimit: { perHour: 60 },
},
],
contentGuards: [
{ reject: "(?i)\\b(prod|production)\\b.+rollback", reason: "production rollback requires human approval" },
],
auditLog: { retentionDays: 365, includeBodyHash: true },
};Errors
PUT /v1/mailboxes/{id}/policy returns 400 Bad Request with {"errors": [...]} on validation failure. Each entry is a human-readable description.
| Error string (example) | Cause |
|---|---|
auditLog.retentionDays must be >= 1 |
retentionDays was zero or negative. |
senders[2].rateLimit.perHour must be >= 1 |
Numeric limit was zero or negative. |
contentGuards[0].reject is not a valid regex |
Pattern source failed the regex parser. |
senders[0].capabilities[1] is empty |
Capability string was empty. |
The Python and TypeScript SDKs surface these as ValidationError, exposing the array as .field_errors / .fieldErrors.
Other status codes:
| Status | Cause |
|---|---|
401 |
Missing or invalid API key. |
403 |
API key does not own this mailbox. |
404 |
Mailbox does not exist. |
429 |
Rate-limited at the API layer. |
5xx |
Backend or upstream failure. |