Starter Policies
Copy-pasteable, production-shaped policies for the guardrails almost every team ships first. Each one validates against the policy schema as-is — paste, rename, and tighten.
Policy anatomy
A policy is JSON: an id, a version, an optional applies_to scope (tools and/or agents), a mode, and an ordered list of rules. Evaluation is deterministic and first-match-wins — the first rule whose whenconditions match decides the action. No rule matching means the engine falls through to your policy's last rule, so end every policy with an explicit catch-all.
| Field | Meaning |
|---|---|
decision | One of allow, deny, warn, require_approval, require_reauth, require_tool_reapproval, require_policy_update, dry_run |
mode | monitor (log only), warn, enforce, or strict |
when | { "all": [...] } or { "any": [...] } of conditions: path, operator, value |
operator | == != > >= < <= in not_in contains matches (aliases eq/=, ne/neq, gt, gte, lt, lte) |
$ref | A value of { "$ref": "path" }compares two context paths — e.g. the action's args.customer_idagainst the passport's resource_constraints.customer_id |
"mode": "monitor". Run it against real traffic for a few days, review the decisions on the Policies page, then flip to enforce.1. Spend limit with human approval
The most common first policy: small payments go through, medium ones pause for a human, large ones are refused outright.
{
"id": "spend-limit",
"version": 1,
"description": "Auto-allow small spend, approve medium, deny large.",
"mode": "monitor",
"applies_to": { "tools": ["stripe.charge", "stripe.refund", "payments.send"] },
"rules": [
{
"name": "deny-large-spend",
"decision": "deny",
"reason": "Amounts over $500 are never agent-initiated.",
"when": { "all": [{ "path": "args.amount", "operator": ">", "value": 50000 }] }
},
{
"name": "approve-medium-spend",
"decision": "require_approval",
"reason": "Spend over $50 needs a human sign-off.",
"approval": { "channel": "slack", "min_role": "manager" },
"when": { "all": [{ "path": "args.amount", "operator": ">", "value": 5000 }] }
},
{
"name": "allow-small-spend",
"decision": "allow",
"reason": "Small spend within the agent's budget.",
"when": { "all": [{ "path": "args.amount", "operator": "<=", "value": 5000 }] }
}
]
}2. Scope the agent to its own customer
A support agent handling ticket #123 must not touch customer #456. The $refcondition pins every action to the customer named in the agent's passport — prompt injection can change what the agent tries, not what this rule compares.
{
"id": "customer-scope",
"version": 1,
"description": "Agent may only act on the customer bound into its passport.",
"mode": "monitor",
"rules": [
{
"name": "allow-own-customer",
"decision": "allow",
"reason": "Action targets the customer this passport was issued for.",
"when": {
"all": [
{
"path": "args.customer_id",
"operator": "==",
"value": { "$ref": "passport.resource_constraints.customer_id" }
}
]
}
},
{
"name": "deny-cross-customer",
"decision": "deny",
"reason": "Cross-customer access is not permitted for this passport.",
"when": { "all": [{ "path": "args.customer_id", "operator": "!=", "value": { "$ref": "passport.resource_constraints.customer_id" } }] }
}
]
}3. Outbound email guardrails
Keep drafts free, allow internal mail, and require approval before anything leaves the org — the classic guard for Gmail/Outlook agents.
{
"id": "outbound-email",
"version": 1,
"description": "Internal email allowed; external email needs approval; bulk sends denied.",
"mode": "monitor",
"applies_to": { "tools": ["gmail.send", "outlook.send"] },
"rules": [
{
"name": "deny-bulk-send",
"decision": "deny",
"reason": "Agents may not send to more than 10 recipients at once.",
"when": { "all": [{ "path": "args.recipient_count", "operator": ">", "value": 10 }] }
},
{
"name": "allow-internal",
"decision": "allow",
"reason": "Internal recipients only.",
"when": { "all": [{ "path": "args.to_domain", "operator": "in", "value": ["yourco.com"] }] }
},
{
"name": "approve-external",
"decision": "require_approval",
"reason": "External email requires a human to approve the draft.",
"approval": { "channel": "slack", "min_role": "member" },
"when": { "all": [{ "path": "args.to_domain", "operator": "not_in", "value": ["yourco.com"] }] }
}
]
}4. Environment gating for deploys
Agents can ship to dev and staging on their own; production is a human decision. The environment path comes from the normalized policy context.
{
"id": "deploy-gate",
"version": 1,
"description": "Free deploys below prod; prod requires approval.",
"mode": "monitor",
"applies_to": { "tools": ["ci.deploy", "k8s.apply", "terraform.apply"] },
"rules": [
{
"name": "approve-prod-deploy",
"decision": "require_approval",
"reason": "Production changes need an on-call engineer's approval.",
"approval": { "channel": "slack", "min_role": "admin" },
"when": { "all": [{ "path": "environment", "operator": "==", "value": "prod" }] }
},
{
"name": "allow-nonprod-deploy",
"decision": "allow",
"reason": "Non-production deploys are agent-safe.",
"when": { "all": [{ "path": "environment", "operator": "in", "value": ["dev", "staging"] }] }
}
]
}5. Block tools that changed since approval
A tool whose manifest hash no longer matches the one your team approved may have gained new scopes or new behavior. This rule forces re-approval instead of silently running the new version.
{
"id": "manifest-drift",
"version": 1,
"description": "Re-approve any tool whose manifest drifted from the approved hash.",
"mode": "monitor",
"rules": [
{
"name": "reapprove-drifted-tool",
"decision": "require_tool_reapproval",
"reason": "Tool manifest changed since it was approved for this passport.",
"when": {
"all": [
{
"path": "tool.current_manifest_hash",
"operator": "!=",
"value": { "$ref": "passport.tool_manifest_hash" }
}
]
}
},
{
"name": "allow-approved-tool",
"decision": "allow",
"reason": "Tool manifest matches the approved hash.",
"when": { "all": [{ "path": "tool.current_manifest_hash", "operator": "==", "value": { "$ref": "passport.tool_manifest_hash" } }] }
}
]
}6. Risk-tiered default policy
A good tenant-wide default while you author finer-grained policies: high-risk tools always pause for a human, everything else is allowed but logged.
{
"id": "risk-tier-default",
"version": 1,
"description": "Tenant-wide default: high-risk tools require approval.",
"mode": "monitor",
"rules": [
{
"name": "approve-high-risk",
"decision": "require_approval",
"reason": "High-risk tools always require a human in the loop.",
"approval": { "channel": "slack", "min_role": "manager" },
"when": { "all": [{ "path": "tool.risk_tier", "operator": "in", "value": ["high", "critical"] }] }
},
{
"name": "allow-low-risk",
"decision": "allow",
"reason": "Low/medium-risk tools are allowed and logged.",
"when": { "all": [{ "path": "tool.risk_tier", "operator": "not_in", "value": ["high", "critical"] }] }
}
]
}policy.rules[0].when.all[1].operator) so a typo never becomes a silent allow. Test with POST /v1/policies/validate before you publish.