web-agent-bridge 2.3.1 → 2.5.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.
Files changed (53) hide show
  1. package/README.ar.md +524 -31
  2. package/README.md +592 -47
  3. package/bin/agent-runner.js +10 -1
  4. package/package.json +1 -1
  5. package/public/agent-workspace.html +347 -0
  6. package/public/browser.html +484 -0
  7. package/public/css/agent-workspace.css +1713 -0
  8. package/public/index.html +94 -0
  9. package/public/js/agent-workspace.js +1740 -0
  10. package/sdk/index.d.ts +253 -0
  11. package/sdk/index.js +360 -1
  12. package/sdk/package.json +1 -1
  13. package/server/config/secrets.js +13 -5
  14. package/server/control-plane/index.js +301 -0
  15. package/server/data-plane/index.js +354 -0
  16. package/server/index.js +185 -4
  17. package/server/llm/index.js +404 -0
  18. package/server/middleware/adminAuth.js +6 -1
  19. package/server/middleware/auth.js +11 -2
  20. package/server/middleware/rateLimits.js +78 -2
  21. package/server/migrations/003_ads_integer_cents.sql +33 -0
  22. package/server/models/db.js +126 -25
  23. package/server/observability/index.js +394 -0
  24. package/server/protocol/capabilities.js +223 -0
  25. package/server/protocol/index.js +243 -0
  26. package/server/protocol/schema.js +584 -0
  27. package/server/registry/index.js +326 -0
  28. package/server/routes/admin.js +16 -2
  29. package/server/routes/ads.js +130 -0
  30. package/server/routes/agent-workspace.js +378 -0
  31. package/server/routes/api.js +21 -2
  32. package/server/routes/auth.js +26 -6
  33. package/server/routes/runtime.js +725 -0
  34. package/server/routes/sovereign.js +78 -0
  35. package/server/routes/universal.js +177 -0
  36. package/server/routes/wab-api.js +20 -5
  37. package/server/runtime/event-bus.js +210 -0
  38. package/server/runtime/index.js +233 -0
  39. package/server/runtime/sandbox.js +266 -0
  40. package/server/runtime/scheduler.js +395 -0
  41. package/server/runtime/state-manager.js +188 -0
  42. package/server/security/index.js +355 -0
  43. package/server/services/agent-chat.js +506 -0
  44. package/server/services/agent-symphony.js +6 -0
  45. package/server/services/agent-tasks.js +1807 -0
  46. package/server/services/fairness-engine.js +409 -0
  47. package/server/services/plugins.js +27 -3
  48. package/server/services/price-intelligence.js +565 -0
  49. package/server/services/price-shield.js +1137 -0
  50. package/server/services/search-engine.js +357 -0
  51. package/server/services/security.js +513 -0
  52. package/server/services/universal-scraper.js +661 -0
  53. package/server/ws.js +61 -1
