web-agent-bridge 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +84 -72
- package/README.ar.md +1304 -1152
- package/README.md +298 -1635
- package/bin/agent-runner.js +474 -474
- package/bin/cli.js +237 -138
- package/bin/wab-init.js +223 -0
- package/bin/wab.js +80 -80
- package/examples/azure-dns-wab.js +83 -0
- package/examples/bidi-agent.js +119 -119
- package/examples/cloudflare-wab-dns.js +121 -0
- package/examples/cpanel-wab-dns.js +114 -0
- package/examples/cross-site-agent.js +91 -91
- package/examples/dns-discovery-agent.js +166 -0
- package/examples/gcp-dns-wab.js +76 -0
- package/examples/governance-agent.js +169 -0
- package/examples/mcp-agent.js +94 -94
- package/examples/next-app-router/README.md +44 -44
- package/examples/plesk-wab-dns.js +103 -0
- package/examples/puppeteer-agent.js +108 -108
- package/examples/route53-wab-dns.js +144 -0
- package/examples/saas-dashboard/README.md +55 -55
- package/examples/safe-mode-agent.js +96 -0
- package/examples/shopify-hydrogen/README.md +74 -74
- package/examples/vision-agent.js +171 -171
- package/examples/wab-sign.js +74 -0
- package/examples/wab-verify.js +60 -0
- package/examples/wordpress-elementor/README.md +77 -77
- package/package.json +19 -6
- package/public/.well-known/agent-tools.json +180 -180
- package/public/.well-known/ai-assets.json +59 -59
- package/public/.well-known/security.txt +8 -0
- package/public/.well-known/wab.json +28 -0
- package/public/activate.html +368 -0
- package/public/adoption-metrics.html +188 -0
- package/public/agent-workspace.html +349 -349
- package/public/ai.html +198 -198
- package/public/api.html +413 -412
- package/public/azure-dns-integration.html +289 -0
- package/public/browser.html +486 -486
- package/public/cloudflare-integration.html +380 -0
- package/public/commander-dashboard.html +243 -243
- package/public/cookies.html +210 -210
- package/public/cpanel-integration.html +398 -0
- package/public/css/agent-workspace.css +1713 -1713
- package/public/css/premium.css +317 -317
- package/public/css/styles.css +1263 -1235
- package/public/dashboard.html +707 -706
- package/public/dns.html +436 -0
- package/public/docs.html +588 -587
- package/public/feed.xml +89 -89
- package/public/gcp-dns-integration.html +318 -0
- package/public/growth.html +465 -463
- package/public/index.html +1266 -982
- package/public/integrations.html +556 -0
- package/public/js/activate.js +145 -0
- package/public/js/agent-workspace.js +1740 -1740
- package/public/js/auth-nav.js +65 -31
- package/public/js/auth-redirect.js +12 -12
- package/public/js/cookie-consent.js +56 -56
- package/public/js/dns.js +438 -0
- package/public/js/wab-demo-page.js +721 -721
- package/public/js/ws-client.js +74 -74
- package/public/llms-full.txt +360 -360
- package/public/llms.txt +125 -125
- package/public/login.html +85 -85
- package/public/mesh-dashboard.html +328 -328
- package/public/openapi.json +669 -580
- package/public/phone-shield.html +281 -0
- package/public/plesk-integration.html +375 -0
- package/public/premium-dashboard.html +2489 -2489
- package/public/premium.html +793 -793
- package/public/privacy.html +297 -297
- package/public/provider-onboarding.html +172 -0
- package/public/provider-sandbox.html +134 -0
- package/public/providers.html +359 -0
- package/public/register.html +105 -105
- package/public/registrar-integrations.html +141 -0
- package/public/robots.txt +99 -87
- package/public/route53-integration.html +531 -0
- package/public/script/wab-consent.d.ts +36 -36
- package/public/script/wab-consent.js +104 -104
- package/public/script/wab-schema.js +131 -131
- package/public/script/wab.d.ts +108 -108
- package/public/script/wab.min.js +580 -580
- package/public/security.txt +8 -0
- package/public/shieldqr.html +231 -0
- package/public/sitemap.xml +6 -0
- package/public/terms.html +256 -256
- package/public/wab-trust.html +200 -0
- package/public/wab-vs-protocols.html +210 -0
- package/public/whitepaper.html +449 -0
- package/script/ai-agent-bridge.js +1754 -1754
- package/sdk/README.md +99 -99
- package/sdk/agent-mesh.js +449 -449
- package/sdk/auto-discovery.js +288 -0
- package/sdk/commander.js +262 -262
- package/sdk/governance.js +262 -0
- package/sdk/index.d.ts +464 -464
- package/sdk/index.js +25 -1
- package/sdk/multi-agent.js +318 -318
- package/sdk/package.json +2 -2
- package/sdk/safe-mode.js +221 -0
- package/sdk/safety-shield.js +219 -0
- package/sdk/schema-discovery.js +83 -83
- package/server/adapters/index.js +520 -520
- package/server/config/plans.js +367 -367
- package/server/config/secrets.js +102 -102
- package/server/control-plane/index.js +301 -301
- package/server/data-plane/index.js +354 -354
- package/server/index.js +670 -427
- package/server/llm/index.js +404 -404
- package/server/middleware/adminAuth.js +35 -35
- package/server/middleware/auth.js +50 -50
- package/server/middleware/featureGate.js +88 -88
- package/server/middleware/rateLimits.js +100 -100
- package/server/middleware/sensitiveAction.js +157 -0
- package/server/migrations/001_add_analytics_indexes.sql +7 -7
- package/server/migrations/002_premium_features.sql +418 -418
- package/server/migrations/003_ads_integer_cents.sql +33 -33
- package/server/migrations/004_agent_os.sql +158 -158
- package/server/migrations/005_marketplace_metering.sql +126 -126
- package/server/migrations/007_governance.sql +106 -0
- package/server/migrations/008_plans.sql +144 -0
- package/server/migrations/009_shieldqr.sql +30 -0
- package/server/migrations/010_extended_trust.sql +33 -0
- package/server/models/adapters/index.js +33 -33
- package/server/models/adapters/mysql.js +183 -183
- package/server/models/adapters/postgresql.js +172 -172
- package/server/models/adapters/sqlite.js +7 -7
- package/server/models/db.js +740 -681
- package/server/observability/failure-analysis.js +337 -337
- package/server/observability/index.js +394 -394
- package/server/protocol/capabilities.js +223 -223
- package/server/protocol/index.js +243 -243
- package/server/protocol/schema.js +584 -584
- package/server/registry/certification.js +271 -271
- package/server/registry/index.js +326 -326
- package/server/routes/admin-plans.js +76 -0
- package/server/routes/admin-premium.js +673 -671
- package/server/routes/admin-shieldqr.js +90 -0
- package/server/routes/admin-trust-monitor.js +83 -0
- package/server/routes/admin.js +549 -261
- package/server/routes/ads.js +130 -130
- package/server/routes/agent-workspace.js +540 -540
- package/server/routes/api.js +150 -150
- package/server/routes/auth.js +71 -71
- package/server/routes/billing.js +57 -45
- package/server/routes/commander.js +316 -316
- package/server/routes/demo-showcase.js +332 -332
- package/server/routes/demo-store.js +154 -0
- package/server/routes/discovery.js +2348 -417
- package/server/routes/gateway.js +173 -157
- package/server/routes/governance.js +208 -0
- package/server/routes/license.js +251 -240
- package/server/routes/mesh.js +469 -469
- package/server/routes/noscript.js +543 -543
- package/server/routes/plans.js +33 -0
- package/server/routes/premium-v2.js +686 -686
- package/server/routes/premium.js +724 -724
- package/server/routes/providers.js +650 -0
- package/server/routes/runtime.js +2148 -2147
- package/server/routes/shieldqr.js +88 -0
- package/server/routes/sovereign.js +465 -385
- package/server/routes/universal.js +200 -185
- package/server/routes/wab-api.js +850 -501
- package/server/runtime/container-worker.js +111 -111
- package/server/runtime/container.js +448 -448
- package/server/runtime/distributed-worker.js +362 -362
- package/server/runtime/event-bus.js +210 -210
- package/server/runtime/index.js +253 -253
- package/server/runtime/queue.js +599 -599
- package/server/runtime/replay.js +666 -666
- package/server/runtime/sandbox.js +266 -266
- package/server/runtime/scheduler.js +534 -534
- package/server/runtime/session-engine.js +293 -293
- package/server/runtime/state-manager.js +188 -188
- package/server/security/cross-site-redactor.js +196 -0
- package/server/security/dry-run.js +180 -0
- package/server/security/human-gate-rate-limit.js +147 -0
- package/server/security/human-gate-transports.js +178 -0
- package/server/security/human-gate.js +281 -0
- package/server/security/index.js +368 -368
- package/server/security/intent-engine.js +245 -0
- package/server/security/reward-guard.js +171 -0
- package/server/security/rollback-store.js +239 -0
- package/server/security/token-scope.js +404 -0
- package/server/security/url-policy.js +139 -0
- package/server/services/agent-chat.js +506 -506
- package/server/services/agent-learning.js +601 -575
- package/server/services/agent-memory.js +625 -625
- package/server/services/agent-mesh.js +555 -539
- package/server/services/agent-symphony.js +717 -717
- package/server/services/agent-tasks.js +1807 -1807
- package/server/services/api-key-engine.js +292 -261
- package/server/services/cluster.js +894 -894
- package/server/services/commander.js +738 -738
- package/server/services/edge-compute.js +440 -440
- package/server/services/email.js +233 -204
- package/server/services/governance.js +466 -0
- package/server/services/hosted-runtime.js +205 -205
- package/server/services/lfd.js +635 -635
- package/server/services/local-ai.js +389 -389
- package/server/services/marketplace.js +270 -270
- package/server/services/metering.js +182 -182
- package/server/services/modules/affiliate-intelligence.js +93 -93
- package/server/services/modules/agent-firewall.js +90 -90
- package/server/services/modules/bounty.js +89 -89
- package/server/services/modules/collective-bargaining.js +92 -92
- package/server/services/modules/dark-pattern.js +66 -66
- package/server/services/modules/gov-intelligence.js +45 -45
- package/server/services/modules/neural.js +55 -55
- package/server/services/modules/notary.js +49 -49
- package/server/services/modules/price-time-machine.js +86 -86
- package/server/services/modules/protocol.js +104 -104
- package/server/services/negotiation.js +439 -439
- package/server/services/plans.js +214 -0
- package/server/services/plugins.js +771 -771
- package/server/services/premium.js +1 -1
- package/server/services/price-intelligence.js +566 -566
- package/server/services/price-shield.js +1137 -1137
- package/server/services/provider-clients.js +740 -0
- package/server/services/reputation.js +465 -465
- package/server/services/search-engine.js +357 -357
- package/server/services/security.js +513 -513
- package/server/services/self-healing.js +843 -843
- package/server/services/shieldqr.js +322 -0
- package/server/services/sovereign-shield.js +542 -0
- package/server/services/ssl-inspector.js +42 -0
- package/server/services/ssl-monitor.js +167 -0
- package/server/services/stripe.js +205 -192
- package/server/services/swarm.js +788 -788
- package/server/services/universal-scraper.js +662 -661
- package/server/services/verification.js +481 -481
- package/server/services/vision.js +1163 -1163
- package/server/services/wab-crypto.js +178 -0
- package/server/utils/cache.js +125 -125
- package/server/utils/migrate.js +81 -81
- package/server/utils/safe-fetch.js +228 -0
- package/server/utils/secureFields.js +50 -50
- package/server/ws.js +161 -161
- package/templates/artisan-marketplace.yaml +104 -104
- package/templates/book-price-scout.yaml +98 -98
- package/templates/electronics-price-tracker.yaml +108 -108
- package/templates/flight-deal-hunter.yaml +113 -113
- package/templates/freelancer-direct.yaml +116 -116
- package/templates/grocery-price-compare.yaml +93 -93
- package/templates/hotel-direct-booking.yaml +113 -113
- package/templates/local-services.yaml +98 -98
- package/templates/olive-oil-tunisia.yaml +88 -88
- package/templates/organic-farm-fresh.yaml +101 -101
- package/templates/restaurant-direct.yaml +97 -97
- package/public/score.html +0 -263
- package/server/migrations/006_growth_suite.sql +0 -138
- package/server/routes/growth.js +0 -962
- package/server/services/fairness-engine.js +0 -409
- package/server/services/fairness.js +0 -420
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAB Agent Governance Service
|
|
3
|
+
* ────────────────────────────
|
|
4
|
+
* Permission Boundaries · Approval Gates · Tamper-Evident Audit Log
|
|
5
|
+
* Kill Switch · Spend & Rate Limits.
|
|
6
|
+
*
|
|
7
|
+
* Tables (created by migrations/007_governance.sql):
|
|
8
|
+
* gov_agents, gov_policies, gov_audit, gov_approvals, gov_spend, gov_rate
|
|
9
|
+
*
|
|
10
|
+
* Audit log uses an HMAC hash chain:
|
|
11
|
+
* hash_n = HMAC(secret, prev_hash || row_payload_n)
|
|
12
|
+
* Tampering with any row breaks subsequent hashes; verifyAuditChain()
|
|
13
|
+
* re-runs the chain and reports the first divergence.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const crypto = require('crypto');
|
|
19
|
+
const { db } = require('../models/db');
|
|
20
|
+
|
|
21
|
+
// ─────────────────────────────────────────── secrets ────
|
|
22
|
+
const AUDIT_SECRET = process.env.WAB_GOV_AUDIT_SECRET
|
|
23
|
+
|| process.env.WAB_HMAC_SECRET
|
|
24
|
+
|| 'wab-governance-audit-default-secret-change-me';
|
|
25
|
+
|
|
26
|
+
const TOKEN_PREFIX = 'wabag_'; // visible prefix for agent tokens
|
|
27
|
+
const APPROVAL_TTL_MS = 24 * 60 * 60 * 1000; // 24h default
|
|
28
|
+
|
|
29
|
+
// ─────────────────────────────────────────── helpers ────
|
|
30
|
+
function newId(bytes = 16) { return crypto.randomBytes(bytes).toString('hex'); }
|
|
31
|
+
function sha256(s) { return crypto.createHash('sha256').update(String(s)).digest('hex'); }
|
|
32
|
+
function hmac(payload, prevHash) {
|
|
33
|
+
return crypto.createHmac('sha256', AUDIT_SECRET)
|
|
34
|
+
.update(String(prevHash || '') + '|' + String(payload))
|
|
35
|
+
.digest('hex');
|
|
36
|
+
}
|
|
37
|
+
function nowIso() { return new Date().toISOString(); }
|
|
38
|
+
function safeJson(v) {
|
|
39
|
+
try { return v == null ? null : JSON.stringify(v); } catch { return null; }
|
|
40
|
+
}
|
|
41
|
+
function parseJson(s) {
|
|
42
|
+
if (!s) return null;
|
|
43
|
+
try { return JSON.parse(s); } catch { return null; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Redact obviously sensitive params before persisting to audit.
|
|
47
|
+
const REDACT_KEYS = /^(password|secret|token|api[_-]?key|authorization|cookie|cvv|pan|ssn)$/i;
|
|
48
|
+
function redact(obj, depth = 0) {
|
|
49
|
+
if (depth > 4 || obj == null) return obj;
|
|
50
|
+
if (Array.isArray(obj)) return obj.map((x) => redact(x, depth + 1));
|
|
51
|
+
if (typeof obj === 'object') {
|
|
52
|
+
const out = {};
|
|
53
|
+
for (const k of Object.keys(obj)) {
|
|
54
|
+
out[k] = REDACT_KEYS.test(k) ? '[redacted]' : redact(obj[k], depth + 1);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
if (typeof obj === 'string' && obj.length > 2000) return obj.slice(0, 2000) + '…[truncated]';
|
|
59
|
+
return obj;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ──────────────────────────────────────── agent registry ────
|
|
63
|
+
function registerAgent({ agentId, ownerId = null, displayName = null, metadata = null } = {}) {
|
|
64
|
+
const id = agentId || ('agent_' + newId(8));
|
|
65
|
+
const token = TOKEN_PREFIX + newId(24);
|
|
66
|
+
const tokenHash = sha256(token);
|
|
67
|
+
db.prepare(`
|
|
68
|
+
INSERT INTO gov_agents (agent_id, owner_id, display_name, token_hash, metadata)
|
|
69
|
+
VALUES (?, ?, ?, ?, ?)
|
|
70
|
+
`).run(id, ownerId, displayName, tokenHash, safeJson(metadata));
|
|
71
|
+
appendAudit({ agentId: id, eventType: 'note', reason: 'agent_registered',
|
|
72
|
+
paramsJson: safeJson({ ownerId, displayName }) });
|
|
73
|
+
return { agentId: id, agentToken: token }; // token shown ONCE
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getAgent(agentId) {
|
|
77
|
+
return db.prepare('SELECT * FROM gov_agents WHERE agent_id = ?').get(agentId) || null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Validate agent's bearer token. Returns agent row or null. */
|
|
81
|
+
function authAgent(agentId, agentToken) {
|
|
82
|
+
if (!agentId || !agentToken) return null;
|
|
83
|
+
const a = getAgent(agentId);
|
|
84
|
+
if (!a) return null;
|
|
85
|
+
if (sha256(agentToken) !== a.token_hash) return null;
|
|
86
|
+
return a;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isAlive(agentId) {
|
|
90
|
+
const a = getAgent(agentId);
|
|
91
|
+
return !!a && a.status === 'alive';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ──────────────────────────────────────────── policies ────
|
|
95
|
+
function definePolicy(p) {
|
|
96
|
+
const stmt = db.prepare(`
|
|
97
|
+
INSERT INTO gov_policies
|
|
98
|
+
(agent_id, resource, action, scope, max_amount, currency,
|
|
99
|
+
daily_cap, per_call_rate, requires_approval, effect, expires_at)
|
|
100
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
101
|
+
`);
|
|
102
|
+
const info = stmt.run(
|
|
103
|
+
p.agentId, p.resource, p.action,
|
|
104
|
+
p.scope || null,
|
|
105
|
+
p.maxAmount == null ? null : Number(p.maxAmount),
|
|
106
|
+
p.currency || 'USD',
|
|
107
|
+
p.dailyCap == null ? null : Number(p.dailyCap),
|
|
108
|
+
p.perCallRate == null ? null : Number(p.perCallRate),
|
|
109
|
+
p.requiresApproval ? 1 : 0,
|
|
110
|
+
p.effect === 'deny' ? 'deny' : 'allow',
|
|
111
|
+
p.expiresAt || null,
|
|
112
|
+
);
|
|
113
|
+
appendAudit({
|
|
114
|
+
agentId: p.agentId, eventType: 'policy_change',
|
|
115
|
+
resource: p.resource, action: p.action, scope: p.scope,
|
|
116
|
+
paramsJson: safeJson({ id: info.lastInsertRowid, ...p }),
|
|
117
|
+
reason: 'policy_added',
|
|
118
|
+
});
|
|
119
|
+
return { id: info.lastInsertRowid };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function listPolicies(agentId) {
|
|
123
|
+
return db.prepare(
|
|
124
|
+
'SELECT * FROM gov_policies WHERE agent_id = ? ORDER BY id'
|
|
125
|
+
).all(agentId);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function deletePolicy(agentId, id) {
|
|
129
|
+
const info = db.prepare(
|
|
130
|
+
'DELETE FROM gov_policies WHERE id = ? AND agent_id = ?'
|
|
131
|
+
).run(id, agentId);
|
|
132
|
+
if (info.changes) {
|
|
133
|
+
appendAudit({ agentId, eventType: 'policy_change',
|
|
134
|
+
paramsJson: safeJson({ removed: id }), reason: 'policy_removed' });
|
|
135
|
+
}
|
|
136
|
+
return info.changes > 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Match an action descriptor against the policy table.
|
|
141
|
+
* Returns the matched policy row or null. Most-specific wins:
|
|
142
|
+
* exact-scope > scope-null, exact-action > '*'.
|
|
143
|
+
*/
|
|
144
|
+
function matchPolicy(agentId, { resource, action, scope }) {
|
|
145
|
+
const rows = db.prepare(`
|
|
146
|
+
SELECT * FROM gov_policies
|
|
147
|
+
WHERE agent_id = ?
|
|
148
|
+
AND (expires_at IS NULL OR datetime(expires_at) > datetime('now'))
|
|
149
|
+
AND resource = ?
|
|
150
|
+
AND (action = ? OR action = '*')
|
|
151
|
+
AND (scope IS NULL OR scope = ?)
|
|
152
|
+
`).all(agentId, resource, action, scope || null);
|
|
153
|
+
if (!rows.length) return null;
|
|
154
|
+
rows.sort((a, b) => {
|
|
155
|
+
const aSpec = (a.scope ? 2 : 0) + (a.action !== '*' ? 1 : 0);
|
|
156
|
+
const bSpec = (b.scope ? 2 : 0) + (b.action !== '*' ? 1 : 0);
|
|
157
|
+
return bSpec - aSpec;
|
|
158
|
+
});
|
|
159
|
+
return rows[0];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─────────────────────────────────────── spend & rates ────
|
|
163
|
+
function rollingSpend(agentId, resource, windowMs = 24 * 60 * 60 * 1000) {
|
|
164
|
+
const since = new Date(Date.now() - windowMs).toISOString();
|
|
165
|
+
const r = db.prepare(`
|
|
166
|
+
SELECT COALESCE(SUM(amount), 0) AS total
|
|
167
|
+
FROM gov_spend WHERE agent_id = ? AND resource = ? AND ts >= ?
|
|
168
|
+
`).get(agentId, resource, since);
|
|
169
|
+
return r ? Number(r.total) : 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function recordSpend(agentId, resource, amount, currency = 'USD', ref = null) {
|
|
173
|
+
if (!amount || amount <= 0) return;
|
|
174
|
+
db.prepare(`
|
|
175
|
+
INSERT INTO gov_spend (agent_id, resource, amount, currency, ref)
|
|
176
|
+
VALUES (?, ?, ?, ?, ?)
|
|
177
|
+
`).run(agentId, resource, Number(amount), currency, ref);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function bumpRate(agentId, resource) {
|
|
181
|
+
// Use a sliding 60-second window with 1-second buckets (epoch seconds as
|
|
182
|
+
// window_start). Legacy ISO-minute rows cast to a small integer (the year)
|
|
183
|
+
// and therefore never fall inside `nowSec - 60`, so they are ignored.
|
|
184
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
185
|
+
db.prepare(`
|
|
186
|
+
INSERT INTO gov_rate (agent_id, resource, window_start, count)
|
|
187
|
+
VALUES (?, ?, ?, 1)
|
|
188
|
+
ON CONFLICT(agent_id, resource, window_start) DO UPDATE SET count = count + 1
|
|
189
|
+
`).run(agentId, resource, String(nowSec));
|
|
190
|
+
// Garbage-collect rows older than 5 minutes for this (agent, resource).
|
|
191
|
+
db.prepare(`
|
|
192
|
+
DELETE FROM gov_rate
|
|
193
|
+
WHERE agent_id = ? AND resource = ?
|
|
194
|
+
AND CAST(window_start AS INTEGER) <= ?
|
|
195
|
+
`).run(agentId, resource, nowSec - 300);
|
|
196
|
+
const row = db.prepare(`
|
|
197
|
+
SELECT COALESCE(SUM(count), 0) AS c FROM gov_rate
|
|
198
|
+
WHERE agent_id = ? AND resource = ?
|
|
199
|
+
AND CAST(window_start AS INTEGER) > ?
|
|
200
|
+
`).get(agentId, resource, nowSec - 60);
|
|
201
|
+
return row?.c || 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ───────────────────────────────────────── audit chain ────
|
|
205
|
+
function lastAuditHash(agentId) {
|
|
206
|
+
const r = db.prepare(`
|
|
207
|
+
SELECT hash FROM gov_audit WHERE agent_id = ? ORDER BY id DESC LIMIT 1
|
|
208
|
+
`).get(agentId);
|
|
209
|
+
return r ? r.hash : null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function appendAudit(ev) {
|
|
213
|
+
const ts = nowIso();
|
|
214
|
+
const prev = lastAuditHash(ev.agentId);
|
|
215
|
+
const payload = [
|
|
216
|
+
ev.agentId, ts, ev.eventType, ev.resource || '', ev.action || '',
|
|
217
|
+
ev.scope || '', ev.amount || '', ev.currency || '',
|
|
218
|
+
ev.decision || '', ev.reason || '',
|
|
219
|
+
ev.paramsJson || '', ev.resultJson || '',
|
|
220
|
+
].join('|');
|
|
221
|
+
const hash = hmac(payload, prev);
|
|
222
|
+
const info = db.prepare(`
|
|
223
|
+
INSERT INTO gov_audit (
|
|
224
|
+
agent_id, ts, event_type, resource, action, scope, amount, currency,
|
|
225
|
+
decision, reason, params_json, result_json, prev_hash, hash
|
|
226
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
227
|
+
`).run(
|
|
228
|
+
ev.agentId, ts, ev.eventType,
|
|
229
|
+
ev.resource || null, ev.action || null, ev.scope || null,
|
|
230
|
+
ev.amount == null ? null : Number(ev.amount),
|
|
231
|
+
ev.currency || null,
|
|
232
|
+
ev.decision || null, ev.reason || null,
|
|
233
|
+
ev.paramsJson || null, ev.resultJson || null,
|
|
234
|
+
prev, hash,
|
|
235
|
+
);
|
|
236
|
+
return { id: info.lastInsertRowid, ts, hash, prev_hash: prev };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function getAudit(agentId, { limit = 200, since = null, eventType = null } = {}) {
|
|
240
|
+
const filters = ['agent_id = ?'];
|
|
241
|
+
const params = [agentId];
|
|
242
|
+
if (since) { filters.push('ts >= ?'); params.push(since); }
|
|
243
|
+
if (eventType) { filters.push('event_type = ?'); params.push(eventType); }
|
|
244
|
+
return db.prepare(
|
|
245
|
+
`SELECT * FROM gov_audit WHERE ${filters.join(' AND ')}
|
|
246
|
+
ORDER BY id DESC LIMIT ?`
|
|
247
|
+
).all(...params, Math.min(1000, limit));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Recompute the chain and detect tampering. */
|
|
251
|
+
function verifyAuditChain(agentId) {
|
|
252
|
+
const rows = db.prepare(
|
|
253
|
+
'SELECT * FROM gov_audit WHERE agent_id = ? ORDER BY id ASC'
|
|
254
|
+
).all(agentId);
|
|
255
|
+
let prev = null;
|
|
256
|
+
for (const r of rows) {
|
|
257
|
+
const payload = [
|
|
258
|
+
r.agent_id, r.ts, r.event_type, r.resource || '', r.action || '',
|
|
259
|
+
r.scope || '', r.amount || '', r.currency || '',
|
|
260
|
+
r.decision || '', r.reason || '',
|
|
261
|
+
r.params_json || '', r.result_json || '',
|
|
262
|
+
].join('|');
|
|
263
|
+
const expect = hmac(payload, prev);
|
|
264
|
+
if (r.prev_hash !== prev || r.hash !== expect) {
|
|
265
|
+
return { ok: false, broken_at: r.id, expected_prev: prev, expected_hash: expect };
|
|
266
|
+
}
|
|
267
|
+
prev = r.hash;
|
|
268
|
+
}
|
|
269
|
+
return { ok: true, count: rows.length, head: prev };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ──────────────────────────────────────── kill switch ────
|
|
273
|
+
function killAgent(agentId, reason = 'manual') {
|
|
274
|
+
const info = db.prepare(`
|
|
275
|
+
UPDATE gov_agents
|
|
276
|
+
SET status = 'killed', killed_at = datetime('now'),
|
|
277
|
+
killed_reason = ?, updated_at = datetime('now')
|
|
278
|
+
WHERE agent_id = ?
|
|
279
|
+
`).run(reason, agentId);
|
|
280
|
+
if (info.changes) {
|
|
281
|
+
appendAudit({ agentId, eventType: 'kill', reason, decision: 'deny' });
|
|
282
|
+
// Cancel any pending approvals so an attacker can't resurrect via approval.
|
|
283
|
+
db.prepare(`
|
|
284
|
+
UPDATE gov_approvals SET status = 'cancelled', decided_at = datetime('now'),
|
|
285
|
+
decided_note = 'agent_killed'
|
|
286
|
+
WHERE agent_id = ? AND status = 'pending'
|
|
287
|
+
`).run(agentId);
|
|
288
|
+
}
|
|
289
|
+
return info.changes > 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function reviveAgent(agentId, reason = 'manual_revive') {
|
|
293
|
+
const info = db.prepare(`
|
|
294
|
+
UPDATE gov_agents SET status = 'alive', killed_at = NULL, killed_reason = NULL,
|
|
295
|
+
updated_at = datetime('now') WHERE agent_id = ?
|
|
296
|
+
`).run(agentId);
|
|
297
|
+
if (info.changes) appendAudit({ agentId, eventType: 'note', reason, decision: 'allow' });
|
|
298
|
+
return info.changes > 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function getStatus(agentId) {
|
|
302
|
+
const a = getAgent(agentId);
|
|
303
|
+
if (!a) return null;
|
|
304
|
+
const since24h = new Date(Date.now() - 24 * 3600 * 1000).toISOString();
|
|
305
|
+
const stats = db.prepare(`
|
|
306
|
+
SELECT
|
|
307
|
+
SUM(CASE WHEN event_type='execute' AND decision='allow' THEN 1 ELSE 0 END) AS allow24h,
|
|
308
|
+
SUM(CASE WHEN decision='deny' THEN 1 ELSE 0 END) AS deny24h,
|
|
309
|
+
SUM(CASE WHEN event_type='approval_request' THEN 1 ELSE 0 END) AS approvals24h,
|
|
310
|
+
COUNT(*) AS total24h
|
|
311
|
+
FROM gov_audit WHERE agent_id = ? AND ts >= ?
|
|
312
|
+
`).get(agentId, since24h);
|
|
313
|
+
const pending = db.prepare(
|
|
314
|
+
'SELECT COUNT(*) AS n FROM gov_approvals WHERE agent_id = ? AND status = ?'
|
|
315
|
+
).get(agentId, 'pending');
|
|
316
|
+
return {
|
|
317
|
+
agent_id: a.agent_id,
|
|
318
|
+
status: a.status,
|
|
319
|
+
killed_at: a.killed_at,
|
|
320
|
+
killed_reason: a.killed_reason,
|
|
321
|
+
display_name: a.display_name,
|
|
322
|
+
metadata: parseJson(a.metadata),
|
|
323
|
+
stats_24h: {
|
|
324
|
+
allow: stats?.allow24h || 0,
|
|
325
|
+
deny: stats?.deny24h || 0,
|
|
326
|
+
approvals: stats?.approvals24h || 0,
|
|
327
|
+
total: stats?.total24h || 0,
|
|
328
|
+
},
|
|
329
|
+
pending_approvals: pending?.n || 0,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ──────────────────────────────────────────── approvals ────
|
|
334
|
+
function requestApproval({ agentId, resource, action, scope, amount, currency, params, reason, ttlMs }) {
|
|
335
|
+
const requestId = 'req_' + newId(12);
|
|
336
|
+
const expires = new Date(Date.now() + (ttlMs || APPROVAL_TTL_MS)).toISOString();
|
|
337
|
+
db.prepare(`
|
|
338
|
+
INSERT INTO gov_approvals
|
|
339
|
+
(request_id, agent_id, resource, action, scope, amount, currency,
|
|
340
|
+
params_json, reason, expires_at)
|
|
341
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
342
|
+
`).run(
|
|
343
|
+
requestId, agentId, resource, action, scope || null,
|
|
344
|
+
amount == null ? null : Number(amount),
|
|
345
|
+
currency || null,
|
|
346
|
+
safeJson(redact(params || null)),
|
|
347
|
+
reason || null,
|
|
348
|
+
expires,
|
|
349
|
+
);
|
|
350
|
+
appendAudit({
|
|
351
|
+
agentId, eventType: 'approval_request',
|
|
352
|
+
resource, action, scope, amount, currency,
|
|
353
|
+
decision: 'pending', reason,
|
|
354
|
+
paramsJson: safeJson({ requestId, expires }),
|
|
355
|
+
});
|
|
356
|
+
return { requestId, status: 'pending', expiresAt: expires };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function expireOldApprovals() {
|
|
360
|
+
db.prepare(`
|
|
361
|
+
UPDATE gov_approvals SET status = 'expired'
|
|
362
|
+
WHERE status = 'pending' AND expires_at < datetime('now')
|
|
363
|
+
`).run();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function getApproval(requestId) {
|
|
367
|
+
expireOldApprovals();
|
|
368
|
+
return db.prepare('SELECT * FROM gov_approvals WHERE request_id = ?').get(requestId) || null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function listPendingApprovals(agentId, limit = 100) {
|
|
372
|
+
expireOldApprovals();
|
|
373
|
+
return db.prepare(`
|
|
374
|
+
SELECT * FROM gov_approvals
|
|
375
|
+
WHERE agent_id = ? AND status = 'pending'
|
|
376
|
+
ORDER BY created_at DESC LIMIT ?
|
|
377
|
+
`).all(agentId, Math.min(500, limit));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function decideApproval(requestId, { decision, decidedBy, note }) {
|
|
381
|
+
expireOldApprovals();
|
|
382
|
+
const row = getApproval(requestId);
|
|
383
|
+
if (!row) return { ok: false, error: 'not_found' };
|
|
384
|
+
if (row.status !== 'pending') return { ok: false, error: 'not_pending', status: row.status };
|
|
385
|
+
const next = decision === 'approved' ? 'approved' : 'rejected';
|
|
386
|
+
db.prepare(`
|
|
387
|
+
UPDATE gov_approvals
|
|
388
|
+
SET status = ?, decided_by = ?, decided_at = datetime('now'), decided_note = ?
|
|
389
|
+
WHERE request_id = ?
|
|
390
|
+
`).run(next, decidedBy || null, note || null, requestId);
|
|
391
|
+
appendAudit({
|
|
392
|
+
agentId: row.agent_id, eventType: 'approval_decision',
|
|
393
|
+
resource: row.resource, action: row.action, scope: row.scope,
|
|
394
|
+
amount: row.amount, currency: row.currency,
|
|
395
|
+
decision: next, reason: note || null,
|
|
396
|
+
paramsJson: safeJson({ requestId, decidedBy }),
|
|
397
|
+
});
|
|
398
|
+
return { ok: true, status: next };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ─────────────────────────────────── core authorisation ────
|
|
402
|
+
/**
|
|
403
|
+
* The single decision point an agent calls before executing any action.
|
|
404
|
+
* Returns: { decision: 'allow'|'deny'|'approval_required',
|
|
405
|
+
* reason, policy, requestId? }
|
|
406
|
+
*/
|
|
407
|
+
function check({ agentId, resource, action, scope, amount, currency }) {
|
|
408
|
+
// 1) Agent must exist and be alive.
|
|
409
|
+
const a = getAgent(agentId);
|
|
410
|
+
if (!a) return _decide('deny', 'unknown_agent');
|
|
411
|
+
if (a.status !== 'alive') return _decide('deny', 'agent_' + a.status);
|
|
412
|
+
|
|
413
|
+
// 2) Match a policy.
|
|
414
|
+
const pol = matchPolicy(agentId, { resource, action, scope });
|
|
415
|
+
if (!pol) return _decide('deny', 'no_policy', null);
|
|
416
|
+
if (pol.effect === 'deny') return _decide('deny', 'policy_deny', pol);
|
|
417
|
+
|
|
418
|
+
// 3) Per-call rate limit.
|
|
419
|
+
if (pol.per_call_rate) {
|
|
420
|
+
const c = bumpRate(agentId, resource);
|
|
421
|
+
if (c > pol.per_call_rate) return _decide('deny', 'rate_limit', pol);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// 4) Monetary caps.
|
|
425
|
+
if (amount != null && amount > 0) {
|
|
426
|
+
if (pol.max_amount != null && amount > pol.max_amount) {
|
|
427
|
+
return _decide('deny', 'over_max_amount', pol);
|
|
428
|
+
}
|
|
429
|
+
if (pol.daily_cap != null) {
|
|
430
|
+
const used = rollingSpend(agentId, resource);
|
|
431
|
+
if (used + amount > pol.daily_cap) {
|
|
432
|
+
return _decide('deny', 'over_daily_cap', pol, { used, cap: pol.daily_cap });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 5) Human approval required?
|
|
438
|
+
if (pol.requires_approval) {
|
|
439
|
+
return { decision: 'approval_required', reason: 'approval_gate', policy: pol };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return _decide('allow', 'policy_match', pol);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function _decide(decision, reason, policy = null, extra = null) {
|
|
446
|
+
return { decision, reason, policy, ...(extra ? { extra } : {}) };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ─────────────────────────────────── public API surface ────
|
|
450
|
+
module.exports = {
|
|
451
|
+
// agent lifecycle
|
|
452
|
+
registerAgent, getAgent, authAgent, isAlive,
|
|
453
|
+
killAgent, reviveAgent, getStatus,
|
|
454
|
+
// policies
|
|
455
|
+
definePolicy, listPolicies, deletePolicy, matchPolicy,
|
|
456
|
+
// approvals
|
|
457
|
+
requestApproval, getApproval, listPendingApprovals, decideApproval,
|
|
458
|
+
// audit
|
|
459
|
+
appendAudit, getAudit, verifyAuditChain,
|
|
460
|
+
// spend / rate
|
|
461
|
+
recordSpend, rollingSpend, bumpRate,
|
|
462
|
+
// core
|
|
463
|
+
check,
|
|
464
|
+
// helpers (exposed for tests)
|
|
465
|
+
_internals: { redact, hmac, sha256 },
|
|
466
|
+
};
|