web-agent-bridge 2.4.0 → 2.6.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.
@@ -0,0 +1,1136 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * WAB Runtime API Routes
5
+ *
6
+ * Exposes the Agent OS runtime via HTTP:
7
+ * - Task management (submit, status, cancel)
8
+ * - Agent lifecycle (register, authenticate, deploy)
9
+ * - Protocol operations (discover, execute, negotiate)
10
+ * - Observability (metrics, traces, logs)
11
+ * - Registry (commands, sites, templates)
12
+ * - LLM operations (complete, models)
13
+ */
14
+
15
+ const express = require('express');
16
+ const router = express.Router();
17
+
18
+ // Core modules
19
+ const protocol = require('../protocol');
20
+ const { runtime, bus } = require('../runtime');
21
+ const { logger, tracer, metrics } = require('../observability');
22
+ const { failureAnalyzer } = require('../observability/failure-analysis');
23
+ const { identity, signer, isolation } = require('../security');
24
+ const { agentManager, policyEngine } = require('../control-plane');
25
+ const { executor } = require('../data-plane');
26
+ const { llm } = require('../llm');
27
+ const { commandRegistry, siteRegistry, templateRegistry } = require('../registry');
28
+ const { certificationEngine } = require('../registry/certification');
29
+ const { adapterManager, mcpAdapter, restAdapter, browserAdapter } = require('../adapters');
30
+ const { replayEngine } = require('../runtime/replay');
31
+ const { sessionEngine } = require('../runtime/session-engine');
32
+
33
+ // ═══════════════════════════════════════════════════════════════════════════
34
+ // AUTH MIDDLEWARE
35
+ // ═══════════════════════════════════════════════════════════════════════════
36
+
37
+ /**
38
+ * Authenticate requests via API key or session token.
39
+ * Public endpoints (protocol info, agent registration, health) bypass auth.
40
+ */
41
+ const PUBLIC_PATHS = [
42
+ '/protocol',
43
+ '/agents/register',
44
+ '/agents/authenticate',
45
+ '/observability/health',
46
+ '/llm/models',
47
+ '/llm/status',
48
+ '/registry/commands',
49
+ '/registry/sites',
50
+ '/registry/templates',
51
+ ];
52
+
53
+ function authMiddleware(req, res, next) {
54
+ // Allow public GET endpoints
55
+ const matchesPublic = PUBLIC_PATHS.some(p =>
56
+ req.path === p || (req.method === 'GET' && req.path.startsWith(p))
57
+ );
58
+ if (matchesPublic) return next();
59
+
60
+ // Check session token
61
+ const authHeader = req.headers['authorization'];
62
+ if (authHeader && authHeader.startsWith('Bearer ')) {
63
+ const token = authHeader.slice(7);
64
+ const session = identity.validateSession(token);
65
+ if (session) {
66
+ req.agentId = session.agentId;
67
+ req.session = session;
68
+ return next();
69
+ }
70
+ }
71
+
72
+ // Check API key
73
+ const apiKey = req.headers['x-wab-key'];
74
+ if (apiKey) {
75
+ const ip = req.ip || req.connection?.remoteAddress;
76
+ const session = identity.authenticate(apiKey, ip);
77
+ if (session) {
78
+ req.agentId = session.agentId;
79
+ req.session = session;
80
+ return next();
81
+ }
82
+ }
83
+
84
+ // Check agent ID header (for internal/trusted calls)
85
+ const agentHeader = req.headers['x-wab-agent'];
86
+ if (agentHeader) {
87
+ const agent = identity.getAgent(agentHeader);
88
+ if (agent && agent.status === 'active') {
89
+ req.agentId = agentHeader;
90
+ return next();
91
+ }
92
+ }
93
+
94
+ // No auth on non-mutation GET requests (read-only)
95
+ if (req.method === 'GET') return next();
96
+
97
+ metrics.increment('auth.rejected');
98
+ return res.status(401).json({ error: 'Authentication required. Provide X-WAB-Key or Authorization: Bearer <token>' });
99
+ }
100
+
101
+ router.use(authMiddleware);
102
+
103
+ // ═══════════════════════════════════════════════════════════════════════════
104
+ // PROTOCOL ENDPOINTS
105
+ // ═══════════════════════════════════════════════════════════════════════════
106
+
107
+ /**
108
+ * Protocol info & capabilities
109
+ */
110
+ router.get('/protocol', (req, res) => {
111
+ res.json({
112
+ protocol: protocol.PROTOCOL_NAME,
113
+ version: protocol.PROTOCOL_VERSION,
114
+ commands: protocol.schema.listCommands().map(c => ({
115
+ name: c.name,
116
+ version: c.version,
117
+ category: c.category,
118
+ description: c.description,
119
+ capabilities: c.capabilities,
120
+ })),
121
+ capabilities: Object.keys(protocol.schema.Capabilities),
122
+ permissionLevels: protocol.schema.PermissionLevels,
123
+ });
124
+ });
125
+
126
+ /**
127
+ * Process a protocol message
128
+ */
129
+ router.post('/protocol/message', async (req, res) => {
130
+ const endTimer = metrics.startTimer('api.protocol.message.duration');
131
+ try {
132
+ const msg = req.body;
133
+ if (!msg || !msg.command) {
134
+ return res.status(400).json({ error: 'Invalid protocol message' });
135
+ }
136
+
137
+ // Create proper protocol request if not already
138
+ const request = msg.protocol === 'wabp' ? msg : protocol.createRequest(msg.command, msg.payload || msg.params || {}, {
139
+ agentId: msg.agentId,
140
+ traceId: msg.traceId,
141
+ });
142
+
143
+ const response = await protocolHandler.process(request);
144
+ endTimer();
145
+ metrics.increment('api.protocol.messages', 1, { command: msg.command });
146
+ res.json(response);
147
+ } catch (err) {
148
+ endTimer();
149
+ res.status(500).json({ error: err.message });
150
+ }
151
+ });
152
+
153
+ // ═══════════════════════════════════════════════════════════════════════════
154
+ // AGENT IDENTITY & AUTH
155
+ // ═══════════════════════════════════════════════════════════════════════════
156
+
157
+ /**
158
+ * Register a new agent
159
+ */
160
+ router.post('/agents/register', (req, res) => {
161
+ try {
162
+ const { name, type, capabilities, publicKey, metadata } = req.body;
163
+ if (!name || !type) return res.status(400).json({ error: 'name and type required' });
164
+
165
+ const result = identity.register(name, type, { capabilities, publicKey, metadata });
166
+ metrics.increment('agents.registered');
167
+ logger.info('Agent registered', { agentId: result.agentId, name, type });
168
+
169
+ res.json({
170
+ agentId: result.agentId,
171
+ apiKey: result.apiKey, // Only returned once!
172
+ message: 'Store your API key securely. It cannot be recovered.',
173
+ });
174
+ } catch (err) {
175
+ res.status(500).json({ error: err.message });
176
+ }
177
+ });
178
+
179
+ /**
180
+ * Authenticate agent
181
+ */
182
+ router.post('/agents/authenticate', (req, res) => {
183
+ const { apiKey } = req.body;
184
+ if (!apiKey) return res.status(400).json({ error: 'apiKey required' });
185
+
186
+ const ip = req.ip || req.connection?.remoteAddress;
187
+ const session = identity.authenticate(apiKey, ip);
188
+ if (!session) {
189
+ metrics.increment('agents.auth.failed');
190
+ return res.status(401).json({ error: 'Invalid API key or agent revoked' });
191
+ }
192
+
193
+ metrics.increment('agents.auth.success');
194
+ res.json(session);
195
+ });
196
+
197
+ /**
198
+ * Get agent info
199
+ */
200
+ router.get('/agents/:agentId', (req, res) => {
201
+ const agent = identity.getAgent(req.params.agentId);
202
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
203
+ res.json(agent);
204
+ });
205
+
206
+ /**
207
+ * List agents
208
+ */
209
+ router.get('/agents', (req, res) => {
210
+ const agents = identity.listAgents({ type: req.query.type, status: req.query.status || 'active' });
211
+ res.json({ agents, total: agents.length });
212
+ });
213
+
214
+ /**
215
+ * Negotiate capabilities
216
+ */
217
+ router.post('/agents/:agentId/capabilities', (req, res) => {
218
+ const { capabilities, siteId, constraints } = req.body;
219
+ if (!capabilities || !Array.isArray(capabilities)) {
220
+ return res.status(400).json({ error: 'capabilities array required' });
221
+ }
222
+
223
+ const result = protocol.negotiator.negotiate(req.params.agentId, capabilities, siteId, constraints || {});
224
+ res.json(result);
225
+ });
226
+
227
+ /**
228
+ * Revoke agent
229
+ */
230
+ router.delete('/agents/:agentId', (req, res) => {
231
+ identity.revoke(req.params.agentId);
232
+ protocol.negotiator.revokeAgent(req.params.agentId);
233
+ logger.info('Agent revoked', { agentId: req.params.agentId });
234
+ res.json({ success: true });
235
+ });
236
+
237
+ // ═══════════════════════════════════════════════════════════════════════════
238
+ // TASK MANAGEMENT (RUNTIME)
239
+ // ═══════════════════════════════════════════════════════════════════════════
240
+
241
+ /**
242
+ * Submit a task
243
+ */
244
+ router.post('/tasks', (req, res) => {
245
+ try {
246
+ const result = runtime.submitTask(req.body);
247
+ metrics.increment('tasks.submitted', 1, { type: req.body.type });
248
+ res.json(result);
249
+ } catch (err) {
250
+ res.status(400).json({ error: err.message });
251
+ }
252
+ });
253
+
254
+ /**
255
+ * Get task status
256
+ */
257
+ router.get('/tasks/:taskId', (req, res) => {
258
+ const task = runtime.scheduler.getTask(req.params.taskId);
259
+ if (!task) return res.status(404).json({ error: 'Task not found' });
260
+ res.json(task);
261
+ });
262
+
263
+ /**
264
+ * List tasks
265
+ */
266
+ router.get('/tasks', (req, res) => {
267
+ const tasks = runtime.scheduler.listTasks(req.query.state, parseInt(req.query.limit) || 50);
268
+ res.json({ tasks, total: tasks.length });
269
+ });
270
+
271
+ /**
272
+ * Cancel a task
273
+ */
274
+ router.delete('/tasks/:taskId', (req, res) => {
275
+ const success = runtime.scheduler.cancel(req.params.taskId);
276
+ res.json({ success });
277
+ });
278
+
279
+ /**
280
+ * Pause a task
281
+ */
282
+ router.post('/tasks/:taskId/pause', (req, res) => {
283
+ const success = runtime.scheduler.pause(req.params.taskId);
284
+ res.json({ success });
285
+ });
286
+
287
+ /**
288
+ * Resume a task
289
+ */
290
+ router.post('/tasks/:taskId/resume', (req, res) => {
291
+ const success = runtime.scheduler.resume(req.params.taskId);
292
+ res.json({ success });
293
+ });
294
+
295
+ // ═══════════════════════════════════════════════════════════════════════════
296
+ // EXECUTION (DATA PLANE)
297
+ // ═══════════════════════════════════════════════════════════════════════════
298
+
299
+ /**
300
+ * Execute a semantic action
301
+ */
302
+ router.post('/execute', async (req, res) => {
303
+ try {
304
+ const result = await executor.execute(req.body);
305
+ res.json(result);
306
+ } catch (err) {
307
+ res.status(500).json({ error: err.message });
308
+ }
309
+ });
310
+
311
+ /**
312
+ * Execute semantic action (domain.action style)
313
+ */
314
+ router.post('/execute/semantic', async (req, res) => {
315
+ try {
316
+ const { domain, action, params, siteId, agentId, siteDomain } = req.body;
317
+ if (!domain || !action) return res.status(400).json({ error: 'domain and action required' });
318
+
319
+ const result = await executor.execute({
320
+ type: 'semantic',
321
+ domain,
322
+ action,
323
+ params: params || {},
324
+ siteId,
325
+ agentId,
326
+ siteDomain,
327
+ });
328
+ res.json(result);
329
+ } catch (err) {
330
+ res.status(500).json({ error: err.message });
331
+ }
332
+ });
333
+
334
+ /**
335
+ * Execute a pipeline
336
+ */
337
+ router.post('/execute/pipeline', async (req, res) => {
338
+ try {
339
+ const result = await executor.execute({ ...req.body, type: 'pipeline' });
340
+ res.json(result);
341
+ } catch (err) {
342
+ res.status(500).json({ error: err.message });
343
+ }
344
+ });
345
+
346
+ /**
347
+ * Resolve a semantic action (without executing)
348
+ */
349
+ router.get('/execute/resolve', (req, res) => {
350
+ const { domain, action, siteDomain } = req.query;
351
+ if (!domain || !action) return res.status(400).json({ error: 'domain and action required' });
352
+ const impl = executor.resolver.resolve(siteDomain || '*', `${domain}.${action}`);
353
+ if (!impl) return res.status(404).json({ error: 'No implementation found' });
354
+ res.json(impl);
355
+ });
356
+
357
+ // ═══════════════════════════════════════════════════════════════════════════
358
+ // CONTROL PLANE
359
+ // ═══════════════════════════════════════════════════════════════════════════
360
+
361
+ /**
362
+ * Deploy an agent
363
+ */
364
+ router.post('/deployments', (req, res) => {
365
+ try {
366
+ const { agentId, config } = req.body;
367
+ if (!agentId) return res.status(400).json({ error: 'agentId required' });
368
+ const deployment = agentManager.deploy(agentId, config || {});
369
+ res.json(deployment);
370
+ } catch (err) {
371
+ res.status(400).json({ error: err.message });
372
+ }
373
+ });
374
+
375
+ /**
376
+ * List deployments
377
+ */
378
+ router.get('/deployments', (req, res) => {
379
+ const deployments = agentManager.listDeployments({
380
+ status: req.query.status,
381
+ agentId: req.query.agentId,
382
+ });
383
+ res.json({ deployments, total: deployments.length });
384
+ });
385
+
386
+ /**
387
+ * Create a policy
388
+ */
389
+ router.post('/policies', (req, res) => {
390
+ try {
391
+ const policy = policyEngine.createPolicy(req.body);
392
+ res.json(policy);
393
+ } catch (err) {
394
+ res.status(400).json({ error: err.message });
395
+ }
396
+ });
397
+
398
+ /**
399
+ * Bind policy to entity
400
+ */
401
+ router.post('/policies/:policyId/bind', (req, res) => {
402
+ const { entityId } = req.body;
403
+ if (!entityId) return res.status(400).json({ error: 'entityId required' });
404
+ policyEngine.bind(entityId, req.params.policyId);
405
+ res.json({ success: true });
406
+ });
407
+
408
+ /**
409
+ * Evaluate policies
410
+ */
411
+ router.post('/policies/evaluate', (req, res) => {
412
+ const { entityId, action, context } = req.body;
413
+ if (!entityId || !action) return res.status(400).json({ error: 'entityId and action required' });
414
+ const result = policyEngine.evaluate(entityId, action, context || {});
415
+ res.json(result);
416
+ });
417
+
418
+ /**
419
+ * List policies
420
+ */
421
+ router.get('/policies', (req, res) => {
422
+ const policies = policyEngine.listPolicies(req.query.entityId);
423
+ res.json({ policies, total: policies.length });
424
+ });
425
+
426
+ // ═══════════════════════════════════════════════════════════════════════════
427
+ // SITE ISOLATION
428
+ // ═══════════════════════════════════════════════════════════════════════════
429
+
430
+ /**
431
+ * Configure site isolation
432
+ */
433
+ router.post('/isolation/:siteId', (req, res) => {
434
+ isolation.configure(req.params.siteId, req.body);
435
+ res.json({ success: true });
436
+ });
437
+
438
+ /**
439
+ * Get site isolation config
440
+ */
441
+ router.get('/isolation/:siteId', (req, res) => {
442
+ const config = isolation.getConfig(req.params.siteId);
443
+ if (!config) return res.status(404).json({ error: 'No isolation config' });
444
+ res.json(config);
445
+ });
446
+
447
+ // ═══════════════════════════════════════════════════════════════════════════
448
+ // OBSERVABILITY
449
+ // ═══════════════════════════════════════════════════════════════════════════
450
+
451
+ /**
452
+ * Get metrics snapshot
453
+ */
454
+ router.get('/observability/metrics', (req, res) => {
455
+ res.json(metrics.snapshot());
456
+ });
457
+
458
+ /**
459
+ * Get specific metric
460
+ */
461
+ router.get('/observability/metrics/:name', (req, res) => {
462
+ const h = metrics.getHistogram(req.params.name);
463
+ if (h) return res.json({ type: 'histogram', name: req.params.name, ...h });
464
+
465
+ const c = metrics.getCounter(req.params.name);
466
+ if (c) return res.json({ type: 'counter', name: req.params.name, value: c });
467
+
468
+ const g = metrics.getGauge(req.params.name);
469
+ if (g) return res.json({ type: 'gauge', name: req.params.name, value: g });
470
+
471
+ res.status(404).json({ error: 'Metric not found' });
472
+ });
473
+
474
+ /**
475
+ * List traces
476
+ */
477
+ router.get('/observability/traces', (req, res) => {
478
+ const traces = tracer.listTraces(
479
+ parseInt(req.query.limit) || 50,
480
+ { status: req.query.status, name: req.query.name, since: parseInt(req.query.since) || undefined }
481
+ );
482
+ res.json({ traces, total: traces.length });
483
+ });
484
+
485
+ /**
486
+ * Get trace details
487
+ */
488
+ router.get('/observability/traces/:traceId', (req, res) => {
489
+ const trace = tracer.getTrace(req.params.traceId);
490
+ if (!trace) return res.status(404).json({ error: 'Trace not found' });
491
+ res.json(trace);
492
+ });
493
+
494
+ /**
495
+ * Query logs
496
+ */
497
+ router.get('/observability/logs', (req, res) => {
498
+ const logs = logger.query({
499
+ level: req.query.level,
500
+ traceId: req.query.traceId,
501
+ agentId: req.query.agentId,
502
+ since: parseInt(req.query.since) || undefined,
503
+ message: req.query.message,
504
+ }, parseInt(req.query.limit) || 100);
505
+ res.json({ logs, total: logs.length });
506
+ });
507
+
508
+ /**
509
+ * Runtime health
510
+ */
511
+ router.get('/observability/health', (req, res) => {
512
+ const health = runtime.getHealth();
513
+ health.identity = identity.getStats();
514
+ health.registry = {
515
+ commands: commandRegistry.getStats(),
516
+ sites: siteRegistry.getStats(),
517
+ templates: templateRegistry.getStats(),
518
+ };
519
+ health.executor = executor.getStats();
520
+ health.llm = llm.getStatus();
521
+ health.adapters = adapterManager.getStats();
522
+ health.replay = replayEngine.getStats();
523
+ health.sessions = sessionEngine.getStats();
524
+ health.failures = failureAnalyzer.getStats();
525
+ health.certification = certificationEngine.getStats();
526
+ res.json(health);
527
+ });
528
+
529
+ // ═══════════════════════════════════════════════════════════════════════════
530
+ // REGISTRY
531
+ // ═══════════════════════════════════════════════════════════════════════════
532
+
533
+ /**
534
+ * Register a command
535
+ */
536
+ router.post('/registry/commands', (req, res) => {
537
+ try {
538
+ const { siteId, ...command } = req.body;
539
+ if (!siteId) return res.status(400).json({ error: 'siteId required' });
540
+ const entry = commandRegistry.register(siteId, command);
541
+ res.json(entry);
542
+ } catch (err) {
543
+ res.status(400).json({ error: err.message });
544
+ }
545
+ });
546
+
547
+ /**
548
+ * Search commands
549
+ */
550
+ router.get('/registry/commands', (req, res) => {
551
+ const results = commandRegistry.search({
552
+ siteId: req.query.siteId,
553
+ category: req.query.category,
554
+ name: req.query.name,
555
+ tag: req.query.tag,
556
+ capability: req.query.capability,
557
+ limit: parseInt(req.query.limit) || 50,
558
+ });
559
+ res.json({ commands: results, total: results.length });
560
+ });
561
+
562
+ /**
563
+ * Register a site
564
+ */
565
+ router.post('/registry/sites', (req, res) => {
566
+ const { domain, ...info } = req.body;
567
+ if (!domain) return res.status(400).json({ error: 'domain required' });
568
+ const entry = siteRegistry.register(domain, info);
569
+ res.json(entry);
570
+ });
571
+
572
+ /**
573
+ * Search sites
574
+ */
575
+ router.get('/registry/sites', (req, res) => {
576
+ const results = siteRegistry.search({
577
+ tier: req.query.tier,
578
+ capability: req.query.capability,
579
+ name: req.query.name,
580
+ verified: req.query.verified === 'true' ? true : undefined,
581
+ limit: parseInt(req.query.limit) || 50,
582
+ });
583
+ res.json({ sites: results, total: results.length });
584
+ });
585
+
586
+ /**
587
+ * Get site info
588
+ */
589
+ router.get('/registry/sites/:domain', (req, res) => {
590
+ const site = siteRegistry.getSite(req.params.domain);
591
+ if (!site) return res.status(404).json({ error: 'Site not found' });
592
+ res.json(site);
593
+ });
594
+
595
+ /**
596
+ * Register a template
597
+ */
598
+ router.post('/registry/templates', (req, res) => {
599
+ try {
600
+ const entry = templateRegistry.register(req.body);
601
+ res.json(entry);
602
+ } catch (err) {
603
+ res.status(400).json({ error: err.message });
604
+ }
605
+ });
606
+
607
+ /**
608
+ * Search templates
609
+ */
610
+ router.get('/registry/templates', (req, res) => {
611
+ const results = templateRegistry.search({
612
+ category: req.query.category,
613
+ name: req.query.name,
614
+ tag: req.query.tag,
615
+ limit: parseInt(req.query.limit) || 50,
616
+ });
617
+ res.json({ templates: results, total: results.length });
618
+ });
619
+
620
+ /**
621
+ * Get template
622
+ */
623
+ router.get('/registry/templates/:templateId', (req, res) => {
624
+ const tmpl = templateRegistry.getTemplate(req.params.templateId);
625
+ if (!tmpl) return res.status(404).json({ error: 'Template not found' });
626
+ templateRegistry.trackDownload(req.params.templateId);
627
+ res.json(tmpl);
628
+ });
629
+
630
+ // ═══════════════════════════════════════════════════════════════════════════
631
+ // LLM
632
+ // ═══════════════════════════════════════════════════════════════════════════
633
+
634
+ /**
635
+ * LLM completion
636
+ */
637
+ router.post('/llm/complete', async (req, res) => {
638
+ try {
639
+ const result = await llm.complete(req.body.prompt, req.body.options || req.body);
640
+ metrics.increment('llm.api.requests');
641
+ res.json(result);
642
+ } catch (err) {
643
+ res.status(500).json({ error: err.message });
644
+ }
645
+ });
646
+
647
+ /**
648
+ * LLM models
649
+ */
650
+ router.get('/llm/models', (req, res) => {
651
+ res.json({ models: llm.listModels() });
652
+ });
653
+
654
+ /**
655
+ * LLM status
656
+ */
657
+ router.get('/llm/status', (req, res) => {
658
+ res.json(llm.getStatus());
659
+ });
660
+
661
+ /**
662
+ * LLM embeddings
663
+ */
664
+ router.post('/llm/embed', async (req, res) => {
665
+ try {
666
+ const result = await llm.embed(req.body.text, req.body.options || {});
667
+ res.json(result);
668
+ } catch (err) {
669
+ res.status(500).json({ error: err.message });
670
+ }
671
+ });
672
+
673
+ // ═══════════════════════════════════════════════════════════════════════════
674
+ // COMMAND SIGNING
675
+ // ═══════════════════════════════════════════════════════════════════════════
676
+
677
+ /**
678
+ * Sign a command
679
+ */
680
+ router.post('/sign', (req, res) => {
681
+ const { payload, agentId } = req.body;
682
+ if (!payload || !agentId) return res.status(400).json({ error: 'payload and agentId required' });
683
+ const signature = signer.sign(payload, agentId);
684
+ res.json(signature);
685
+ });
686
+
687
+ /**
688
+ * Verify a signed command
689
+ */
690
+ router.post('/verify', (req, res) => {
691
+ const { payload, agentId, nonce, timestamp, signature } = req.body;
692
+ const result = signer.verify(payload, agentId, nonce, timestamp, signature);
693
+ res.json(result);
694
+ });
695
+
696
+ // ═══════════════════════════════════════════════════════════════════════════
697
+ // EVENT STREAM (SSE)
698
+ // ═══════════════════════════════════════════════════════════════════════════
699
+
700
+ /**
701
+ * Server-Sent Events for real-time updates
702
+ */
703
+ router.get('/events', (req, res) => {
704
+ res.writeHead(200, {
705
+ 'Content-Type': 'text/event-stream',
706
+ 'Cache-Control': 'no-cache',
707
+ 'Connection': 'keep-alive',
708
+ });
709
+
710
+ const filter = req.query.filter; // e.g., 'task.*' or 'agent.*'
711
+
712
+ const subId = bus.on(filter || '*', (data, meta) => {
713
+ res.write(`event: ${meta.event || 'message'}\n`);
714
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
715
+ });
716
+
717
+ req.on('close', () => {
718
+ bus.off(subId);
719
+ res.end();
720
+ });
721
+ });
722
+
723
+ // ═══════════════════════════════════════════════════════════════════════════
724
+ // Protocol Handler Setup
725
+ // ═══════════════════════════════════════════════════════════════════════════
726
+
727
+ const protocolHandler = new protocol.ProtocolHandler();
728
+
729
+ // Wire protocol commands to runtime
730
+ protocolHandler.handle('wab.discover', async (payload) => {
731
+ const commands = commandRegistry.search({ siteId: payload.siteId, category: payload.category });
732
+ return {
733
+ actions: commands.map(c => ({
734
+ name: c.name,
735
+ category: c.category,
736
+ params: c.input,
737
+ capabilities: c.capabilities,
738
+ })),
739
+ meta: {
740
+ protocol: protocol.PROTOCOL_VERSION,
741
+ timestamp: Date.now(),
742
+ },
743
+ };
744
+ });
745
+
746
+ protocolHandler.handle('wab.execute', async (payload, ctx) => {
747
+ const result = await executor.execute({
748
+ type: 'semantic',
749
+ domain: payload.domain || 'general',
750
+ action: payload.action,
751
+ params: payload.params,
752
+ agentId: ctx.message.agentId,
753
+ });
754
+ return result;
755
+ });
756
+
757
+ protocolHandler.handle('wab.task.submit', async (payload) => {
758
+ return runtime.submitTask(payload);
759
+ });
760
+
761
+ protocolHandler.handle('wab.task.status', async (payload) => {
762
+ return runtime.scheduler.getTask(payload.taskId);
763
+ });
764
+
765
+ protocolHandler.handle('wab.agent.register', async (payload) => {
766
+ const result = identity.register(payload.name, payload.type, {
767
+ capabilities: payload.capabilities,
768
+ publicKey: payload.publicKey,
769
+ metadata: payload.metadata,
770
+ });
771
+
772
+ // Negotiate requested capabilities
773
+ const negotiation = protocol.negotiator.negotiate(
774
+ result.agentId,
775
+ payload.capabilities,
776
+ payload.siteId || '*'
777
+ );
778
+
779
+ return {
780
+ agentId: result.agentId,
781
+ token: result.apiKey,
782
+ grantedCapabilities: negotiation.granted,
783
+ expiresAt: negotiation.grant?.constraints?.expiresAt || Date.now() + 3600_000,
784
+ };
785
+ });
786
+
787
+ protocolHandler.handle('wab.ai.infer', async (payload) => {
788
+ return llm.complete(payload.prompt, {
789
+ model: payload.model,
790
+ provider: payload.provider,
791
+ ...payload.options,
792
+ });
793
+ });
794
+
795
+ protocolHandler.handle('wab.commerce.compare', async (payload) => {
796
+ return executor.execute({
797
+ type: 'parallel',
798
+ tasks: (payload.sources || []).map(url => ({
799
+ type: 'extraction',
800
+ params: { url, query: payload.query },
801
+ })),
802
+ });
803
+ });
804
+
805
+ // ═══════════════════════════════════════════════════════════════════════════
806
+ // ADAPTERS
807
+ // ═══════════════════════════════════════════════════════════════════════════
808
+
809
+ /**
810
+ * List adapters
811
+ */
812
+ router.get('/adapters', (req, res) => {
813
+ res.json({ adapters: adapterManager.list() });
814
+ });
815
+
816
+ /**
817
+ * Adapter stats
818
+ */
819
+ router.get('/adapters/stats', (req, res) => {
820
+ res.json(adapterManager.getStats());
821
+ });
822
+
823
+ /**
824
+ * MCP: list tools
825
+ */
826
+ router.get('/adapters/mcp/tools', (req, res) => {
827
+ const commands = protocol.schema.listCommands();
828
+ res.json(mcpAdapter.handleListTools(commands));
829
+ });
830
+
831
+ /**
832
+ * MCP: call tool
833
+ */
834
+ router.post('/adapters/mcp/call', async (req, res) => {
835
+ try {
836
+ const result = await mcpAdapter.handleCallTool(req.body, async (wapReq) => {
837
+ const request = protocol.createRequest(wapReq.command, wapReq.payload);
838
+ return protocolHandler.process(request);
839
+ });
840
+ res.json(result);
841
+ } catch (err) {
842
+ res.status(500).json({ error: err.message });
843
+ }
844
+ });
845
+
846
+ /**
847
+ * REST adapter: register endpoint
848
+ */
849
+ router.post('/adapters/rest/endpoints', (req, res) => {
850
+ try {
851
+ const endpoint = restAdapter.registerEndpoint(req.body.id, req.body);
852
+ res.json(endpoint);
853
+ } catch (err) {
854
+ res.status(400).json({ error: err.message });
855
+ }
856
+ });
857
+
858
+ /**
859
+ * REST adapter: list endpoints
860
+ */
861
+ router.get('/adapters/rest/endpoints', (req, res) => {
862
+ res.json({ endpoints: restAdapter.listEndpoints() });
863
+ });
864
+
865
+ /**
866
+ * REST adapter: execute
867
+ */
868
+ router.post('/adapters/rest/execute', async (req, res) => {
869
+ try {
870
+ const result = await restAdapter.execute(req.body.endpoint, req.body.params);
871
+ res.json(result);
872
+ } catch (err) {
873
+ res.status(500).json({ error: err.message });
874
+ }
875
+ });
876
+
877
+ /**
878
+ * Browser adapter: list semantic mappings
879
+ */
880
+ router.get('/adapters/browser/mappings', (req, res) => {
881
+ res.json({ mappings: browserAdapter.listMappings() });
882
+ });
883
+
884
+ /**
885
+ * Browser adapter: resolve semantic action
886
+ */
887
+ router.post('/adapters/browser/resolve', (req, res) => {
888
+ const { domain, action, params } = req.body;
889
+ const plan = browserAdapter.fromWAP({ domain, action, params });
890
+ if (!plan) return res.status(404).json({ error: 'No mapping for this semantic action' });
891
+ res.json(plan);
892
+ });
893
+
894
+ /**
895
+ * Browser adapter: register mapping
896
+ */
897
+ router.post('/adapters/browser/mappings', (req, res) => {
898
+ const { domainAction, plan } = req.body;
899
+ if (!domainAction || !plan) return res.status(400).json({ error: 'domainAction and plan required' });
900
+ browserAdapter.registerMapping(domainAction, plan);
901
+ res.json({ success: true });
902
+ });
903
+
904
+ // ═══════════════════════════════════════════════════════════════════════════
905
+ // REPLAY ENGINE
906
+ // ═══════════════════════════════════════════════════════════════════════════
907
+
908
+ /**
909
+ * List recordings
910
+ */
911
+ router.get('/replay/recordings', (req, res) => {
912
+ res.json({ recordings: replayEngine.listRecordings(parseInt(req.query.limit) || 50) });
913
+ });
914
+
915
+ /**
916
+ * Get recording
917
+ */
918
+ router.get('/replay/recordings/:taskId', (req, res) => {
919
+ const rec = replayEngine.getRecording(req.params.taskId);
920
+ if (!rec) return res.status(404).json({ error: 'Recording not found' });
921
+ res.json(rec);
922
+ });
923
+
924
+ /**
925
+ * Replay a task
926
+ */
927
+ router.post('/replay/:taskId', async (req, res) => {
928
+ try {
929
+ const result = await replayEngine.replay(req.params.taskId, {
930
+ verify: req.body.verify !== false,
931
+ continueOnMismatch: !!req.body.continueOnMismatch,
932
+ });
933
+ res.json(result);
934
+ } catch (err) {
935
+ res.status(400).json({ error: err.message });
936
+ }
937
+ });
938
+
939
+ /**
940
+ * Diff two recordings
941
+ */
942
+ router.get('/replay/diff/:taskId1/:taskId2', (req, res) => {
943
+ const diff = replayEngine.diff(req.params.taskId1, req.params.taskId2);
944
+ if (!diff) return res.status(404).json({ error: 'One or both recordings not found' });
945
+ res.json(diff);
946
+ });
947
+
948
+ /**
949
+ * Replay stats
950
+ */
951
+ router.get('/replay/stats', (req, res) => {
952
+ res.json(replayEngine.getStats());
953
+ });
954
+
955
+ // ═══════════════════════════════════════════════════════════════════════════
956
+ // SESSION ENGINE
957
+ // ═══════════════════════════════════════════════════════════════════════════
958
+
959
+ /**
960
+ * Create browser session
961
+ */
962
+ router.post('/sessions', (req, res) => {
963
+ const session = sessionEngine.create(req.body);
964
+ res.json(session);
965
+ });
966
+
967
+ /**
968
+ * List sessions
969
+ */
970
+ router.get('/sessions', (req, res) => {
971
+ const sessions = sessionEngine.list({
972
+ agentId: req.query.agentId,
973
+ siteId: req.query.siteId,
974
+ state: req.query.state,
975
+ }, parseInt(req.query.limit) || 50);
976
+ res.json({ sessions, total: sessions.length });
977
+ });
978
+
979
+ /**
980
+ * Get session
981
+ */
982
+ router.get('/sessions/:sessionId', (req, res) => {
983
+ const session = sessionEngine.get(req.params.sessionId);
984
+ if (!session) return res.status(404).json({ error: 'Session not found or expired' });
985
+ res.json(session);
986
+ });
987
+
988
+ /**
989
+ * Export session
990
+ */
991
+ router.get('/sessions/:sessionId/export', (req, res) => {
992
+ const data = sessionEngine.export(req.params.sessionId);
993
+ if (!data) return res.status(404).json({ error: 'Session not found' });
994
+ res.json(data);
995
+ });
996
+
997
+ /**
998
+ * Import session
999
+ */
1000
+ router.post('/sessions/import', (req, res) => {
1001
+ const session = sessionEngine.import(req.body);
1002
+ res.json(session);
1003
+ });
1004
+
1005
+ /**
1006
+ * Set cookies
1007
+ */
1008
+ router.post('/sessions/:sessionId/cookies', (req, res) => {
1009
+ sessionEngine.setCookies(req.params.sessionId, req.body.cookies || []);
1010
+ res.json({ success: true });
1011
+ });
1012
+
1013
+ /**
1014
+ * Get cookies
1015
+ */
1016
+ router.get('/sessions/:sessionId/cookies', (req, res) => {
1017
+ const cookies = sessionEngine.getCookies(req.params.sessionId, req.query.domain);
1018
+ res.json({ cookies });
1019
+ });
1020
+
1021
+ /**
1022
+ * Set storage
1023
+ */
1024
+ router.post('/sessions/:sessionId/storage', (req, res) => {
1025
+ const { key, value, type } = req.body;
1026
+ sessionEngine.setStorage(req.params.sessionId, key, value, type);
1027
+ res.json({ success: true });
1028
+ });
1029
+
1030
+ /**
1031
+ * Destroy session
1032
+ */
1033
+ router.delete('/sessions/:sessionId', (req, res) => {
1034
+ sessionEngine.destroy(req.params.sessionId);
1035
+ res.json({ success: true });
1036
+ });
1037
+
1038
+ // ═══════════════════════════════════════════════════════════════════════════
1039
+ // FAILURE ANALYSIS
1040
+ // ═══════════════════════════════════════════════════════════════════════════
1041
+
1042
+ /**
1043
+ * Query failures
1044
+ */
1045
+ router.get('/failures', (req, res) => {
1046
+ const failures = failureAnalyzer.query({
1047
+ classification: req.query.classification,
1048
+ severity: req.query.severity,
1049
+ agentId: req.query.agentId,
1050
+ taskId: req.query.taskId,
1051
+ retryable: req.query.retryable === 'true' ? true : req.query.retryable === 'false' ? false : undefined,
1052
+ since: parseInt(req.query.since) || undefined,
1053
+ }, parseInt(req.query.limit) || 50);
1054
+ res.json({ failures, total: failures.length });
1055
+ });
1056
+
1057
+ /**
1058
+ * Get failure
1059
+ */
1060
+ router.get('/failures/:failureId', (req, res) => {
1061
+ const failure = failureAnalyzer.getFailure(req.params.failureId);
1062
+ if (!failure) return res.status(404).json({ error: 'Failure not found' });
1063
+ res.json(failure);
1064
+ });
1065
+
1066
+ /**
1067
+ * Get failure patterns
1068
+ */
1069
+ router.get('/failures/analysis/patterns', (req, res) => {
1070
+ res.json({ patterns: failureAnalyzer.getPatterns() });
1071
+ });
1072
+
1073
+ /**
1074
+ * Get failure summary
1075
+ */
1076
+ router.get('/failures/analysis/summary', (req, res) => {
1077
+ res.json(failureAnalyzer.getSummary(parseInt(req.query.since) || 0));
1078
+ });
1079
+
1080
+ /**
1081
+ * Classify a failure manually
1082
+ */
1083
+ router.post('/failures/classify', (req, res) => {
1084
+ const { error, context } = req.body;
1085
+ if (!error) return res.status(400).json({ error: 'error object required' });
1086
+ const classification = failureAnalyzer.classify(error, context || {});
1087
+ res.json(classification);
1088
+ });
1089
+
1090
+ // ═══════════════════════════════════════════════════════════════════════════
1091
+ // CERTIFICATION
1092
+ // ═══════════════════════════════════════════════════════════════════════════
1093
+
1094
+ /**
1095
+ * Verify a site
1096
+ */
1097
+ router.post('/certification/verify', async (req, res) => {
1098
+ try {
1099
+ const { domain, probeData } = req.body;
1100
+ if (!domain) return res.status(400).json({ error: 'domain required' });
1101
+ const result = await certificationEngine.verify(domain, probeData || {});
1102
+ res.json(result);
1103
+ } catch (err) {
1104
+ res.status(500).json({ error: err.message });
1105
+ }
1106
+ });
1107
+
1108
+ /**
1109
+ * Get certificate
1110
+ */
1111
+ router.get('/certification/:domain', (req, res) => {
1112
+ const cert = certificationEngine.getCertificate(req.params.domain);
1113
+ if (!cert) return res.status(404).json({ error: 'No active certificate for this domain' });
1114
+ res.json(cert);
1115
+ });
1116
+
1117
+ /**
1118
+ * List certificates
1119
+ */
1120
+ router.get('/certification', (req, res) => {
1121
+ const certs = certificationEngine.listCertificates({
1122
+ level: req.query.level,
1123
+ minScore: parseInt(req.query.minScore) || undefined,
1124
+ }, parseInt(req.query.limit) || 50);
1125
+ res.json({ certificates: certs, total: certs.length });
1126
+ });
1127
+
1128
+ /**
1129
+ * Revoke certificate
1130
+ */
1131
+ router.delete('/certification/:domain', (req, res) => {
1132
+ certificationEngine.revoke(req.params.domain);
1133
+ res.json({ success: true });
1134
+ });
1135
+
1136
+ module.exports = router;