Skip to content

Commit f7ebdd7

Browse files
committed
feat(governance): Phase 20 - Agent Governance Layer (Layer 3 above WAB Protocol)
Introduces the missing governance layer that turns WAB from a web-interaction protocol into a full Agent OS. Five primitives with deep, server-enforced state: 1) Permission Boundaries - resource x action x scope policies with effect 2) Approval Gates - async human-in-the-loop for sensitive actions 3) Tamper-Evident Audit - HMAC hash chain (verifyAuditChain detects tampering) 4) Kill Switch - per-agent emergency stop, cancels pending approvals 5) Spend & Rate Limits - per-call cap, rolling 24h cap, per-minute rate New files: - server/migrations/007_governance.sql (6 tables, hash-chained audit) - server/services/governance.js (full service, redaction, HMAC chain) - server/routes/governance.js (mounted at /api/governance) - sdk/governance.js (WABGovernance class + .guard()) - examples/governance-agent.js (9-step demo: register->policies-> deny->allow->approve->audit->kill) Wired: - server/index.js: mounted /api/governance - sdk/index.js: exports WABGovernance, WABGovernanceError Verified end-to-end: migration applies, allow/deny/over-cap/no-policy decisions correct, HMAC chain verifies, kill switch blocks subsequent actions.
1 parent 8ea475e commit f7ebdd7

7 files changed

Lines changed: 1206 additions & 0 deletions

File tree