@@ -0,0 +1,394 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * WAB Observability - Structured Logger, Distributed Tracer, Metrics Collector
5
+ *
6
+ * Provides production-grade observability:
7
+ * - Structured logging (JSON)
8
+ * - Distributed tracing (OpenTelemetry-compatible spans)
9
+ * - Metrics (counters, gauges, histograms)
10
+ * - Correlation via traceId/spanId
11
+ */
12
+
13
+ const crypto = require('crypto');
14
+
15
+ // ─── Structured Logger ──────────────────────────────────────────────────────
16
+
17
+ const LogLevel = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, FATAL: 4 };
18
+ const LogLevelNames = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'];
19
+
20
+ class Logger {
21
+ constructor(options = {}) {
22
+ this._level = LogLevel[options.level?.toUpperCase()] ?? LogLevel.INFO;
23
+ this._buffer = [];
24
+ this._maxBuffer = options.maxBuffer || 10000;
25
+ this._sinks = []; // output sinks (console, file, API, etc.)
26
+ this._context = options.context || {};
27
+
28
+ // Default: console sink
29
+ if (options.console !== false) {
30
+ this._sinks.push(this._consoleSink.bind(this));
31
+ }
32
+ }
33
+
34
+ child(context) {
35
+ const childLogger = new Logger({ level: LogLevelNames[this._level], maxBuffer: 0, console: false });
36
+ childLogger._context = { ...this._context, ...context };
37
+ childLogger._sinks = this._sinks;
38
+ childLogger._buffer = this._buffer;
39
+ childLogger._maxBuffer = this._maxBuffer;
40
+ return childLogger;
41
+ }
42
+
43
+ addSink(sink) { this._sinks.push(sink); }
44
+
45
+ debug(msg, data) { this._log(LogLevel.DEBUG, msg, data); }
46
+ info(msg, data) { this._log(LogLevel.INFO, msg, data); }
47
+ warn(msg, data) { this._log(LogLevel.WARN, msg, data); }
48
+ error(msg, data) { this._log(LogLevel.ERROR, msg, data); }
49
+ fatal(msg, data) { this._log(LogLevel.FATAL, msg, data); }
50
+
51
+ _log(level, msg, data) {
52
+ if (level < this._level) return;
53
+
54
+ const entry = {
55
+ timestamp: new Date().toISOString(),
56
+ level: LogLevelNames[level],
57
+ message: msg,
58
+ ...this._context,
59
+ ...(data || {}),
60
+ };
61
+
62
+ // Buffer
63
+ this._buffer.push(entry);
64
+ if (this._buffer.length > this._maxBuffer) {
65
+ this._buffer.splice(0, this._buffer.length - Math.floor(this._maxBuffer * 0.8));
66
+ }
67
+
68
+ // Flush to sinks
69
+ for (const sink of this._sinks) {
70
+ try { sink(entry); } catch (_) { /* sink error, ignore */ }
71
+ }
72
+ }
73
+
74
+ _consoleSink(entry) {
75
+ const { timestamp, level, message, ...rest } = entry;
76
+ const meta = Object.keys(rest).length > 0 ? ` ${JSON.stringify(rest)}` : '';
77
+ const fn = level === 'ERROR' || level === 'FATAL' ? console.error
78
+ : level === 'WARN' ? console.warn : console.log;
79
+ fn(`[${timestamp}] [${level}] ${message}${meta}`);
80
+ }
81
+
82
+ query(filter = {}, limit = 100) {
83
+ let results = this._buffer;
84
+ if (filter.level) results = results.filter(e => e.level === filter.level);
85
+ if (filter.traceId) results = results.filter(e => e.traceId === filter.traceId);
86
+ if (filter.agentId) results = results.filter(e => e.agentId === filter.agentId);
87
+ if (filter.since) results = results.filter(e => new Date(e.timestamp).getTime() >= filter.since);
88
+ if (filter.message) {
89
+ const re = new RegExp(filter.message, 'i');
90
+ results = results.filter(e => re.test(e.message));
91
+ }
92
+ return results.slice(-limit);
93
+ }
94
+ }
95
+
96
+ // ─── Distributed Tracer ─────────────────────────────────────────────────────
97
+
98
+ class Tracer {
99
+ constructor() {
100
+ this._traces = new Map(); // traceId → { spans, metadata }
101
+ this._maxTraces = 5000;
102
+ this._stats = { traces: 0, spans: 0, errors: 0 };
103
+ }
104
+
105
+ /**
106
+ * Start a new trace
107
+ */
108
+ startTrace(name, metadata = {}) {
109
+ const traceId = `trace_${crypto.randomBytes(16).toString('hex')}`;
110
+ const rootSpan = this._createSpan(traceId, name, null, metadata);
111
+
112
+ this._traces.set(traceId, {
113
+ id: traceId,
114
+ name,
115
+ rootSpanId: rootSpan.id,
116
+ spans: new Map([[rootSpan.id, rootSpan]]),
117
+ metadata,
118
+ startedAt: Date.now(),
119
+ completedAt: null,
120
+ status: 'active',
121
+ });
122
+
123
+ this._stats.traces++;
124
+ this._enforceLimit();
125
+ return { traceId, spanId: rootSpan.id, span: rootSpan };
126
+ }
127
+
128
+ /**
129
+ * Start a child span within a trace
130
+ */
131
+ startSpan(traceId, name, parentSpanId = null, metadata = {}) {
132
+ const trace = this._traces.get(traceId);
133
+ if (!trace) return null;
134
+
135
+ const span = this._createSpan(traceId, name, parentSpanId || trace.rootSpanId, metadata);
136
+ trace.spans.set(span.id, span);
137
+ this._stats.spans++;
138
+ return span;
139
+ }
140
+
141
+ /**
142
+ * End a span
143
+ */
144
+ endSpan(traceId, spanId, result = {}) {
145
+ const trace = this._traces.get(traceId);
146
+ if (!trace) return;
147
+
148
+ const span = trace.spans.get(spanId);
149
+ if (!span) return;
150
+
151
+ span.endedAt = Date.now();
152
+ span.duration = span.endedAt - span.startedAt;
153
+ span.status = result.error ? 'error' : 'ok';
154
+ span.result = result;
155
+
156
+ if (result.error) this._stats.errors++;
157
+
158
+ // Check if all spans are done → complete trace
159
+ let allDone = true;
160
+ for (const [, s] of trace.spans) {
161
+ if (!s.endedAt) { allDone = false; break; }
162
+ }
163
+ if (allDone) {
164
+ trace.completedAt = Date.now();
165
+ trace.status = 'completed';
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Add event to a span (point-in-time annotation)
171
+ */
172
+ addEvent(traceId, spanId, name, attributes = {}) {
173
+ const trace = this._traces.get(traceId);
174
+ if (!trace) return;
175
+ const span = trace.spans.get(spanId);
176
+ if (!span) return;
177
+ span.events.push({ name, attributes, timestamp: Date.now() });
178
+ }
179
+
180
+ /**
181
+ * Get full trace
182
+ */
183
+ getTrace(traceId) {
184
+ const trace = this._traces.get(traceId);
185
+ if (!trace) return null;
186
+
187
+ return {
188
+ id: trace.id,
189
+ name: trace.name,
190
+ metadata: trace.metadata,
191
+ status: trace.status,
192
+ startedAt: trace.startedAt,
193
+ completedAt: trace.completedAt,
194
+ duration: trace.completedAt ? trace.completedAt - trace.startedAt : Date.now() - trace.startedAt,
195
+ spans: Array.from(trace.spans.values()).map(s => ({
196
+ id: s.id,
197
+ name: s.name,
198
+ parentId: s.parentId,
199
+ status: s.status,
200
+ duration: s.duration,
201
+ startedAt: s.startedAt,
202
+ endedAt: s.endedAt,
203
+ events: s.events,
204
+ attributes: s.attributes,
205
+ result: s.result,
206
+ })),
207
+ };
208
+ }
209
+
210
+ /**
211
+ * List recent traces
212
+ */
213
+ listTraces(limit = 50, filter = {}) {
214
+ const results = [];
215
+ for (const [, trace] of this._traces) {
216
+ if (filter.status && trace.status !== filter.status) continue;
217
+ if (filter.name && !trace.name.includes(filter.name)) continue;
218
+ if (filter.since && trace.startedAt < filter.since) continue;
219
+ results.push({
220
+ id: trace.id,
221
+ name: trace.name,
222
+ status: trace.status,
223
+ spanCount: trace.spans.size,
224
+ startedAt: trace.startedAt,
225
+ duration: trace.completedAt ? trace.completedAt - trace.startedAt : Date.now() - trace.startedAt,
226
+ });
227
+ if (results.length >= limit) break;
228
+ }
229
+ return results;
230
+ }
231
+
232
+ getStats() {
233
+ return { ...this._stats, activeTraces: this._traces.size };
234
+ }
235
+
236
+ _createSpan(traceId, name, parentId, metadata) {
237
+ return {
238
+ id: `span_${crypto.randomBytes(8).toString('hex')}`,
239
+ traceId,
240
+ name,
241
+ parentId,
242
+ attributes: metadata,
243
+ events: [],
244
+ startedAt: Date.now(),
245
+ endedAt: null,
246
+ duration: null,
247
+ status: 'active',
248
+ result: null,
249
+ };
250
+ }
251
+
252
+ _enforceLimit() {
253
+ if (this._traces.size <= this._maxTraces) return;
254
+ // Remove oldest completed traces
255
+ const sorted = Array.from(this._traces.entries())
256
+ .filter(([, t]) => t.status === 'completed')
257
+ .sort((a, b) => a[1].startedAt - b[1].startedAt);
258
+ const toRemove = sorted.slice(0, Math.floor(this._maxTraces * 0.2));
259
+ for (const [id] of toRemove) this._traces.delete(id);
260
+ }
261
+
262
+ cleanup(maxAge = 3600_000) {
263
+ const cutoff = Date.now() - maxAge;
264
+ for (const [id, trace] of this._traces) {
265
+ if (trace.status === 'completed' && trace.completedAt < cutoff) {
266
+ this._traces.delete(id);
267
+ }
268
+ }
269
+ }
270
+ }
271
+
272
+ // ─── Metrics Collector ──────────────────────────────────────────────────────
273
+
274
+ class MetricsCollector {
275
+ constructor() {
276
+ this._counters = new Map(); // name → value
277
+ this._gauges = new Map(); // name → value
278
+ this._histograms = new Map(); // name → { values, sum, count, min, max }
279
+ this._timeSeries = new Map(); // name → [{ value, timestamp }]
280
+ this._maxTimeSeries = 1000;
281
+ }
282
+
283
+ // Counter (monotonically increasing)
284
+ increment(name, value = 1, labels = {}) {
285
+ const key = this._key(name, labels);
286
+ this._counters.set(key, (this._counters.get(key) || 0) + value);
287
+ this._recordTimeSeries(key, this._counters.get(key));
288
+ }
289
+
290
+ getCounter(name, labels = {}) {
291
+ return this._counters.get(this._key(name, labels)) || 0;
292
+ }
293
+
294
+ // Gauge (can go up or down)
295
+ gauge(name, value, labels = {}) {
296
+ const key = this._key(name, labels);
297
+ this._gauges.set(key, value);
298
+ this._recordTimeSeries(key, value);
299
+ }
300
+
301
+ getGauge(name, labels = {}) {
302
+ return this._gauges.get(this._key(name, labels)) || 0;
303
+ }
304
+
305
+ // Histogram (distribution of values)
306
+ observe(name, value, labels = {}) {
307
+ const key = this._key(name, labels);
308
+ let hist = this._histograms.get(key);
309
+ if (!hist) {
310
+ hist = { values: [], sum: 0, count: 0, min: Infinity, max: -Infinity };
311
+ this._histograms.set(key, hist);
312
+ }
313
+ hist.values.push(value);
314
+ hist.sum += value;
315
+ hist.count++;
316
+ if (value < hist.min) hist.min = value;
317
+ if (value > hist.max) hist.max = value;
318
+
319
+ // Keep last 10000 values for percentile calculations
320
+ if (hist.values.length > 10000) {
321
+ hist.values = hist.values.slice(-5000);
322
+ }
323
+ }
324
+
325
+ getHistogram(name, labels = {}) {
326
+ const key = this._key(name, labels);
327
+ const hist = this._histograms.get(key);
328
+ if (!hist) return null;
329
+
330
+ const sorted = [...hist.values].sort((a, b) => a - b);
331
+ return {
332
+ count: hist.count,
333
+ sum: hist.sum,
334
+ avg: hist.sum / hist.count,
335
+ min: hist.min,
336
+ max: hist.max,
337
+ p50: sorted[Math.floor(sorted.length * 0.5)] || 0,
338
+ p90: sorted[Math.floor(sorted.length * 0.9)] || 0,
339
+ p95: sorted[Math.floor(sorted.length * 0.95)] || 0,
340
+ p99: sorted[Math.floor(sorted.length * 0.99)] || 0,
341
+ };
342
+ }
343
+
344
+ // Time series for dashboards
345
+ getTimeSeries(name, labels = {}, since) {
346
+ const key = this._key(name, labels);
347
+ const series = this._timeSeries.get(key) || [];
348
+ if (since) return series.filter(p => p.timestamp >= since);
349
+ return series;
350
+ }
351
+
352
+ // Snapshot (all metrics)
353
+ snapshot() {
354
+ const result = { counters: {}, gauges: {}, histograms: {} };
355
+ for (const [k, v] of this._counters) result.counters[k] = v;
356
+ for (const [k, v] of this._gauges) result.gauges[k] = v;
357
+ for (const [k] of this._histograms) result.histograms[k] = this.getHistogram(k);
358
+ return result;
359
+ }
360
+
361
+ /**
362
+ * Timer helper: returns a function to call when done
363
+ */
364
+ startTimer(name, labels = {}) {
365
+ const start = process.hrtime.bigint();
366
+ return () => {
367
+ const duration = Number(process.hrtime.bigint() - start) / 1e6; // ms
368
+ this.observe(name, duration, labels);
369
+ return duration;
370
+ };
371
+ }
372
+
373
+ _key(name, labels) {
374
+ const labelStr = Object.entries(labels).sort().map(([k, v]) => `${k}=${v}`).join(',');
375
+ return labelStr ? `${name}{${labelStr}}` : name;
376
+ }
377
+
378
+ _recordTimeSeries(key, value) {
379
+ if (!this._timeSeries.has(key)) this._timeSeries.set(key, []);
380
+ const series = this._timeSeries.get(key);
381
+ series.push({ value, timestamp: Date.now() });
382
+ if (series.length > this._maxTimeSeries) {
383
+ series.splice(0, series.length - Math.floor(this._maxTimeSeries * 0.8));
384
+ }
385
+ }
386
+ }
387
+
388
+ // ─── Singletons ─────────────────────────────────────────────────────────────
389
+
390
+ const logger = new Logger({ level: process.env.LOG_LEVEL || 'INFO', console: false });
391
+ const tracer = new Tracer();
392
+ const metrics = new MetricsCollector();
393
+
394
+ module.exports = { Logger, Tracer, MetricsCollector, logger, tracer, metrics, LogLevel };
@@ -0,0 +1,223 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * WAB Protocol (WABP) - Capabilities Negotiation
5
+ *
6
+ * Dynamic capability negotiation between agents and sites.
7
+ * Agents request capabilities → sites grant/deny based on policies.
8
+ */
9
+
10
+ const crypto = require('crypto');
11
+
12
+ // ─── Capability Grant ───────────────────────────────────────────────────────
13
+
14
+ class CapabilityGrant {
15
+ constructor(agentId, capabilities, constraints = {}) {
16
+ this.id = `grant_${crypto.randomBytes(16).toString('hex')}`;
17
+ this.agentId = agentId;
18
+ this.capabilities = new Set(capabilities);
19
+ this.constraints = {
20
+ maxCalls: constraints.maxCalls || Infinity,
21
+ expiresAt: constraints.expiresAt || (Date.now() + 3600_000),
22
+ allowedDomains: constraints.allowedDomains || ['*'],
23
+ rateLimit: constraints.rateLimit || { maxPerMinute: 60 },
24
+ ipRestriction: constraints.ipRestriction || null,
25
+ };
26
+ this.usage = { calls: 0, lastUsed: 0 };
27
+ this.revoked = false;
28
+ this.createdAt = Date.now();
29
+ }
30
+
31
+ has(capability) {
32
+ if (this.revoked) return false;
33
+ if (Date.now() > this.constraints.expiresAt) return false;
34
+ if (this.usage.calls >= this.constraints.maxCalls) return false;
35
+ return this.capabilities.has(capability);
36
+ }
37
+
38
+ use(capability) {
39
+ if (!this.has(capability)) return false;
40
+ this.usage.calls++;
41
+ this.usage.lastUsed = Date.now();
42
+ return true;
43
+ }
44
+
45
+ revoke() {
46
+ this.revoked = true;
47
+ }
48
+
49
+ toJSON() {
50
+ return {
51
+ id: this.id,
52
+ agentId: this.agentId,
53
+ capabilities: [...this.capabilities],
54
+ constraints: this.constraints,
55
+ usage: this.usage,
56
+ revoked: this.revoked,
57
+ createdAt: this.createdAt,
58
+ };
59
+ }
60
+ }
61
+
62
+ // ─── Capability Negotiator ──────────────────────────────────────────────────
63
+
64
+ class CapabilityNegotiator {
65
+ constructor() {
66
+ this._grants = new Map(); // grantId → CapabilityGrant
67
+ this._agentGrants = new Map(); // agentId → Set<grantId>
68
+ this._policies = new Map(); // siteId → policy object
69
+ }
70
+
71
+ /**
72
+ * Set site-level capability policy
73
+ */
74
+ setPolicy(siteId, policy) {
75
+ this._policies.set(siteId, {
76
+ allowedCapabilities: new Set(policy.allowedCapabilities || []),
77
+ deniedCapabilities: new Set(policy.deniedCapabilities || []),
78
+ requireApproval: new Set(policy.requireApproval || []),
79
+ maxGrantDuration: policy.maxGrantDuration || 3600_000,
80
+ defaultRateLimit: policy.defaultRateLimit || { maxPerMinute: 60 },
81
+ autoGrant: policy.autoGrant !== false,
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Negotiate capabilities for an agent
87
+ * Returns: { granted: string[], denied: string[], pending: string[], grant: CapabilityGrant }
88
+ */
89
+ negotiate(agentId, requestedCapabilities, siteId, constraints = {}) {
90
+ const policy = this._policies.get(siteId);
91
+ const granted = [];
92
+ const denied = [];
93
+ const pending = [];
94
+
95
+ for (const cap of requestedCapabilities) {
96
+ if (policy) {
97
+ if (policy.deniedCapabilities.has(cap)) {
98
+ denied.push(cap);
99
+ } else if (policy.requireApproval.has(cap)) {
100
+ pending.push(cap);
101
+ } else if (policy.allowedCapabilities.has(cap) || policy.allowedCapabilities.has('*')) {
102
+ granted.push(cap);
103
+ } else if (policy.autoGrant) {
104
+ granted.push(cap);
105
+ } else {
106
+ denied.push(cap);
107
+ }
108
+ } else {
109
+ // No policy = grant low-risk capabilities only
110
+ const riskLevel = _getCapabilityRisk(cap);
111
+ if (riskLevel === 'low') granted.push(cap);
112
+ else if (riskLevel === 'medium') pending.push(cap);
113
+ else denied.push(cap);
114
+ }
115
+ }
116
+
117
+ let grant = null;
118
+ if (granted.length > 0) {
119
+ const maxDuration = policy ? policy.maxGrantDuration : 3600_000;
120
+ grant = new CapabilityGrant(agentId, granted, {
121
+ ...constraints,
122
+ expiresAt: Date.now() + Math.min(constraints.duration || maxDuration, maxDuration),
123
+ rateLimit: policy ? policy.defaultRateLimit : constraints.rateLimit,
124
+ });
125
+ this._grants.set(grant.id, grant);
126
+ if (!this._agentGrants.has(agentId)) this._agentGrants.set(agentId, new Set());
127
+ this._agentGrants.get(agentId).add(grant.id);
128
+ }
129
+
130
+ return { granted, denied, pending, grant };
131
+ }
132
+
133
+ /**
134
+ * Check if agent has capability via any active grant
135
+ */
136
+ check(agentId, capability) {
137
+ const grantIds = this._agentGrants.get(agentId);
138
+ if (!grantIds) return false;
139
+ for (const gid of grantIds) {
140
+ const grant = this._grants.get(gid);
141
+ if (grant && grant.has(capability)) return true;
142
+ }
143
+ return false;
144
+ }
145
+
146
+ /**
147
+ * Use a capability (decrements usage counter)
148
+ */
149
+ use(agentId, capability) {
150
+ const grantIds = this._agentGrants.get(agentId);
151
+ if (!grantIds) return false;
152
+ for (const gid of grantIds) {
153
+ const grant = this._grants.get(gid);
154
+ if (grant && grant.use(capability)) return true;
155
+ }
156
+ return false;
157
+ }
158
+
159
+ /**
160
+ * Revoke all grants for an agent
161
+ */
162
+ revokeAgent(agentId) {
163
+ const grantIds = this._agentGrants.get(agentId);
164
+ if (!grantIds) return;
165
+ for (const gid of grantIds) {
166
+ const grant = this._grants.get(gid);
167
+ if (grant) grant.revoke();
168
+ }
169
+ this._agentGrants.delete(agentId);
170
+ }
171
+
172
+ /**
173
+ * Get all active grants for an agent
174
+ */
175
+ getGrants(agentId) {
176
+ const grantIds = this._agentGrants.get(agentId);
177
+ if (!grantIds) return [];
178
+ const grants = [];
179
+ for (const gid of grantIds) {
180
+ const grant = this._grants.get(gid);
181
+ if (grant && !grant.revoked && Date.now() <= grant.constraints.expiresAt) {
182
+ grants.push(grant.toJSON());
183
+ }
184
+ }
185
+ return grants;
186
+ }
187
+
188
+ /**
189
+ * Cleanup expired grants
190
+ */
191
+ cleanup() {
192
+ const now = Date.now();
193
+ for (const [gid, grant] of this._grants) {
194
+ if (grant.revoked || now > grant.constraints.expiresAt) {
195
+ this._grants.delete(gid);
196
+ const agentGrants = this._agentGrants.get(grant.agentId);
197
+ if (agentGrants) {
198
+ agentGrants.delete(gid);
199
+ if (agentGrants.size === 0) this._agentGrants.delete(grant.agentId);
200
+ }
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ // ─── Risk Assessment ────────────────────────────────────────────────────────
207
+
208
+ const _riskMap = {
209
+ 'browser.read': 'low', 'browser.scroll': 'low', 'browser.screenshot': 'low',
210
+ 'browser.click': 'medium', 'browser.fill': 'medium', 'browser.navigate': 'medium',
211
+ 'browser.execute': 'high',
212
+ 'data.extract': 'low', 'data.compare': 'low', 'data.store': 'medium',
213
+ 'agent.communicate': 'medium', 'agent.spawn': 'high', 'agent.delegate': 'high',
214
+ 'system.api': 'high', 'system.webhook': 'high', 'system.schedule': 'medium',
215
+ 'commerce.price': 'low', 'commerce.negotiate': 'high', 'commerce.purchase': 'critical',
216
+ 'ai.infer': 'medium', 'ai.vision': 'low', 'ai.embed': 'low',
217
+ };
218
+
219
+ function _getCapabilityRisk(capability) {
220
+ return _riskMap[capability] || 'high';
221
+ }
222
+
223
+ module.exports = { CapabilityGrant, CapabilityNegotiator };