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.
- package/README.ar.md +524 -31
- package/README.md +592 -47
- package/bin/agent-runner.js +10 -1
- package/package.json +1 -1
- package/public/agent-workspace.html +347 -0
- package/public/browser.html +484 -0
- package/public/css/agent-workspace.css +1713 -0
- package/public/index.html +94 -0
- package/public/js/agent-workspace.js +1740 -0
- package/sdk/index.d.ts +253 -0
- package/sdk/index.js +360 -1
- package/sdk/package.json +1 -1
- package/server/config/secrets.js +13 -5
- package/server/control-plane/index.js +301 -0
- package/server/data-plane/index.js +354 -0
- package/server/index.js +185 -4
- package/server/llm/index.js +404 -0
- package/server/middleware/adminAuth.js +6 -1
- package/server/middleware/auth.js +11 -2
- package/server/middleware/rateLimits.js +78 -2
- package/server/migrations/003_ads_integer_cents.sql +33 -0
- package/server/models/db.js +126 -25
- package/server/observability/index.js +394 -0
- package/server/protocol/capabilities.js +223 -0
- package/server/protocol/index.js +243 -0
- package/server/protocol/schema.js +584 -0
- package/server/registry/index.js +326 -0
- package/server/routes/admin.js +16 -2
- package/server/routes/ads.js +130 -0
- package/server/routes/agent-workspace.js +378 -0
- package/server/routes/api.js +21 -2
- package/server/routes/auth.js +26 -6
- package/server/routes/runtime.js +725 -0
- package/server/routes/sovereign.js +78 -0
- package/server/routes/universal.js +177 -0
- package/server/routes/wab-api.js +20 -5
- package/server/runtime/event-bus.js +210 -0
- package/server/runtime/index.js +233 -0
- package/server/runtime/sandbox.js +266 -0
- package/server/runtime/scheduler.js +395 -0
- package/server/runtime/state-manager.js +188 -0
- package/server/security/index.js +355 -0
- package/server/services/agent-chat.js +506 -0
- package/server/services/agent-symphony.js +6 -0
- package/server/services/agent-tasks.js +1807 -0
- package/server/services/fairness-engine.js +409 -0
- package/server/services/plugins.js +27 -3
- package/server/services/price-intelligence.js +565 -0
- package/server/services/price-shield.js +1137 -0
- package/server/services/search-engine.js +357 -0
- package/server/services/security.js +513 -0
- package/server/services/universal-scraper.js +661 -0
- 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 };
|