examples/governance-agent.js

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
#!/usr/bin/env node
2+
/**
3+
* WAB Governance Demo
4+
* ───────────────────
5+
* Walks through the full Layer-3 governance pipeline:
6+
*
7+
* 1) register agent → get one-time token
8+
* 2) define permission boundaries (Stripe read-only + refund <$50 + ClickUp write)
9+
* 3) try a forbidden action → DENIED
10+
* 4) try an allowed action → ALLOWED + audited
11+
* 5) try a high-value refund → APPROVAL_REQUIRED → human approves → executed
12+
* 6) verify audit chain → tamper-evident
13+
* 7) kill switch → all subsequent actions DENIED
14+
*
15+
* Run:
16+
* node examples/governance-agent.js
17+
* WAB_API=http://localhost:3000 node examples/governance-agent.js
18+
*/
19+
20+
'use strict';
21+
22+
const { WABGovernance } = require('../sdk');
23+
24+
const API = process.env.WAB_API || 'http://localhost:3000';
25+
26+
const c = {
27+
reset: '\x1b[0m', dim: '\x1b[2m', bold: '\x1b[1m',
28+
green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m',
29+
blue: '\x1b[34m', cyan: '\x1b[36m', magenta: '\x1b[35m',
30+
};
31+
const log = (msg) => console.log(msg);
32+
const head = (n, msg) => log(`\n${c.bold}${c.cyan}── ${n}. ${msg} ──${c.reset}`);
33+
const ok = (m) => log(` ${c.green}${c.reset} ${m}`);
34+
const no = (m) => log(` ${c.red}${c.reset} ${m}`);
35+
const info = (m) => log(` ${c.dim}${m}${c.reset}`);
36+
37+
async function main() {
38+
log(`${c.bold}${c.magenta}WAB Agent Governance Demo${c.reset} ${c.dim}(API: ${API})${c.reset}`);
39+
40+
// ── 1) Register a fresh agent identity
41+
head(1, 'Register agent');
42+
const reg = await WABGovernance.register({
43+
apiBase: API,
44+
displayName: 'Demo Agent — Stripe + ClickUp',
45+
metadata: { demo: true, created: Date.now() },
46+
});
47+
ok(`agent_id = ${reg.agent_id}`);
48+
ok(`agent_token (shown ONCE) = ${reg.agent_token.slice(0, 12)}…`);
49+
50+
const gov = new WABGovernance({
51+
apiBase: API,
52+
agentId: reg.agent_id,
53+
agentToken: reg.agent_token,
54+
// Auto-approve in this demo (a real app would post to Slack/email).
55+
onApprovalRequired: async (req) => {
56+
info(`[human approval] resource=${req.resource} action=${req.action} amount=${req.amount}`);
57+
info('[human approval] auto-approving in demo (3s think...)');
58+
await sleep(3000);
59+
return 'approved';
60+
},
61+
approvalTimeoutMs: 30_000,
62+
});
63+
64+
// ── 2) Define policies
65+
head(2, 'Define permission boundaries');
66+
await gov.definePolicy({ resource: 'stripe', action: 'read', scope: 'customers' });
67+
ok('stripe:read on customers');
68+
await gov.definePolicy({
69+
resource: 'stripe', action: 'write', scope: 'refunds',
70+
max_amount: 50, currency: 'USD', daily_cap: 200,
71+
});
72+
ok('stripe:write on refunds (max $50/call, $200/day)');
73+
await gov.definePolicy({
74+
resource: 'stripe', action: 'write', scope: 'refunds-large',
75+
max_amount: 5000, currency: 'USD', requires_approval: true,
76+
});
77+
ok('stripe:write on refunds-large (≤$5000, REQUIRES HUMAN APPROVAL)');
78+
await gov.definePolicy({
79+
resource: 'clickup', action: 'write', scope: 'tasks', per_call_rate: 30,
80+
});
81+
ok('clickup:write on tasks (rate-limited 30/min)');
82+
83+
// ── 3) Forbidden action
84+
head(3, 'Attempt forbidden action: gmail:write');
85+
try {
86+
await gov.guard({ resource: 'gmail', action: 'write', scope: 'inbox' },
87+
async () => { throw new Error('should not run'); });
88+
no('UNEXPECTED: action ran (governance failed)');
89+
} catch (e) {
90+
ok(`correctly blocked: ${e.message}`);
91+
}
92+
93+
// ── 4) Allowed read
94+
head(4, 'Allowed action: stripe:read on customers');
95+
const r = await gov.guard(
96+
{ resource: 'stripe', action: 'read', scope: 'customers' },
97+
async () => ({ count: 12, sample: [{ id: 'cus_abc', email: 'demo@x' }] }),
98+
);
99+
ok(`executed in ${r.elapsed_ms}ms — result keys: ${Object.keys(r.result).join(', ')}`);
100+
101+
// ── 5) Small refund: under cap, no approval
102+
head(5, 'Small refund: $9.99 (under cap)');
103+
const r2 = await gov.guard(
104+
{ resource: 'stripe', action: 'write', scope: 'refunds',
105+
amount: 9.99, currency: 'USD',
106+
params: { charge: 'ch_xyz', reason: 'duplicate' } },
107+
async () => ({ refund_id: 're_demo_' + Date.now(), status: 'succeeded' }),
108+
);
109+
ok(`refund posted: ${r2.result.refund_id}`);
110+
111+
// ── 6) Refund OVER per-call cap → DENIED instantly
112+
head(6, 'Refund $9999 with cap=$50 → DENY');
113+
try {
114+
await gov.guard(
115+
{ resource: 'stripe', action: 'write', scope: 'refunds',
116+
amount: 9999, currency: 'USD' },
117+
async () => 'should not run',
118+
);
119+
no('UNEXPECTED: action ran');
120+
} catch (e) {
121+
ok(`correctly blocked: ${e.message}`);
122+
}
123+
124+
// ── 7) Large refund routed through approval gate
125+
head(7, 'Large refund $499.99 → APPROVAL GATE');
126+
const r3 = await gov.guard(
127+
{ resource: 'stripe', action: 'write', scope: 'refunds-large',
128+
amount: 499.99, currency: 'USD',
129+
params: { charge: 'ch_big', reason: 'fraud_dispute' },
130+
reason: 'high_value_refund_requires_review' },
131+
async () => ({ refund_id: 're_big_' + Date.now(), status: 'succeeded' }),
132+
);
133+
ok(`approved + executed: ${r3.result.refund_id}`);
134+
135+
// ── 8) Audit log + chain verification
136+
head(8, 'Audit log + tamper check');
137+
const audit = await gov.getAudit({ limit: 20 });
138+
info(`last ${audit.audit.length} events:`);
139+
for (const ev of audit.audit.slice(0, 8)) {
140+
const tag = ev.decision === 'deny' ? c.red : ev.decision === 'pending' ? c.yellow : c.green;
141+
log(` ${tag}${(ev.decision || '·').padEnd(8)}${c.reset}` +
142+
` ${(ev.event_type || '').padEnd(18)} ${ev.resource || ''}/${ev.action || ''}` +
143+
` ${ev.scope ? '['+ev.scope+'] ' : ''}${ev.amount ? '$'+ev.amount : ''}`);
144+
}
145+
const v = await gov.verifyAudit();
146+
if (v.ok) ok(`chain verified: ${v.count} entries, head=${(v.head || '').slice(0, 12)}…`);
147+
else no(`chain BROKEN at id=${v.broken_at}`);
148+
149+
// ── 9) Kill switch
150+
head(9, 'Kill switch');
151+
await gov.kill('demo_complete');
152+
ok('agent killed');
153+
try {
154+
await gov.guard({ resource: 'stripe', action: 'read', scope: 'customers' },
155+
async () => 'should not run');
156+
no('UNEXPECTED: action ran after kill');
157+
} catch (e) {
158+
ok(`post-kill action blocked: ${e.message}`);
159+
}
160+
161+
log(`\n${c.bold}${c.green}✓ Demo complete${c.reset}\n`);
162+
}
163+
164+
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
165+
166+
main().catch((e) => {
167+
console.error(`\n${c.red}Demo failed:${c.reset}`, e.message);
168+
process.exit(1);
169+
});

0 commit comments

Comments
 (0)