web-agent-bridge 2.4.0 → 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.
@@ -0,0 +1,725 @@
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 { identity, signer, isolation } = require('../security');
23
+ const { agentManager, policyEngine } = require('../control-plane');
24
+ const { executor } = require('../data-plane');
25
+ const { llm } = require('../llm');
26
+ const { commandRegistry, siteRegistry, templateRegistry } = require('../registry');
27
+
28
+ // ═══════════════════════════════════════════════════════════════════════════
29
+ // PROTOCOL ENDPOINTS
30
+ // ═══════════════════════════════════════════════════════════════════════════
31
+
32
+ /**
33
+ * Protocol info & capabilities
34
+ */
35
+ router.get('/protocol', (req, res) => {
36
+ res.json({
37
+ protocol: protocol.PROTOCOL_NAME,
38
+ version: protocol.PROTOCOL_VERSION,
39
+ commands: protocol.schema.listCommands().map(c => ({
40
+ name: c.name,
41
+ version: c.version,
42
+ category: c.category,
43
+ description: c.description,
44
+ capabilities: c.capabilities,
45
+ })),
46
+ capabilities: Object.keys(protocol.schema.Capabilities),
47
+ permissionLevels: protocol.schema.PermissionLevels,
48
+ });
49
+ });
50
+
51
+ /**
52
+ * Process a protocol message
53
+ */
54
+ router.post('/protocol/message', async (req, res) => {
55
+ const endTimer = metrics.startTimer('api.protocol.message.duration');
56
+ try {
57
+ const msg = req.body;
58
+ if (!msg || !msg.command) {
59
+ return res.status(400).json({ error: 'Invalid protocol message' });
60
+ }
61
+
62
+ // Create proper protocol request if not already
63
+ const request = msg.protocol === 'wabp' ? msg : protocol.createRequest(msg.command, msg.payload || msg.params || {}, {
64
+ agentId: msg.agentId,
65
+ traceId: msg.traceId,
66
+ });
67
+
68
+ const response = await protocolHandler.process(request);
69
+ endTimer();
70
+ metrics.increment('api.protocol.messages', 1, { command: msg.command });
71
+ res.json(response);
72
+ } catch (err) {
73
+ endTimer();
74
+ res.status(500).json({ error: err.message });
75
+ }
76
+ });
77
+
78
+ // ═══════════════════════════════════════════════════════════════════════════
79
+ // AGENT IDENTITY & AUTH
80
+ // ═══════════════════════════════════════════════════════════════════════════
81
+
82
+ /**
83
+ * Register a new agent
84
+ */
85
+ router.post('/agents/register', (req, res) => {
86
+ try {
87
+ const { name, type, capabilities, publicKey, metadata } = req.body;
88
+ if (!name || !type) return res.status(400).json({ error: 'name and type required' });
89
+
90
+ const result = identity.register(name, type, { capabilities, publicKey, metadata });
91
+ metrics.increment('agents.registered');
92
+ logger.info('Agent registered', { agentId: result.agentId, name, type });
93
+
94
+ res.json({
95
+ agentId: result.agentId,
96
+ apiKey: result.apiKey, // Only returned once!
97
+ message: 'Store your API key securely. It cannot be recovered.',
98
+ });
99
+ } catch (err) {
100
+ res.status(500).json({ error: err.message });
101
+ }
102
+ });
103
+
104
+ /**
105
+ * Authenticate agent
106
+ */
107
+ router.post('/agents/authenticate', (req, res) => {
108
+ const { apiKey } = req.body;
109
+ if (!apiKey) return res.status(400).json({ error: 'apiKey required' });
110
+
111
+ const ip = req.ip || req.connection?.remoteAddress;
112
+ const session = identity.authenticate(apiKey, ip);
113
+ if (!session) {
114
+ metrics.increment('agents.auth.failed');
115
+ return res.status(401).json({ error: 'Invalid API key or agent revoked' });
116
+ }
117
+
118
+ metrics.increment('agents.auth.success');
119
+ res.json(session);
120
+ });
121
+
122
+ /**
123
+ * Get agent info
124
+ */
125
+ router.get('/agents/:agentId', (req, res) => {
126
+ const agent = identity.getAgent(req.params.agentId);
127
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
128
+ res.json(agent);
129
+ });
130
+
131
+ /**
132
+ * List agents
133
+ */
134
+ router.get('/agents', (req, res) => {
135
+ const agents = identity.listAgents({ type: req.query.type, status: req.query.status || 'active' });
136
+ res.json({ agents, total: agents.length });
137
+ });
138
+
139
+ /**
140
+ * Negotiate capabilities
141
+ */
142
+ router.post('/agents/:agentId/capabilities', (req, res) => {
143
+ const { capabilities, siteId, constraints } = req.body;
144
+ if (!capabilities || !Array.isArray(capabilities)) {
145
+ return res.status(400).json({ error: 'capabilities array required' });
146
+ }
147
+
148
+ const result = protocol.negotiator.negotiate(req.params.agentId, capabilities, siteId, constraints || {});
149
+ res.json(result);
150
+ });
151
+
152
+ /**
153
+ * Revoke agent
154
+ */
155
+ router.delete('/agents/:agentId', (req, res) => {
156
+ identity.revoke(req.params.agentId);
157
+ protocol.negotiator.revokeAgent(req.params.agentId);
158
+ logger.info('Agent revoked', { agentId: req.params.agentId });
159
+ res.json({ success: true });
160
+ });
161
+
162
+ // ═══════════════════════════════════════════════════════════════════════════
163
+ // TASK MANAGEMENT (RUNTIME)
164
+ // ═══════════════════════════════════════════════════════════════════════════
165
+
166
+ /**
167
+ * Submit a task
168
+ */
169
+ router.post('/tasks', (req, res) => {
170
+ try {
171
+ const result = runtime.submitTask(req.body);
172
+ metrics.increment('tasks.submitted', 1, { type: req.body.type });
173
+ res.json(result);
174
+ } catch (err) {
175
+ res.status(400).json({ error: err.message });
176
+ }
177
+ });
178
+
179
+ /**
180
+ * Get task status
181
+ */
182
+ router.get('/tasks/:taskId', (req, res) => {
183
+ const task = runtime.scheduler.getTask(req.params.taskId);
184
+ if (!task) return res.status(404).json({ error: 'Task not found' });
185
+ res.json(task);
186
+ });
187
+
188
+ /**
189
+ * List tasks
190
+ */
191
+ router.get('/tasks', (req, res) => {
192
+ const tasks = runtime.scheduler.listTasks(req.query.state, parseInt(req.query.limit) || 50);
193
+ res.json({ tasks, total: tasks.length });
194
+ });
195
+
196
+ /**
197
+ * Cancel a task
198
+ */
199
+ router.delete('/tasks/:taskId', (req, res) => {
200
+ const success = runtime.scheduler.cancel(req.params.taskId);
201
+ res.json({ success });
202
+ });
203
+
204
+ /**
205
+ * Pause a task
206
+ */
207
+ router.post('/tasks/:taskId/pause', (req, res) => {
208
+ const success = runtime.scheduler.pause(req.params.taskId);
209
+ res.json({ success });
210
+ });
211
+
212
+ /**
213
+ * Resume a task
214
+ */
215
+ router.post('/tasks/:taskId/resume', (req, res) => {
216
+ const success = runtime.scheduler.resume(req.params.taskId);
217
+ res.json({ success });
218
+ });
219
+
220
+ // ═══════════════════════════════════════════════════════════════════════════
221
+ // EXECUTION (DATA PLANE)
222
+ // ═══════════════════════════════════════════════════════════════════════════
223
+
224
+ /**
225
+ * Execute a semantic action
226
+ */
227
+ router.post('/execute', async (req, res) => {
228
+ try {
229
+ const result = await executor.execute(req.body);
230
+ res.json(result);
231
+ } catch (err) {
232
+ res.status(500).json({ error: err.message });
233
+ }
234
+ });
235
+
236
+ /**
237
+ * Execute semantic action (domain.action style)
238
+ */
239
+ router.post('/execute/semantic', async (req, res) => {
240
+ try {
241
+ const { domain, action, params, siteId, agentId, siteDomain } = req.body;
242
+ if (!domain || !action) return res.status(400).json({ error: 'domain and action required' });
243
+
244
+ const result = await executor.execute({
245
+ type: 'semantic',
246
+ domain,
247
+ action,
248
+ params: params || {},
249
+ siteId,
250
+ agentId,
251
+ siteDomain,
252
+ });
253
+ res.json(result);
254
+ } catch (err) {
255
+ res.status(500).json({ error: err.message });
256
+ }
257
+ });
258
+
259
+ /**
260
+ * Execute a pipeline
261
+ */
262
+ router.post('/execute/pipeline', async (req, res) => {
263
+ try {
264
+ const result = await executor.execute({ ...req.body, type: 'pipeline' });
265
+ res.json(result);
266
+ } catch (err) {
267
+ res.status(500).json({ error: err.message });
268
+ }
269
+ });
270
+
271
+ /**
272
+ * Resolve a semantic action (without executing)
273
+ */
274
+ router.get('/execute/resolve', (req, res) => {
275
+ const { domain, action, siteDomain } = req.query;
276
+ if (!domain || !action) return res.status(400).json({ error: 'domain and action required' });
277
+ const impl = executor.resolver.resolve(siteDomain || '*', `${domain}.${action}`);
278
+ if (!impl) return res.status(404).json({ error: 'No implementation found' });
279
+ res.json(impl);
280
+ });
281
+
282
+ // ═══════════════════════════════════════════════════════════════════════════
283
+ // CONTROL PLANE
284
+ // ═══════════════════════════════════════════════════════════════════════════
285
+
286
+ /**
287
+ * Deploy an agent
288
+ */
289
+ router.post('/deployments', (req, res) => {
290
+ try {
291
+ const { agentId, config } = req.body;
292
+ if (!agentId) return res.status(400).json({ error: 'agentId required' });
293
+ const deployment = agentManager.deploy(agentId, config || {});
294
+ res.json(deployment);
295
+ } catch (err) {
296
+ res.status(400).json({ error: err.message });
297
+ }
298
+ });
299
+
300
+ /**
301
+ * List deployments
302
+ */
303
+ router.get('/deployments', (req, res) => {
304
+ const deployments = agentManager.listDeployments({
305
+ status: req.query.status,
306
+ agentId: req.query.agentId,
307
+ });
308
+ res.json({ deployments, total: deployments.length });
309
+ });
310
+
311
+ /**
312
+ * Create a policy
313
+ */
314
+ router.post('/policies', (req, res) => {
315
+ try {
316
+ const policy = policyEngine.createPolicy(req.body);
317
+ res.json(policy);
318
+ } catch (err) {
319
+ res.status(400).json({ error: err.message });
320
+ }
321
+ });
322
+
323
+ /**
324
+ * Bind policy to entity
325
+ */
326
+ router.post('/policies/:policyId/bind', (req, res) => {
327
+ const { entityId } = req.body;
328
+ if (!entityId) return res.status(400).json({ error: 'entityId required' });
329
+ policyEngine.bind(entityId, req.params.policyId);
330
+ res.json({ success: true });
331
+ });
332
+
333
+ /**
334
+ * Evaluate policies
335
+ */
336
+ router.post('/policies/evaluate', (req, res) => {
337
+ const { entityId, action, context } = req.body;
338
+ if (!entityId || !action) return res.status(400).json({ error: 'entityId and action required' });
339
+ const result = policyEngine.evaluate(entityId, action, context || {});
340
+ res.json(result);
341
+ });
342
+
343
+ /**
344
+ * List policies
345
+ */
346
+ router.get('/policies', (req, res) => {
347
+ const policies = policyEngine.listPolicies(req.query.entityId);
348
+ res.json({ policies, total: policies.length });
349
+ });
350
+
351
+ // ═══════════════════════════════════════════════════════════════════════════
352
+ // SITE ISOLATION
353
+ // ═══════════════════════════════════════════════════════════════════════════
354
+
355
+ /**
356
+ * Configure site isolation
357
+ */
358
+ router.post('/isolation/:siteId', (req, res) => {
359
+ isolation.configure(req.params.siteId, req.body);
360
+ res.json({ success: true });
361
+ });
362
+
363
+ /**
364
+ * Get site isolation config
365
+ */
366
+ router.get('/isolation/:siteId', (req, res) => {
367
+ const config = isolation.getConfig(req.params.siteId);
368
+ if (!config) return res.status(404).json({ error: 'No isolation config' });
369
+ res.json(config);
370
+ });
371
+
372
+ // ═══════════════════════════════════════════════════════════════════════════
373
+ // OBSERVABILITY
374
+ // ═══════════════════════════════════════════════════════════════════════════
375
+
376
+ /**
377
+ * Get metrics snapshot
378
+ */
379
+ router.get('/observability/metrics', (req, res) => {
380
+ res.json(metrics.snapshot());
381
+ });
382
+
383
+ /**
384
+ * Get specific metric
385
+ */
386
+ router.get('/observability/metrics/:name', (req, res) => {
387
+ const h = metrics.getHistogram(req.params.name);
388
+ if (h) return res.json({ type: 'histogram', name: req.params.name, ...h });
389
+
390
+ const c = metrics.getCounter(req.params.name);
391
+ if (c) return res.json({ type: 'counter', name: req.params.name, value: c });
392
+
393
+ const g = metrics.getGauge(req.params.name);
394
+ if (g) return res.json({ type: 'gauge', name: req.params.name, value: g });
395
+
396
+ res.status(404).json({ error: 'Metric not found' });
397
+ });
398
+
399
+ /**
400
+ * List traces
401
+ */
402
+ router.get('/observability/traces', (req, res) => {
403
+ const traces = tracer.listTraces(
404
+ parseInt(req.query.limit) || 50,
405
+ { status: req.query.status, name: req.query.name, since: parseInt(req.query.since) || undefined }
406
+ );
407
+ res.json({ traces, total: traces.length });
408
+ });
409
+
410
+ /**
411
+ * Get trace details
412
+ */
413
+ router.get('/observability/traces/:traceId', (req, res) => {
414
+ const trace = tracer.getTrace(req.params.traceId);
415
+ if (!trace) return res.status(404).json({ error: 'Trace not found' });
416
+ res.json(trace);
417
+ });
418
+
419
+ /**
420
+ * Query logs
421
+ */
422
+ router.get('/observability/logs', (req, res) => {
423
+ const logs = logger.query({
424
+ level: req.query.level,
425
+ traceId: req.query.traceId,
426
+ agentId: req.query.agentId,
427
+ since: parseInt(req.query.since) || undefined,
428
+ message: req.query.message,
429
+ }, parseInt(req.query.limit) || 100);
430
+ res.json({ logs, total: logs.length });
431
+ });
432
+
433
+ /**
434
+ * Runtime health
435
+ */
436
+ router.get('/observability/health', (req, res) => {
437
+ const health = runtime.getHealth();
438
+ health.identity = identity.getStats();
439
+ health.registry = {
440
+ commands: commandRegistry.getStats(),
441
+ sites: siteRegistry.getStats(),
442
+ templates: templateRegistry.getStats(),
443
+ };
444
+ health.executor = executor.getStats();
445
+ health.llm = llm.getStatus();
446
+ res.json(health);
447
+ });
448
+
449
+ // ═══════════════════════════════════════════════════════════════════════════
450
+ // REGISTRY
451
+ // ═══════════════════════════════════════════════════════════════════════════
452
+
453
+ /**
454
+ * Register a command
455
+ */
456
+ router.post('/registry/commands', (req, res) => {
457
+ try {
458
+ const { siteId, ...command } = req.body;
459
+ if (!siteId) return res.status(400).json({ error: 'siteId required' });
460
+ const entry = commandRegistry.register(siteId, command);
461
+ res.json(entry);
462
+ } catch (err) {
463
+ res.status(400).json({ error: err.message });
464
+ }
465
+ });
466
+
467
+ /**
468
+ * Search commands
469
+ */
470
+ router.get('/registry/commands', (req, res) => {
471
+ const results = commandRegistry.search({
472
+ siteId: req.query.siteId,
473
+ category: req.query.category,
474
+ name: req.query.name,
475
+ tag: req.query.tag,
476
+ capability: req.query.capability,
477
+ limit: parseInt(req.query.limit) || 50,
478
+ });
479
+ res.json({ commands: results, total: results.length });
480
+ });
481
+
482
+ /**
483
+ * Register a site
484
+ */
485
+ router.post('/registry/sites', (req, res) => {
486
+ const { domain, ...info } = req.body;
487
+ if (!domain) return res.status(400).json({ error: 'domain required' });
488
+ const entry = siteRegistry.register(domain, info);
489
+ res.json(entry);
490
+ });
491
+
492
+ /**
493
+ * Search sites
494
+ */
495
+ router.get('/registry/sites', (req, res) => {
496
+ const results = siteRegistry.search({
497
+ tier: req.query.tier,
498
+ capability: req.query.capability,
499
+ name: req.query.name,
500
+ verified: req.query.verified === 'true' ? true : undefined,
501
+ limit: parseInt(req.query.limit) || 50,
502
+ });
503
+ res.json({ sites: results, total: results.length });
504
+ });
505
+
506
+ /**
507
+ * Get site info
508
+ */
509
+ router.get('/registry/sites/:domain', (req, res) => {
510
+ const site = siteRegistry.getSite(req.params.domain);
511
+ if (!site) return res.status(404).json({ error: 'Site not found' });
512
+ res.json(site);
513
+ });
514
+
515
+ /**
516
+ * Register a template
517
+ */
518
+ router.post('/registry/templates', (req, res) => {
519
+ try {
520
+ const entry = templateRegistry.register(req.body);
521
+ res.json(entry);
522
+ } catch (err) {
523
+ res.status(400).json({ error: err.message });
524
+ }
525
+ });
526
+
527
+ /**
528
+ * Search templates
529
+ */
530
+ router.get('/registry/templates', (req, res) => {
531
+ const results = templateRegistry.search({
532
+ category: req.query.category,
533
+ name: req.query.name,
534
+ tag: req.query.tag,
535
+ limit: parseInt(req.query.limit) || 50,
536
+ });
537
+ res.json({ templates: results, total: results.length });
538
+ });
539
+
540
+ /**
541
+ * Get template
542
+ */
543
+ router.get('/registry/templates/:templateId', (req, res) => {
544
+ const tmpl = templateRegistry.getTemplate(req.params.templateId);
545
+ if (!tmpl) return res.status(404).json({ error: 'Template not found' });
546
+ templateRegistry.trackDownload(req.params.templateId);
547
+ res.json(tmpl);
548
+ });
549
+
550
+ // ═══════════════════════════════════════════════════════════════════════════
551
+ // LLM
552
+ // ═══════════════════════════════════════════════════════════════════════════
553
+
554
+ /**
555
+ * LLM completion
556
+ */
557
+ router.post('/llm/complete', async (req, res) => {
558
+ try {
559
+ const result = await llm.complete(req.body.prompt, req.body.options || req.body);
560
+ metrics.increment('llm.api.requests');
561
+ res.json(result);
562
+ } catch (err) {
563
+ res.status(500).json({ error: err.message });
564
+ }
565
+ });
566
+
567
+ /**
568
+ * LLM models
569
+ */
570
+ router.get('/llm/models', (req, res) => {
571
+ res.json({ models: llm.listModels() });
572
+ });
573
+
574
+ /**
575
+ * LLM status
576
+ */
577
+ router.get('/llm/status', (req, res) => {
578
+ res.json(llm.getStatus());
579
+ });
580
+
581
+ /**
582
+ * LLM embeddings
583
+ */
584
+ router.post('/llm/embed', async (req, res) => {
585
+ try {
586
+ const result = await llm.embed(req.body.text, req.body.options || {});
587
+ res.json(result);
588
+ } catch (err) {
589
+ res.status(500).json({ error: err.message });
590
+ }
591
+ });
592
+
593
+ // ═══════════════════════════════════════════════════════════════════════════
594
+ // COMMAND SIGNING
595
+ // ═══════════════════════════════════════════════════════════════════════════
596
+
597
+ /**
598
+ * Sign a command
599
+ */
600
+ router.post('/sign', (req, res) => {
601
+ const { payload, agentId } = req.body;
602
+ if (!payload || !agentId) return res.status(400).json({ error: 'payload and agentId required' });
603
+ const signature = signer.sign(payload, agentId);
604
+ res.json(signature);
605
+ });
606
+
607
+ /**
608
+ * Verify a signed command
609
+ */
610
+ router.post('/verify', (req, res) => {
611
+ const { payload, agentId, nonce, timestamp, signature } = req.body;
612
+ const result = signer.verify(payload, agentId, nonce, timestamp, signature);
613
+ res.json(result);
614
+ });
615
+
616
+ // ═══════════════════════════════════════════════════════════════════════════
617
+ // EVENT STREAM (SSE)
618
+ // ═══════════════════════════════════════════════════════════════════════════
619
+
620
+ /**
621
+ * Server-Sent Events for real-time updates
622
+ */
623
+ router.get('/events', (req, res) => {
624
+ res.writeHead(200, {
625
+ 'Content-Type': 'text/event-stream',
626
+ 'Cache-Control': 'no-cache',
627
+ 'Connection': 'keep-alive',
628
+ });
629
+
630
+ const filter = req.query.filter; // e.g., 'task.*' or 'agent.*'
631
+
632
+ const subId = bus.on(filter || '*', (data, meta) => {
633
+ res.write(`event: ${meta.event || 'message'}\n`);
634
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
635
+ });
636
+
637
+ req.on('close', () => {
638
+ bus.off(subId);
639
+ res.end();
640
+ });
641
+ });
642
+
643
+ // ═══════════════════════════════════════════════════════════════════════════
644
+ // Protocol Handler Setup
645
+ // ═══════════════════════════════════════════════════════════════════════════
646
+
647
+ const protocolHandler = new protocol.ProtocolHandler();
648
+
649
+ // Wire protocol commands to runtime
650
+ protocolHandler.handle('wab.discover', async (payload) => {
651
+ const commands = commandRegistry.search({ siteId: payload.siteId, category: payload.category });
652
+ return {
653
+ actions: commands.map(c => ({
654
+ name: c.name,
655
+ category: c.category,
656
+ params: c.input,
657
+ capabilities: c.capabilities,
658
+ })),
659
+ meta: {
660
+ protocol: protocol.PROTOCOL_VERSION,
661
+ timestamp: Date.now(),
662
+ },
663
+ };
664
+ });
665
+
666
+ protocolHandler.handle('wab.execute', async (payload, ctx) => {
667
+ const result = await executor.execute({
668
+ type: 'semantic',
669
+ domain: payload.domain || 'general',
670
+ action: payload.action,
671
+ params: payload.params,
672
+ agentId: ctx.message.agentId,
673
+ });
674
+ return result;
675
+ });
676
+
677
+ protocolHandler.handle('wab.task.submit', async (payload) => {
678
+ return runtime.submitTask(payload);
679
+ });
680
+
681
+ protocolHandler.handle('wab.task.status', async (payload) => {
682
+ return runtime.scheduler.getTask(payload.taskId);
683
+ });
684
+
685
+ protocolHandler.handle('wab.agent.register', async (payload) => {
686
+ const result = identity.register(payload.name, payload.type, {
687
+ capabilities: payload.capabilities,
688
+ publicKey: payload.publicKey,
689
+ metadata: payload.metadata,
690
+ });
691
+
692
+ // Negotiate requested capabilities
693
+ const negotiation = protocol.negotiator.negotiate(
694
+ result.agentId,
695
+ payload.capabilities,
696
+ payload.siteId || '*'
697
+ );
698
+
699
+ return {
700
+ agentId: result.agentId,
701
+ token: result.apiKey,
702
+ grantedCapabilities: negotiation.granted,
703
+ expiresAt: negotiation.grant?.constraints?.expiresAt || Date.now() + 3600_000,
704
+ };
705
+ });
706
+
707
+ protocolHandler.handle('wab.ai.infer', async (payload) => {
708
+ return llm.complete(payload.prompt, {
709
+ model: payload.model,
710
+ provider: payload.provider,
711
+ ...payload.options,
712
+ });
713
+ });
714
+
715
+ protocolHandler.handle('wab.commerce.compare', async (payload) => {
716
+ return executor.execute({
717
+ type: 'parallel',
718
+ tasks: (payload.sources || []).map(url => ({
719
+ type: 'extraction',
720
+ params: { url, query: payload.query },
721
+ })),
722
+ });
723
+ });
724
+
725
+ module.exports = router;