// AgentGuard gate webhook, Node, no dependencies. 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" } // This 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 local note is written to stderr. Metadata only: never put names, numbers, // transcripts or card data in the event. 'use strict'; const http = require('node:http'); const https = require('node:https'); const GATE = new URL(process.env.AGENTGUARD_GATE_URL || 'https://agentguard.run/api/gate/authorize'); const KEY = process.env.AGENTGUARD_VENDOR_KEY || ''; const TIMEOUT_MS = 2000; // The operator's policy. Replace with the one from your order form, or fetch gate_policy_example. const 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. const TOOL_TO_ACTION = { waive_late_fee: 'waive_fee', refund: 'issue_refund', call: 'place_call', sms: 'send_sms' }; function onUnavailable(type) { const t = POLICY.on_unavailable || {}; return t[type] === 'proceed' || t[type] === 'stop' ? t[type] : (t.default === 'proceed' ? 'proceed' : 'stop'); } function toGateRequest(event) { const type = TOOL_TO_ACTION[event.tool] || event.tool; const a = event.arguments || {}; const action = { type }; for (const k of ['amount_cents', 'currency', 'target_system', 'recipient_ref', 'recipient_tz', 'consent_ref']) if (a[k] !== undefined) action[k] = a[k]; return { tenant_id: KEY ? undefined : 'sandbox', agent_id: event.agent_id || 'agent', capability_claim: 'data_write', action, policy: POLICY, }; } function askGate(body) { return new Promise((resolve, reject) => { const data = JSON.stringify(body); const req = https.request(GATE, { method: 'POST', headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(data), ...(KEY ? { authorization: 'Bearer ' + KEY } : {}) }, timeout: TIMEOUT_MS }, res => { let text = ''; res.on('data', c => { text += c; }); res.on('end', () => { try { resolve({ status: res.statusCode, body: JSON.parse(text) }); } catch (e) { reject(e); } }); }); req.on('timeout', () => req.destroy(new Error('gate_timeout'))); req.on('error', reject); req.end(data); }); } async function decide(event) { const gateReq = toGateRequest(event); try { const { status, body } = await askGate(gateReq); if (status !== 200 || !body.decision) return { proceed: false, reason: body.error || 'gate_refused', http: status }; if (body.decision === 'allow') return { proceed: true, receipt_id: body.receipt.receipt_id, text: body.decision_text }; if (body.decision === 'escalate') return { proceed: false, approval_id: body.approval ? body.approval.id : null, status_url: body.approval ? body.approval.status_url : null, text: body.decision_text }; return { proceed: false, reason: body.decision_text, receipt_id: body.receipt && body.receipt.receipt_id }; } catch (error) { const choice = onUnavailable(gateReq.action.type); process.stderr.write(`agentguard gate unavailable (${error.message}); on_unavailable for ${gateReq.action.type} is ${choice}\n`); return { proceed: choice === 'proceed', reason: 'gate_unavailable', on_unavailable: choice, gap: true }; } } http.createServer((req, res) => { if (req.method !== 'POST' || req.url !== '/gate') { res.writeHead(404); return res.end(); } let text = ''; req.on('data', c => { text += c; if (text.length > 65536) req.destroy(); }); req.on('end', async () => { let event; try { event = JSON.parse(text); } catch { res.writeHead(400); return res.end('{"error":"bad_json"}'); } const out = await decide(event); res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify(out)); }); }).listen(Number(process.env.PORT) || 8787, () => process.stderr.write('gate webhook listening\n'));