Your self-storage, voice, SMS or property-management agent drafts an action: waive a late fee, issue a credit, change a gate code, call a tenant at 9 PM. It asks AgentGuard first. The gate reads the operator's policy, answers allow, escalate or block, and signs a receipt of the decision. An escalation reaches a person by one tap, and that person's answer becomes a second signed receipt chained to the first. The gate sees amounts, action types and hashed references. It never sees the conversation, the customer record or a phone number.
Action type, amount, currency, a target-system category, a hashed recipient reference, the recipient's time zone, a consent reference. Any request carrying prompt, message or customer content is rejected with content_rejected.
Per-action limits, a daily money cap, a capability ladder, a calling window in the recipient's local time, a consent requirement, a per-recipient contact cap, fleet caps for the whole tenant, and a pause switch for the operator.
An Ed25519-signed receipt for every decision, with the policy hash, the inputs it judged and neutral text such as "outside the calling window set in the policy". Anyone holding a receipt can check it at agentguard.run/verify.
Your runtime already knows the tool call it is about to make. Send its metadata, not its content.
With your vendor key and the operator's policy. The answer comes back in one round trip with a signed receipt.
allow runs the tool. escalate opens a one-tap approval for a person. block stops, and the receipt says why.
Hand them to the operator, an auditor or an underwriter. They verify at /verify without asking us.
Pre-execution policy enforcement is the control the Cloud Security Alliance listed first after the 2026 Hugging Face incident; the gate decides before the action runs. Read the CSA note.
Three ways in, same gate, same receipts. Pick the one your platform already speaks.
If your agent platform connects to MCP servers, add https://agentguard.run/api/mcp and send your vendor key on the connection as Authorization: Bearer agk_vendor_... (or as an X-AgentGuard-Vendor-Key header where the platform reserves Authorization). Tools: gate_authorize, gate_approval_status, gate_policy_example. Without a key the tools run only for tenant sandbox.
// tools/call gate_authorize, the same request body as the REST route { "name": "gate_authorize", "arguments": { "tenant_id": "sandbox", "agent_id": "storage-bot", "capability_claim": "data_write", "action": { "type": "waive_fee", "amount_cents": 5000, "currency": "USD", "target_system": "property_management" }, "policy": { "id": "storage-bot-v1", "version": 1, "on_unavailable": { "default": "stop" }, "rules": { "waive_fee": { "required_capability": "data_write", "review_above_cents": 0 } } } } } // structuredContent: { decision: "escalate", decision_text: "held for a person under the policy", receipt: {...}, on_unavailable_effective: "stop" }
Setup for Claude Code, Cursor and ChatGPT is on the hosted MCP page; the gate tools are listed there with the six public ones.
If your platform calls an HTTPS URL on each tool call, run one of these handlers next to your agent. Each receives a small event, asks the gate, and answers proceed or not, with the approval id or the block text. The approval link itself goes to your approver, never back to the agent. When the gate does not answer within two seconds the handler reads your own on_unavailable setting for that action type and notes the gap.
handler.js (Node, standard library) · handler.py (Python, standard library) · README
# the event your platform posts to the handler; metadata only { "tool": "waive_fee", "arguments": { "amount_cents": 5000, "currency": "USD", "target_system": "property_management" }, "tenant_id": "acme-storage", "agent_id": "storage-bot" } # what the handler answers { "proceed": false, "approval_id": "apr_...", "text": "held for a person under the policy" }
The curl examples below run as is in sandbox. With your key, drop tenant_id; the key names the tenant.
Send Authorization: Bearer agk_vendor_.... The key binds every request to your tenant, counts toward that tenant's fleet totals and may open approvals. Without a key the gate runs only in sandbox for the public demos: tenant sandbox or demo, nothing stored, receipts marked sandbox: true. Any other tenant without a key is a 401. Keys are issued on the order form and stored hashed; they never appear in receipts.
This example runs in sandbox, so you can paste it as is.
# a $50 late-fee waiver; the policy holds anything above $0 for a person
curl -s https://agentguard.run/api/gate/authorize \
-H "content-type: application/json" -d '{
"tenant_id":"sandbox","agent_id":"storage-bot","capability_claim":"data_write",
"action":{"type":"waive_fee","amount_cents":5000,"currency":"USD","target_system":"property_management"},
"policy":{"id":"storage-bot-v1","version":1,"window":"per_day","window_cap_cents":50000,
"fleet_caps":{"money_cents_per_day":200000,"contacts_per_day":400},
"rules":{"waive_fee":{"required_capability":"data_write","review_above_cents":0,"hard_block_above_cents":50000},
"issue_refund":{"required_capability":"data_write","review_above_cents":10000,"hard_block_above_cents":50000}}}
}' | jq '{decision, decision_text, receipt_id: .receipt.receipt_id}'
Contact actions are place_call and send_sms. The recipient reference and the consent reference are ids or hashes your system already holds; the gate never receives a number. In sandbox you can supply recipient_contacts_24h to simulate the per-recipient count; with a key the gate keeps that count itself.
# a collections call at 9:15 PM in the recipient's time zone
curl -s https://agentguard.run/api/gate/authorize \
-H "content-type: application/json" -d '{
"tenant_id":"sandbox","agent_id":"collections-bot","capability_claim":"data_write",
"evaluated_at":"2026-10-19T01:15:00.000Z",
"action":{"type":"place_call","recipient_ref":"rcp_7f3a9c","recipient_tz":"America/New_York","consent_ref":"cns_01HZX"},
"policy":{"id":"storage-bot-v1","version":1,
"calling_window":{"start":"08:00","end":"20:00"},"consent_required":true,
"velocity_cap":{"per_recipient_per_24h":2},"fleet_caps":{"contacts_per_day":400},
"rules":{"place_call":{"required_capability":"data_write"},"send_sms":{"required_capability":"data_write"}}}
}' | jq '{decision, decision_text, local: .receipt.recipient_local_time_at_evaluation}'
{
"decision": "escalate", // allow | escalate | block
"decision_reason": "amount_above_review_threshold",
"decision_text": "held for a person under the policy",
"spend_reserved_cents": 0, // money counts only on allow
"on_unavailable_effective": "stop", // your policy's setting for this action type
"receipt": { "receipt_type": "money_action_authorization", "receipt_id": "rcpt_...", "tenant_id": "...",
"policy_hash": "...", "decision": "escalate", "signature": "...", "public_key_hex": "..." },
"approval": { "id": "apr_...", "expires_at": "...", "link": "https://agentguard.run/api/gate/approvals/apr_...?t=...",
"status_url": "https://agentguard.run/api/gate/approvals/apr_.../status" },
"approval_delivery": { "email": { "sent": true, "reason": null }, "sms": null },
"verify_url": "https://agentguard.run/verify"
}
| Field | Applies to | Effect |
|---|---|---|
rules[type].required_capability | all | Ladder read_only < data_write < payment_initiate < payment_execute. A claim below the rule's level blocks. |
rules[type].review_above_cents | money | Above this amount the action escalates to a person. Set 0 to hold every waiver or credit. |
rules[type].hard_block_above_cents | money | Above this amount the action blocks, no person asked. |
window_cap_cents | money | Daily ceiling for one agent's allowed money, supplied by your runtime as window_spent_cents. |
calling_window.start / end | contact | Recipient local time, start inclusive, end exclusive. An unknown time zone escalates instead of guessing. |
consent_required | contact | Blocks any call or text without a consent_ref. Per-action override under rules[type]. |
velocity_cap.per_recipient_per_24h | contact | Blocks the next contact to the same recipient once the count is reached. |
fleet_caps.contacts_per_day, fleet_caps.money_cents_per_day | all | Tenant-wide daily totals across every agent. See Fleet limits and pause. |
on_unavailable[type], on_unavailable.default | all | Your own answer to "what if the gate does not reply": proceed or stop, default stop. The gate never decides this; it echoes the setting back as on_unavailable_effective so your runtime can honor it on a timeout and your records can show the gap. |
When the decision is escalate, the response carries approval.id and approval.status_url, never the link. The link goes only to the approver set on your vendor key: an email address, and a phone number where SMS delivery is enabled for your tenant. We set these with you when we issue the key; a request cannot name its own approver, so an agent cannot route an approval to itself. If your own server shows the owner an Approve button (a platform connector, for example), we can turn on the link in the response for that server's key only. Never give that key to an agent. The link works once and expires in 24 hours. The person sees the action and two buttons. Their decision is signed and chained to the request receipt, with the approver's label and time. Your agent polls the status with its key:
curl -s https://agentguard.run/api/gate/approvals/apr_YOUR_ID/status \
-H "authorization: Bearer $AGENTGUARD_VENDOR_KEY" | jq '{status, decided_at, approver_label}'
# status: pending | approved | denied | expired; receipt holds the signed decision once settled
Fleet caps are tenant-wide and reset each UTC day. When fleet_caps.contacts_per_day is reached, every further call or text for the tenant returns block with the text "fleet cap reached for today in the policy"; fleet_caps.money_cents_per_day does the same for allowed refunds, credits and waivers, counting approved escalations too. An operator can also pause a tenant: every action then returns block with "tenant paused by operator" until the pause is cleared. Pauses are set by us on the operator's request today, through an administered endpoint; a self-serve switch is on the order form.
| Decision | Text on the receipt |
|---|---|
| allow | permitted under the policy |
| escalate | held for a person under the policy; recipient time zone not recognized, held for a person |
| block | above the per-action limit set in the policy; above the daily money cap set in the policy; outside the calling window set in the policy; no consent reference supplied, the policy requires one; recipient contact count reached the cap set in the policy; capability below the level the policy requires; fleet cap reached for today in the policy; tenant paused by operator |
| allow / block by a person | permitted by a person under the policy; denied by a person under the policy |
jq '{receipt: .receipt}' response.json | curl -s https://agentguard.run/api/verify \
-H "content-type: application/json" -d @- | jq .ok
# change one byte of the receipt and run it again: ok becomes false
Every response carries its receipt, and a decision receipt arrives in the status poll. Keep them with your records; they verify without us. A per-tenant export shaped for an operator, an insurer or an auditor is part of the order form, not a public endpoint yet.
Draft terms for early access. The signed order form controls.
on_unavailable choice, proceed or stop. When the gate cannot answer, the vendor's runtime applies that choice and the receipt chain shows the gap. No uptime is promised in early access.The first partners start with a 30-day pilot at 1 to 3 locations. We agree the goal and the price up front and write both on the order form. It takes one engineer's afternoon to wire the call and a real operator policy to run. You keep every receipt whatever happens next.
Answer one question about what your agent does for an operator. We reply with a starting policy that fits, in the request shape above, and the page shows where to look first.