web-agent-bridge 3.3.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 +12 -0
- package/README.ar.md +18 -0
- package/README.md +198 -1664
- package/bin/wab-init.js +223 -0
- package/examples/azure-dns-wab.js +83 -0
- package/examples/cloudflare-wab-dns.js +121 -0
- package/examples/cpanel-wab-dns.js +114 -0
- 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/plesk-wab-dns.js +103 -0
- package/examples/route53-wab-dns.js +144 -0
- package/examples/safe-mode-agent.js +96 -0
- package/examples/wab-sign.js +74 -0
- package/examples/wab-verify.js +60 -0
- package/package.json +5 -5
- package/public/.well-known/wab.json +28 -0
- package/public/activate.html +368 -0
- package/public/adoption-metrics.html +188 -0
- package/public/api.html +1 -1
- package/public/azure-dns-integration.html +289 -0
- package/public/cloudflare-integration.html +380 -0
- package/public/cpanel-integration.html +398 -0
- package/public/css/styles.css +28 -0
- package/public/dashboard.html +1 -0
- package/public/dns.html +101 -172
- package/public/docs.html +1 -0
- package/public/gcp-dns-integration.html +318 -0
- package/public/growth.html +4 -2
- package/public/index.html +227 -31
- package/public/integrations.html +1 -1
- package/public/js/activate.js +145 -0
- package/public/js/auth-nav.js +34 -0
- package/public/js/dns.js +438 -0
- package/public/openapi.json +89 -0
- package/public/plesk-integration.html +375 -0
- package/public/premium.html +1 -1
- package/public/provider-onboarding.html +172 -0
- package/public/provider-sandbox.html +134 -0
- package/public/providers.html +359 -0
- package/public/registrar-integrations.html +141 -0
- package/public/robots.txt +12 -0
- package/public/route53-integration.html +531 -0
- package/public/shieldqr.html +231 -0
- package/public/sitemap.xml +6 -0
- package/public/wab-trust.html +200 -0
- package/public/wab-vs-protocols.html +210 -0
- package/public/whitepaper.html +449 -0
- package/sdk/auto-discovery.js +288 -0
- package/sdk/governance.js +262 -0
- package/sdk/index.js +13 -0
- package/sdk/package.json +2 -2
- package/sdk/safe-mode.js +221 -0
- package/server/index.js +144 -5
- 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/mysql.js +1 -1
- package/server/models/adapters/postgresql.js +1 -1
- package/server/models/db.js +60 -1
- package/server/routes/admin-plans.js +76 -0
- package/server/routes/admin-premium.js +4 -2
- package/server/routes/admin-shieldqr.js +90 -0
- package/server/routes/admin-trust-monitor.js +83 -0
- package/server/routes/admin.js +289 -1
- package/server/routes/billing.js +16 -4
- package/server/routes/discovery.js +1933 -2
- package/server/routes/governance.js +208 -0
- package/server/routes/plans.js +33 -0
- package/server/routes/providers.js +650 -0
- package/server/routes/shieldqr.js +88 -0
- package/server/services/email.js +29 -0
- package/server/services/governance.js +466 -0
- package/server/services/plans.js +214 -0
- package/server/services/premium.js +1 -1
- package/server/services/provider-clients.js +740 -0
- package/server/services/shieldqr.js +322 -0
- package/server/services/ssl-inspector.js +42 -0
- package/server/services/ssl-monitor.js +167 -0
- package/server/services/stripe.js +18 -5
- package/server/services/vision.js +1 -1
- package/server/services/wab-crypto.js +178 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAB SDK — Auto-Discovery Fallback
|
|
3
|
+
*
|
|
4
|
+
* For sites that haven't installed WAB yet (no /.well-known/wab.json,
|
|
5
|
+
* no _wab DNS TXT), this module produces a normalized capabilities envelope
|
|
6
|
+
* by parsing publicly available metadata:
|
|
7
|
+
* 1. /.well-known/wab.json (canonical)
|
|
8
|
+
* 2. <script type="application/ld+json"> (JSON-LD / Schema.org)
|
|
9
|
+
* 3. <meta property="og:*"> (OpenGraph)
|
|
10
|
+
* 4. <meta name="description"> / <title>
|
|
11
|
+
* 5. /sitemap.xml (URL inventory)
|
|
12
|
+
* 6. /robots.txt (allow/disallow + Sitemap directives)
|
|
13
|
+
*
|
|
14
|
+
* The resulting envelope shape mirrors a minimal wab.json so downstream
|
|
15
|
+
* code can treat unsigned sites uniformly:
|
|
16
|
+
*
|
|
17
|
+
* {
|
|
18
|
+
* ok: boolean,
|
|
19
|
+
* source: 'wab.json' | 'auto-discovery',
|
|
20
|
+
* site: { name, description, url },
|
|
21
|
+
* trust: { signed: false, ssl: { ... } },
|
|
22
|
+
* actions: [ { name, description, source } ],
|
|
23
|
+
* products: [ { name, sku, offers } ],
|
|
24
|
+
* sitemap: [ url, ... ],
|
|
25
|
+
* robots: { allow: [], disallow: [], sitemaps: [] }
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* Pure JS, no external deps. Works in Node (with global fetch).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const { extractJsonLdBlocks, extractProductsFromHtml, suggestWabActionsFromProducts } =
|
|
32
|
+
require('./schema-discovery');
|
|
33
|
+
|
|
34
|
+
/* ------------------------------------------------------------------ */
|
|
35
|
+
/* Helpers */
|
|
36
|
+
/* ------------------------------------------------------------------ */
|
|
37
|
+
|
|
38
|
+
function _abs(base, path) {
|
|
39
|
+
try {
|
|
40
|
+
return new URL(path, base).toString();
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function _fetchText(url, { timeoutMs = 8000 } = {}) {
|
|
47
|
+
if (typeof fetch !== 'function') {
|
|
48
|
+
throw new Error('global fetch() is required (Node 18+) for auto-discovery');
|
|
49
|
+
}
|
|
50
|
+
const ctrl = new AbortController();
|
|
51
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
52
|
+
try {
|
|
53
|
+
const r = await fetch(url, {
|
|
54
|
+
redirect: 'follow',
|
|
55
|
+
signal: ctrl.signal,
|
|
56
|
+
headers: { 'user-agent': 'wab-auto-discovery/1.0 (+https://webagentbridge.com)' }
|
|
57
|
+
});
|
|
58
|
+
if (!r.ok) return { ok: false, status: r.status, text: '' };
|
|
59
|
+
return { ok: true, status: r.status, text: await r.text() };
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return { ok: false, status: 0, text: '', error: e.message };
|
|
62
|
+
} finally {
|
|
63
|
+
clearTimeout(t);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ------------------------------------------------------------------ */
|
|
68
|
+
/* HTML metadata extractors */
|
|
69
|
+
/* ------------------------------------------------------------------ */
|
|
70
|
+
|
|
71
|
+
function extractMetaTags(html) {
|
|
72
|
+
const out = { og: {}, twitter: {}, description: null, title: null };
|
|
73
|
+
if (!html) return out;
|
|
74
|
+
|
|
75
|
+
const titleM = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
76
|
+
if (titleM) out.title = titleM[1].trim();
|
|
77
|
+
|
|
78
|
+
const metaRe = /<meta\b[^>]*>/gi;
|
|
79
|
+
let m;
|
|
80
|
+
while ((m = metaRe.exec(html)) !== null) {
|
|
81
|
+
const tag = m[0];
|
|
82
|
+
const nameM = tag.match(/\bname\s*=\s*["']([^"']+)["']/i);
|
|
83
|
+
const propM = tag.match(/\bproperty\s*=\s*["']([^"']+)["']/i);
|
|
84
|
+
const contentM = tag.match(/\bcontent\s*=\s*["']([^"']*)["']/i);
|
|
85
|
+
if (!contentM) continue;
|
|
86
|
+
const content = contentM[1];
|
|
87
|
+
const key = (propM && propM[1]) || (nameM && nameM[1]) || '';
|
|
88
|
+
if (!key) continue;
|
|
89
|
+
const lk = key.toLowerCase();
|
|
90
|
+
if (lk === 'description') out.description = content;
|
|
91
|
+
else if (lk.startsWith('og:')) out.og[lk.slice(3)] = content;
|
|
92
|
+
else if (lk.startsWith('twitter:')) out.twitter[lk.slice(8)] = content;
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* ------------------------------------------------------------------ */
|
|
98
|
+
/* Sitemap / robots */
|
|
99
|
+
/* ------------------------------------------------------------------ */
|
|
100
|
+
|
|
101
|
+
function parseSitemap(xml, { limit = 200 } = {}) {
|
|
102
|
+
if (!xml) return [];
|
|
103
|
+
const urls = [];
|
|
104
|
+
const re = /<loc>\s*([^<\s]+)\s*<\/loc>/gi;
|
|
105
|
+
let m;
|
|
106
|
+
while ((m = re.exec(xml)) !== null && urls.length < limit) {
|
|
107
|
+
urls.push(m[1]);
|
|
108
|
+
}
|
|
109
|
+
return urls;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseRobots(text) {
|
|
113
|
+
const out = { allow: [], disallow: [], sitemaps: [], userAgents: [] };
|
|
114
|
+
if (!text) return out;
|
|
115
|
+
let currentUA = '*';
|
|
116
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
117
|
+
const line = raw.replace(/#.*$/, '').trim();
|
|
118
|
+
if (!line) continue;
|
|
119
|
+
const idx = line.indexOf(':');
|
|
120
|
+
if (idx < 0) continue;
|
|
121
|
+
const key = line.slice(0, idx).trim().toLowerCase();
|
|
122
|
+
const val = line.slice(idx + 1).trim();
|
|
123
|
+
if (key === 'user-agent') {
|
|
124
|
+
currentUA = val;
|
|
125
|
+
if (!out.userAgents.includes(val)) out.userAgents.push(val);
|
|
126
|
+
} else if (key === 'allow') out.allow.push({ ua: currentUA, path: val });
|
|
127
|
+
else if (key === 'disallow') out.disallow.push({ ua: currentUA, path: val });
|
|
128
|
+
else if (key === 'sitemap') out.sitemaps.push(val);
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* ------------------------------------------------------------------ */
|
|
134
|
+
/* JSON-LD extras: WebSite + Organization + SearchAction */
|
|
135
|
+
/* ------------------------------------------------------------------ */
|
|
136
|
+
|
|
137
|
+
function extractSiteIdentity(html) {
|
|
138
|
+
const blocks = extractJsonLdBlocks(html);
|
|
139
|
+
const out = { name: null, description: null, url: null, search: null, organization: null };
|
|
140
|
+
for (const text of blocks) {
|
|
141
|
+
let data;
|
|
142
|
+
try { data = JSON.parse(text); } catch { continue; }
|
|
143
|
+
const items = Array.isArray(data) ? data : Array.isArray(data['@graph']) ? data['@graph'] : [data];
|
|
144
|
+
for (const node of items) {
|
|
145
|
+
if (!node || typeof node !== 'object') continue;
|
|
146
|
+
let types = node['@type'];
|
|
147
|
+
if (typeof types === 'string') types = [types];
|
|
148
|
+
if (!Array.isArray(types)) types = [];
|
|
149
|
+
if (types.includes('WebSite')) {
|
|
150
|
+
out.name = out.name || node.name;
|
|
151
|
+
out.url = out.url || node.url;
|
|
152
|
+
out.description = out.description || node.description;
|
|
153
|
+
const action = node.potentialAction;
|
|
154
|
+
if (action && (Array.isArray(action) ? action[0] : action)) {
|
|
155
|
+
const a = Array.isArray(action) ? action[0] : action;
|
|
156
|
+
if (a && (a['@type'] === 'SearchAction' || /SearchAction/.test(String(a['@type'])))) {
|
|
157
|
+
out.search = {
|
|
158
|
+
target: typeof a.target === 'string' ? a.target : (a.target && a.target.urlTemplate),
|
|
159
|
+
queryParam: a['query-input'] || a.queryInput || null
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} else if (types.includes('Organization')) {
|
|
164
|
+
out.organization = out.organization || { name: node.name, url: node.url, logo: node.logo };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* ------------------------------------------------------------------ */
|
|
172
|
+
/* Main entry */
|
|
173
|
+
/* ------------------------------------------------------------------ */
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Discover capabilities for a site. Tries /.well-known/wab.json first;
|
|
177
|
+
* falls back to HTML/sitemap/robots scraping.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} siteUrl e.g. "https://example.com"
|
|
180
|
+
* @param {object} [opts]
|
|
181
|
+
* @param {number} [opts.timeoutMs=8000]
|
|
182
|
+
* @param {number} [opts.sitemapLimit=200]
|
|
183
|
+
* @param {boolean} [opts.skipWabJson=false]
|
|
184
|
+
* @returns {Promise<object>} normalized envelope
|
|
185
|
+
*/
|
|
186
|
+
async function discover(siteUrl, opts = {}) {
|
|
187
|
+
const timeoutMs = opts.timeoutMs || 8000;
|
|
188
|
+
const sitemapLimit = opts.sitemapLimit || 200;
|
|
189
|
+
|
|
190
|
+
let baseUrl;
|
|
191
|
+
try { baseUrl = new URL(siteUrl).origin; }
|
|
192
|
+
catch { return { ok: false, error: 'invalid_url', source: 'auto-discovery' }; }
|
|
193
|
+
|
|
194
|
+
// 1) Canonical wab.json — if present, use as authoritative source.
|
|
195
|
+
if (!opts.skipWabJson) {
|
|
196
|
+
const wabUrl = _abs(baseUrl, '/.well-known/wab.json');
|
|
197
|
+
const wabRes = await _fetchText(wabUrl, { timeoutMs });
|
|
198
|
+
if (wabRes.ok && wabRes.text) {
|
|
199
|
+
try {
|
|
200
|
+
const parsed = JSON.parse(wabRes.text);
|
|
201
|
+
return {
|
|
202
|
+
ok: true,
|
|
203
|
+
source: 'wab.json',
|
|
204
|
+
site: { name: parsed.site || parsed.name, description: parsed.description, url: baseUrl },
|
|
205
|
+
trust: { signed: !!parsed.sig, ...(parsed.trust || {}) },
|
|
206
|
+
actions: Array.isArray(parsed.actions) ? parsed.actions : [],
|
|
207
|
+
products: [],
|
|
208
|
+
sitemap: [],
|
|
209
|
+
robots: null,
|
|
210
|
+
raw: parsed
|
|
211
|
+
};
|
|
212
|
+
} catch { /* fall through to auto-discovery */ }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 2) Fetch homepage HTML in parallel with sitemap + robots.
|
|
217
|
+
const [homeRes, sitemapRes, robotsRes] = await Promise.all([
|
|
218
|
+
_fetchText(baseUrl, { timeoutMs }),
|
|
219
|
+
_fetchText(_abs(baseUrl, '/sitemap.xml'), { timeoutMs }),
|
|
220
|
+
_fetchText(_abs(baseUrl, '/robots.txt'), { timeoutMs })
|
|
221
|
+
]);
|
|
222
|
+
|
|
223
|
+
const html = homeRes.text || '';
|
|
224
|
+
const meta = extractMetaTags(html);
|
|
225
|
+
const ident = extractSiteIdentity(html);
|
|
226
|
+
const products = extractProductsFromHtml(html);
|
|
227
|
+
const robots = parseRobots(robotsRes.text || '');
|
|
228
|
+
let sitemap = parseSitemap(sitemapRes.text || '', { limit: sitemapLimit });
|
|
229
|
+
|
|
230
|
+
// Discover additional sitemaps from robots.
|
|
231
|
+
if (sitemap.length === 0 && robots.sitemaps.length) {
|
|
232
|
+
for (const sm of robots.sitemaps.slice(0, 3)) {
|
|
233
|
+
const r = await _fetchText(sm, { timeoutMs });
|
|
234
|
+
if (r.ok) sitemap = sitemap.concat(parseSitemap(r.text, { limit: sitemapLimit }));
|
|
235
|
+
if (sitemap.length >= sitemapLimit) break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Build action hints.
|
|
240
|
+
const actions = suggestWabActionsFromProducts(products);
|
|
241
|
+
if (ident.search && ident.search.target) {
|
|
242
|
+
actions.push({
|
|
243
|
+
name: 'searchSite',
|
|
244
|
+
description: 'Schema.org SearchAction: ' + ident.search.target,
|
|
245
|
+
source: 'schema.org/SearchAction',
|
|
246
|
+
template: ident.search.target
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (sitemap.length) {
|
|
250
|
+
actions.push({
|
|
251
|
+
name: 'browseSitemap',
|
|
252
|
+
description: `${sitemap.length} URLs discovered from sitemap.xml`,
|
|
253
|
+
source: 'sitemap.xml'
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (meta.og && meta.og.url) {
|
|
257
|
+
actions.push({
|
|
258
|
+
name: 'getOpenGraph',
|
|
259
|
+
description: 'OpenGraph metadata available',
|
|
260
|
+
source: 'opengraph'
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
ok: true,
|
|
266
|
+
source: 'auto-discovery',
|
|
267
|
+
site: {
|
|
268
|
+
name: ident.name || meta.og.site_name || meta.title || baseUrl,
|
|
269
|
+
description: ident.description || meta.description || meta.og.description || null,
|
|
270
|
+
url: ident.url || meta.og.url || baseUrl
|
|
271
|
+
},
|
|
272
|
+
trust: { signed: false, auto: true },
|
|
273
|
+
actions,
|
|
274
|
+
products,
|
|
275
|
+
sitemap,
|
|
276
|
+
robots,
|
|
277
|
+
meta: { og: meta.og, twitter: meta.twitter, title: meta.title, description: meta.description },
|
|
278
|
+
organization: ident.organization || null
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = {
|
|
283
|
+
discover,
|
|
284
|
+
extractMetaTags,
|
|
285
|
+
parseSitemap,
|
|
286
|
+
parseRobots,
|
|
287
|
+
extractSiteIdentity
|
|
288
|
+
};
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAB Agent Governance — Client SDK
|
|
3
|
+
* ─────────────────────────────────
|
|
4
|
+
* The Layer-3 piece that sits ABOVE the WAB Protocol.
|
|
5
|
+
*
|
|
6
|
+
* ┌─────────────────────────────────┐
|
|
7
|
+
* │ WABGovernance (this module) │ ← permissions / approval / audit
|
|
8
|
+
* ├─────────────────────────────────┤
|
|
9
|
+
* │ WAB Protocol (AICommands) │
|
|
10
|
+
* ├─────────────────────────────────┤
|
|
11
|
+
* │ Dynamic Shield (price / OCR) │
|
|
12
|
+
* └─────────────────────────────────┘
|
|
13
|
+
*
|
|
14
|
+
* Five governance primitives every agent should call:
|
|
15
|
+
*
|
|
16
|
+
* 1) Permission Boundary await gov.check({resource,action,scope,amount})
|
|
17
|
+
* 2) Human Approval Gate await gov.requestApproval({...}) + waitForDecision()
|
|
18
|
+
* 3) Audit Log await gov.audit({eventType,...})
|
|
19
|
+
* 4) Kill Switch await gov.isAlive() / await gov.kill('reason')
|
|
20
|
+
* 5) Spend / Rate Limits enforced server-side via policies
|
|
21
|
+
*
|
|
22
|
+
* Convenience: gov.guard(actionDesc, fn) wraps an action with all 5 at once.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* const { WABGovernance } = require('web-agent-bridge/sdk');
|
|
26
|
+
* const gov = new WABGovernance({
|
|
27
|
+
* apiBase: 'https://webagentbridge.com',
|
|
28
|
+
* agentId: process.env.AGENT_ID,
|
|
29
|
+
* agentToken: process.env.AGENT_TOKEN,
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* await gov.guard(
|
|
33
|
+
* { resource: 'stripe', action: 'write', scope: 'refunds', amount: 49.99, currency: 'USD' },
|
|
34
|
+
* async () => stripe.refunds.create({ ... }),
|
|
35
|
+
* );
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
'use strict';
|
|
39
|
+
|
|
40
|
+
const DEFAULT_API = 'https://webagentbridge.com';
|
|
41
|
+
|
|
42
|
+
class WABGovernanceError extends Error {
|
|
43
|
+
constructor(message, decision) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = 'WABGovernanceError';
|
|
46
|
+
this.decision = decision || null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class WABGovernance {
|
|
51
|
+
/**
|
|
52
|
+
* @param {object} opts
|
|
53
|
+
* @param {string} opts.apiBase
|
|
54
|
+
* @param {string} opts.agentId
|
|
55
|
+
* @param {string} opts.agentToken
|
|
56
|
+
* @param {function} [opts.onApprovalRequired] — async (request) => 'approved'|'rejected'
|
|
57
|
+
* If provided, guard() will block until human decides; otherwise it
|
|
58
|
+
* polls the approval endpoint up to opts.approvalTimeoutMs.
|
|
59
|
+
* @param {number} [opts.approvalTimeoutMs=300000] — 5 min default
|
|
60
|
+
* @param {number} [opts.approvalPollMs=2000]
|
|
61
|
+
* @param {number} [opts.timeoutMs=10000]
|
|
62
|
+
* @param {function} [opts.fetch]
|
|
63
|
+
*/
|
|
64
|
+
constructor(opts = {}) {
|
|
65
|
+
this.apiBase = (opts.apiBase || DEFAULT_API).replace(/\/+$/, '');
|
|
66
|
+
this.agentId = opts.agentId;
|
|
67
|
+
this.agentToken = opts.agentToken;
|
|
68
|
+
this.onApprovalRequired = typeof opts.onApprovalRequired === 'function' ? opts.onApprovalRequired : null;
|
|
69
|
+
this.approvalTimeoutMs = Number.isFinite(opts.approvalTimeoutMs) ? opts.approvalTimeoutMs : 300_000;
|
|
70
|
+
this.approvalPollMs = Number.isFinite(opts.approvalPollMs) ? opts.approvalPollMs : 2_000;
|
|
71
|
+
this.timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : 10_000;
|
|
72
|
+
this._fetch = opts.fetch || (typeof fetch !== 'undefined' ? fetch : null);
|
|
73
|
+
if (!this._fetch) {
|
|
74
|
+
try { this._fetch = require('node-fetch'); } catch { /* user must supply */ }
|
|
75
|
+
}
|
|
76
|
+
if (!this.agentId) throw new Error('WABGovernance: agentId required');
|
|
77
|
+
if (!this.agentToken) throw new Error('WABGovernance: agentToken required');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ────────────────────────────────────── HTTP plumbing ────
|
|
81
|
+
async _req(method, path, body) {
|
|
82
|
+
if (!this._fetch) throw new Error('WABGovernance: fetch not available');
|
|
83
|
+
const ctl = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
|
84
|
+
const timer = ctl ? setTimeout(() => ctl.abort(), this.timeoutMs) : null;
|
|
85
|
+
try {
|
|
86
|
+
const r = await this._fetch(this.apiBase + path, {
|
|
87
|
+
method,
|
|
88
|
+
headers: {
|
|
89
|
+
'content-type': 'application/json',
|
|
90
|
+
'authorization': 'Bearer ' + this.agentToken,
|
|
91
|
+
},
|
|
92
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
93
|
+
signal: ctl ? ctl.signal : undefined,
|
|
94
|
+
});
|
|
95
|
+
const text = await r.text();
|
|
96
|
+
const data = text ? safeParse(text) : null;
|
|
97
|
+
if (!r.ok) {
|
|
98
|
+
const e = new Error(`gov_http_${r.status}: ${data?.error || r.statusText}`);
|
|
99
|
+
e.status = r.status; e.body = data;
|
|
100
|
+
throw e;
|
|
101
|
+
}
|
|
102
|
+
return data;
|
|
103
|
+
} finally { if (timer) clearTimeout(timer); }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ────────────────────────────────────────── lifecycle ────
|
|
107
|
+
/** Static helper to register a brand-new agent and capture its token. */
|
|
108
|
+
static async register({ apiBase = DEFAULT_API, displayName, ownerId, metadata, fetch: f } = {}) {
|
|
109
|
+
const fn = f || (typeof fetch !== 'undefined' ? fetch : require('node-fetch'));
|
|
110
|
+
const r = await fn(apiBase.replace(/\/+$/, '') + '/api/governance/agents', {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: { 'content-type': 'application/json' },
|
|
113
|
+
body: JSON.stringify({ display_name: displayName, owner_id: ownerId, metadata }),
|
|
114
|
+
});
|
|
115
|
+
if (!r.ok) throw new Error('register_failed_' + r.status);
|
|
116
|
+
return r.json(); // { agent_id, agent_token, message }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
status() { return this._req('GET', `/api/governance/agents/${this.agentId}/status`); }
|
|
120
|
+
async isAlive() {
|
|
121
|
+
const s = await this.status().catch(() => null);
|
|
122
|
+
return !!s && s.status === 'alive';
|
|
123
|
+
}
|
|
124
|
+
kill(reason) { return this._req('POST', `/api/governance/agents/${this.agentId}/kill`, { reason }); }
|
|
125
|
+
revive(reason) { return this._req('POST', `/api/governance/agents/${this.agentId}/revive`, { reason }); }
|
|
126
|
+
|
|
127
|
+
// ─────────────────────────────────────── policies ────
|
|
128
|
+
policies() { return this._req('GET', `/api/governance/agents/${this.agentId}/policies`); }
|
|
129
|
+
definePolicy(p) { return this._req('POST', `/api/governance/agents/${this.agentId}/policies`, p); }
|
|
130
|
+
removePolicy(id) { return this._req('DELETE', `/api/governance/agents/${this.agentId}/policies/${id}`); }
|
|
131
|
+
|
|
132
|
+
// ────────────────────────────────────── decision ────
|
|
133
|
+
/** Pre-action permission check. */
|
|
134
|
+
check({ resource, action, scope, amount, currency }) {
|
|
135
|
+
return this._req('POST', `/api/governance/agents/${this.agentId}/check`,
|
|
136
|
+
{ resource, action, scope, amount, currency });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Report executed action — feeds audit + spend tracker. */
|
|
140
|
+
reportExecute(payload) {
|
|
141
|
+
return this._req('POST', `/api/governance/agents/${this.agentId}/execute`, payload);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ────────────────────────────────────── audit ────
|
|
145
|
+
audit({ eventType = 'note', resource, action, scope, decision, reason, params, result } = {}) {
|
|
146
|
+
// Convenience wrapper around reportExecute() for non-execute events.
|
|
147
|
+
return this._req('POST', `/api/governance/agents/${this.agentId}/execute`, {
|
|
148
|
+
resource: resource || 'note', action: action || eventType, scope,
|
|
149
|
+
decision: decision || 'allow', reason, params, result,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
getAudit(opts = {}) {
|
|
153
|
+
const q = new URLSearchParams();
|
|
154
|
+
if (opts.limit) q.set('limit', String(opts.limit));
|
|
155
|
+
if (opts.since) q.set('since', opts.since);
|
|
156
|
+
if (opts.event) q.set('event', opts.event);
|
|
157
|
+
const qs = q.toString();
|
|
158
|
+
return this._req('GET', `/api/governance/agents/${this.agentId}/audit${qs ? '?' + qs : ''}`);
|
|
159
|
+
}
|
|
160
|
+
verifyAudit() {
|
|
161
|
+
return this._req('GET', `/api/governance/agents/${this.agentId}/audit/verify`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─────────────────────────────────── approvals ────
|
|
165
|
+
requestApproval(payload) {
|
|
166
|
+
return this._req('POST', `/api/governance/agents/${this.agentId}/approvals`, payload);
|
|
167
|
+
}
|
|
168
|
+
pendingApprovals() {
|
|
169
|
+
return this._req('GET', `/api/governance/agents/${this.agentId}/approvals/pending`);
|
|
170
|
+
}
|
|
171
|
+
getApproval(requestId) {
|
|
172
|
+
return this._req('GET', `/api/governance/approvals/${requestId}`);
|
|
173
|
+
}
|
|
174
|
+
decideApproval(requestId, decision, note) {
|
|
175
|
+
return this._req('POST', `/api/governance/approvals/${requestId}/decide`,
|
|
176
|
+
{ decision, note });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Block until an approval is decided or times out. */
|
|
180
|
+
async waitForDecision(requestId, { timeoutMs, pollMs } = {}) {
|
|
181
|
+
const tEnd = Date.now() + (timeoutMs || this.approvalTimeoutMs);
|
|
182
|
+
const step = pollMs || this.approvalPollMs;
|
|
183
|
+
while (Date.now() < tEnd) {
|
|
184
|
+
const r = await this.getApproval(requestId).catch(() => null);
|
|
185
|
+
if (r && r.status !== 'pending') return r;
|
|
186
|
+
await sleep(step);
|
|
187
|
+
}
|
|
188
|
+
return { request_id: requestId, status: 'timeout' };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ────────────────────────────────────── guard (the big one) ────
|
|
192
|
+
/**
|
|
193
|
+
* Wrap an action. Runs the full governance pipeline:
|
|
194
|
+
* 1) check permission
|
|
195
|
+
* 2) if approval_required → request + wait for human
|
|
196
|
+
* 3) execute the function
|
|
197
|
+
* 4) record execute (audit + spend)
|
|
198
|
+
* 5) on throw → audit deny + rethrow
|
|
199
|
+
*/
|
|
200
|
+
async guard(actionDesc, fn) {
|
|
201
|
+
const { resource, action, scope, amount, currency } = actionDesc || {};
|
|
202
|
+
if (!resource || !action) throw new WABGovernanceError('guard: resource and action required');
|
|
203
|
+
|
|
204
|
+
// 1) Pre-check
|
|
205
|
+
const v = await this.check({ resource, action, scope, amount, currency });
|
|
206
|
+
|
|
207
|
+
// 2) Approval gate
|
|
208
|
+
if (v.decision === 'approval_required') {
|
|
209
|
+
const req = await this.requestApproval({
|
|
210
|
+
resource, action, scope, amount, currency,
|
|
211
|
+
params: actionDesc.params || null,
|
|
212
|
+
reason: actionDesc.reason || 'policy_required_approval',
|
|
213
|
+
ttl_ms: actionDesc.ttlMs || null,
|
|
214
|
+
});
|
|
215
|
+
let outcome;
|
|
216
|
+
if (this.onApprovalRequired) {
|
|
217
|
+
outcome = await this.onApprovalRequired({ request_id: req.requestId, ...actionDesc });
|
|
218
|
+
if (outcome === 'approved' || outcome === 'rejected') {
|
|
219
|
+
await this.decideApproval(req.requestId, outcome, 'callback_decision');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const decided = await this.waitForDecision(req.requestId);
|
|
223
|
+
if (decided.status !== 'approved') {
|
|
224
|
+
throw new WABGovernanceError(
|
|
225
|
+
`Approval ${decided.status} for ${resource}/${action}`,
|
|
226
|
+
{ ...v, approval: decided },
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
} else if (v.decision !== 'allow') {
|
|
230
|
+
throw new WABGovernanceError(
|
|
231
|
+
`Governance denied ${resource}/${action}: ${v.reason}`, v,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 3) Execute
|
|
236
|
+
let result, error;
|
|
237
|
+
const t0 = Date.now();
|
|
238
|
+
try { result = await fn(v); }
|
|
239
|
+
catch (e) { error = e; }
|
|
240
|
+
const elapsed = Date.now() - t0;
|
|
241
|
+
|
|
242
|
+
// 4) Audit (always, even on error)
|
|
243
|
+
try {
|
|
244
|
+
await this.reportExecute({
|
|
245
|
+
resource, action, scope, amount, currency,
|
|
246
|
+
decision: error ? 'deny' : 'allow',
|
|
247
|
+
reason: error ? ('exec_error: ' + (error.message || 'unknown')) : 'guard_executed',
|
|
248
|
+
params: actionDesc.params || null,
|
|
249
|
+
result: error ? null : (typeof result === 'object' ? result : { value: result }),
|
|
250
|
+
});
|
|
251
|
+
} catch { /* audit best-effort; don't mask the original outcome */ }
|
|
252
|
+
|
|
253
|
+
if (error) throw error;
|
|
254
|
+
return { result, elapsed_ms: elapsed };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ─────────────────────────────────────────── helpers ────
|
|
259
|
+
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
|
260
|
+
function safeParse(s) { try { return JSON.parse(s); } catch { return null; } }
|
|
261
|
+
|
|
262
|
+
module.exports = { WABGovernance, WABGovernanceError };
|
package/sdk/index.js
CHANGED
|
@@ -624,6 +624,12 @@ try { WABToolkit = require('../packages/langchain').WABToolkit; } catch {
|
|
|
624
624
|
|
|
625
625
|
// SPEC §8.10–§8.13 client helper
|
|
626
626
|
const { SafetyShieldClient } = require('./safety-shield');
|
|
627
|
+
// Phase 19 — Safe Mode trust gate
|
|
628
|
+
const { WABSafeMode, WABSafeModeError, POLICIES: WAB_SAFE_POLICIES } = require('./safe-mode');
|
|
629
|
+
// Phase 20 — Agent Governance Layer (permissions + approval + audit + kill-switch)
|
|
630
|
+
const { WABGovernance, WABGovernanceError } = require('./governance');
|
|
631
|
+
// Zero-Config Adoption — Auto-Discovery fallback for sites without /.well-known/wab.json
|
|
632
|
+
const autoDiscovery = require('./auto-discovery');
|
|
627
633
|
|
|
628
634
|
module.exports = {
|
|
629
635
|
WABAgent,
|
|
@@ -633,4 +639,11 @@ module.exports = {
|
|
|
633
639
|
WABAgentOS,
|
|
634
640
|
WABToolkit,
|
|
635
641
|
SafetyShieldClient,
|
|
642
|
+
WABSafeMode,
|
|
643
|
+
WABSafeModeError,
|
|
644
|
+
WAB_SAFE_POLICIES,
|
|
645
|
+
WABGovernance,
|
|
646
|
+
WABGovernanceError,
|
|
647
|
+
autoDiscovery,
|
|
648
|
+
discover: autoDiscovery.discover,
|
|
636
649
|
};
|
package/sdk/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "web-agent-bridge-sdk",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "SDK for building AI agents that interact with Web Agent Bridge (WAB)",
|
|
3
|
+
"version": "3.2.0",
|
|
4
|
+
"description": "SDK for building AI agents that interact with Web Agent Bridge (WAB) — includes auto-discovery fallback, ShieldQR verifier, and governance layer.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"license": "MIT",
|