# AgentGuard gate webhook, Python 3, standard library only. Copy this file, set two env vars, run it. # # AGENTGUARD_VENDOR_KEY=agk_vendor_... (from your order form; leave unset to run in sandbox) # PORT=8787 # # Your agent platform calls POST http://your-host:8787/gate on each tool call with a small event: # {"tool": "waive_fee", "arguments": {"amount_cents": 5000, "currency": "USD"}, "tenant_id": "acme", "agent_id": "storage-bot"} # The handler maps it to a gate request, asks https://agentguard.run/api/gate/authorize, and answers: # {"proceed": true} on allow # {"proceed": false, "approval_id": "apr_...", "status_url": "..."} on escalate; the person gets the link, not the agent # {"proceed": false, "reason": "..."} on block # If the gate does not answer within 2 seconds, the vendor's own on_unavailable setting in POLICY decides # (proceed or stop) and a note is written to stderr. Metadata only: never put names, numbers, transcripts # or card data in the event. import json import os import sys import urllib.request import urllib.error from http.server import BaseHTTPRequestHandler, HTTPServer GATE = os.environ.get("AGENTGUARD_GATE_URL", "https://agentguard.run/api/gate/authorize") KEY = os.environ.get("AGENTGUARD_VENDOR_KEY", "") TIMEOUT_S = 2.0 # The operator's policy. Replace with the one from your order form, or fetch gate_policy_example. 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}, "calling_window": {"start": "08:00", "end": "20:00"}, "consent_required": True, "velocity_cap": {"per_recipient_per_24h": 2}, "on_unavailable": {"default": "stop", "place_call": "proceed", "send_sms": "proceed"}, "rules": { "waive_fee": {"required_capability": "data_write", "review_above_cents": 0, "hard_block_above_cents": 50000}, "apply_credit": {"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}, "place_call": {"required_capability": "data_write"}, "send_sms": {"required_capability": "data_write"}, }, } # Map the platform's tool names to gate action types here if they differ. TOOL_TO_ACTION = {"waive_late_fee": "waive_fee", "refund": "issue_refund", "call": "place_call", "sms": "send_sms"} META_KEYS = ("amount_cents", "currency", "target_system", "recipient_ref", "recipient_tz", "consent_ref") def on_unavailable(action_type): table = POLICY.get("on_unavailable") or {} choice = table.get(action_type) if choice in ("proceed", "stop"): return choice return "proceed" if table.get("default") == "proceed" else "stop" def to_gate_request(event): args = event.get("arguments") or {} action = {"type": TOOL_TO_ACTION.get(event.get("tool"), event.get("tool"))} for key in META_KEYS: if key in args: action[key] = args[key] body = {"agent_id": event.get("agent_id") or "agent", "capability_claim": "data_write", "action": action, "policy": POLICY} if not KEY: body["tenant_id"] = "sandbox" return body def ask_gate(body): data = json.dumps(body).encode("utf-8") headers = {"content-type": "application/json"} if KEY: headers["authorization"] = "Bearer " + KEY req = urllib.request.Request(GATE, data=data, headers=headers, method="POST") try: with urllib.request.urlopen(req, timeout=TIMEOUT_S) as res: return res.status, json.loads(res.read().decode("utf-8")) except urllib.error.HTTPError as err: return err.code, json.loads(err.read().decode("utf-8") or "{}") def decide(event): gate_req = to_gate_request(event) action_type = gate_req["action"]["type"] try: status, body = ask_gate(gate_req) except Exception as err: # timeout, DNS, connection choice = on_unavailable(action_type) sys.stderr.write(f"agentguard gate unavailable ({err}); on_unavailable for {action_type} is {choice}\n") return {"proceed": choice == "proceed", "reason": "gate_unavailable", "on_unavailable": choice, "gap": True} if status != 200 or "decision" not in body: return {"proceed": False, "reason": body.get("error", "gate_refused"), "http": status} receipt = body.get("receipt") or {} if body["decision"] == "allow": return {"proceed": True, "receipt_id": receipt.get("receipt_id"), "text": body.get("decision_text")} if body["decision"] == "escalate": approval = body.get("approval") or {} return {"proceed": False, "approval_id": approval.get("id"), "status_url": approval.get("status_url"), "text": body.get("decision_text")} return {"proceed": False, "reason": body.get("decision_text"), "receipt_id": receipt.get("receipt_id")} class Handler(BaseHTTPRequestHandler): def do_POST(self): if self.path != "/gate": self.send_response(404); self.end_headers(); return length = int(self.headers.get("content-length") or 0) if length > 65536: self.send_response(413); self.end_headers(); return try: event = json.loads(self.rfile.read(length).decode("utf-8")) except Exception: self.send_response(400); self.end_headers(); self.wfile.write(b'{"error":"bad_json"}'); return out = json.dumps(decide(event)).encode("utf-8") self.send_response(200) self.send_header("content-type", "application/json") self.send_header("content-length", str(len(out))) self.end_headers() self.wfile.write(out) def log_message(self, *args): pass if __name__ == "__main__": port = int(os.environ.get("PORT") or 8787) sys.stderr.write("gate webhook listening\n") HTTPServer(("0.0.0.0", port), Handler).serve_forever()