web-agent-bridge 3.2.0 → 3.3.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 (202) hide show
  1. package/LICENSE +72 -72
  2. package/README.ar.md +1286 -1152
  3. package/README.md +1764 -1635
  4. package/bin/agent-runner.js +474 -474
  5. package/bin/cli.js +237 -138
  6. package/bin/wab.js +80 -80
  7. package/examples/bidi-agent.js +119 -119
  8. package/examples/cross-site-agent.js +91 -91
  9. package/examples/mcp-agent.js +94 -94
  10. package/examples/next-app-router/README.md +44 -44
  11. package/examples/puppeteer-agent.js +108 -108
  12. package/examples/saas-dashboard/README.md +55 -55
  13. package/examples/shopify-hydrogen/README.md +74 -74
  14. package/examples/vision-agent.js +171 -171
  15. package/examples/wordpress-elementor/README.md +77 -77
  16. package/package.json +16 -3
  17. package/public/.well-known/agent-tools.json +180 -180
  18. package/public/.well-known/ai-assets.json +59 -59
  19. package/public/.well-known/security.txt +8 -0
  20. package/public/agent-workspace.html +349 -349
  21. package/public/ai.html +198 -198
  22. package/public/api.html +413 -412
  23. package/public/browser.html +486 -486
  24. package/public/commander-dashboard.html +243 -243
  25. package/public/cookies.html +210 -210
  26. package/public/css/agent-workspace.css +1713 -1713
  27. package/public/css/premium.css +317 -317
  28. package/public/css/styles.css +1235 -1235
  29. package/public/dashboard.html +706 -706
  30. package/public/dns.html +507 -0
  31. package/public/docs.html +587 -587
  32. package/public/feed.xml +89 -89
  33. package/public/growth.html +463 -463
  34. package/public/index.html +1070 -982
  35. package/public/integrations.html +556 -0
  36. package/public/js/agent-workspace.js +1740 -1740
  37. package/public/js/auth-nav.js +31 -31
  38. package/public/js/auth-redirect.js +12 -12
  39. package/public/js/cookie-consent.js +56 -56
  40. package/public/js/wab-demo-page.js +721 -721
  41. package/public/js/ws-client.js +74 -74
  42. package/public/llms-full.txt +360 -360
  43. package/public/llms.txt +125 -125
  44. package/public/login.html +85 -85
  45. package/public/mesh-dashboard.html +328 -328
  46. package/public/openapi.json +580 -580
  47. package/public/phone-shield.html +281 -0
  48. package/public/premium-dashboard.html +2489 -2489
  49. package/public/premium.html +793 -793
  50. package/public/privacy.html +297 -297
  51. package/public/register.html +105 -105
  52. package/public/robots.txt +87 -87
  53. package/public/script/wab-consent.d.ts +36 -36
  54. package/public/script/wab-consent.js +104 -104
  55. package/public/script/wab-schema.js +131 -131
  56. package/public/script/wab.d.ts +108 -108
  57. package/public/script/wab.min.js +580 -580
  58. package/public/security.txt +8 -0
  59. package/public/terms.html +256 -256
  60. package/script/ai-agent-bridge.js +1754 -1754
  61. package/sdk/README.md +99 -99
  62. package/sdk/agent-mesh.js +449 -449
  63. package/sdk/commander.js +262 -262
  64. package/sdk/index.d.ts +464 -464
  65. package/sdk/index.js +12 -1
  66. package/sdk/multi-agent.js +318 -318
  67. package/sdk/package.json +1 -1
  68. package/sdk/safety-shield.js +219 -0
  69. package/sdk/schema-discovery.js +83 -83
  70. package/server/adapters/index.js +520 -520
  71. package/server/config/plans.js +367 -367
  72. package/server/config/secrets.js +102 -102
  73. package/server/control-plane/index.js +301 -301
  74. package/server/data-plane/index.js +354 -354
  75. package/server/index.js +531 -427
  76. package/server/llm/index.js +404 -404
  77. package/server/middleware/adminAuth.js +35 -35
  78. package/server/middleware/auth.js +50 -50
  79. package/server/middleware/featureGate.js +88 -88
  80. package/server/middleware/rateLimits.js +100 -100
  81. package/server/middleware/sensitiveAction.js +157 -0
  82. package/server/migrations/001_add_analytics_indexes.sql +7 -7
  83. package/server/migrations/002_premium_features.sql +418 -418
  84. package/server/migrations/003_ads_integer_cents.sql +33 -33
  85. package/server/migrations/004_agent_os.sql +158 -158
  86. package/server/migrations/005_marketplace_metering.sql +126 -126
  87. package/server/models/adapters/index.js +33 -33
  88. package/server/models/adapters/mysql.js +183 -183
  89. package/server/models/adapters/postgresql.js +172 -172
  90. package/server/models/adapters/sqlite.js +7 -7
  91. package/server/models/db.js +681 -681
  92. package/server/observability/failure-analysis.js +337 -337
  93. package/server/observability/index.js +394 -394
  94. package/server/protocol/capabilities.js +223 -223
  95. package/server/protocol/index.js +243 -243
  96. package/server/protocol/schema.js +584 -584
  97. package/server/registry/certification.js +271 -271
  98. package/server/registry/index.js +326 -326
  99. package/server/routes/admin-premium.js +671 -671
  100. package/server/routes/admin.js +261 -261
  101. package/server/routes/ads.js +130 -130
  102. package/server/routes/agent-workspace.js +540 -540
  103. package/server/routes/api.js +150 -150
  104. package/server/routes/auth.js +71 -71
  105. package/server/routes/billing.js +45 -45
  106. package/server/routes/commander.js +316 -316
  107. package/server/routes/demo-showcase.js +332 -332
  108. package/server/routes/demo-store.js +154 -0
  109. package/server/routes/discovery.js +417 -417
  110. package/server/routes/gateway.js +173 -157
  111. package/server/routes/license.js +251 -240
  112. package/server/routes/mesh.js +469 -469
  113. package/server/routes/noscript.js +543 -543
  114. package/server/routes/premium-v2.js +686 -686
  115. package/server/routes/premium.js +724 -724
  116. package/server/routes/runtime.js +2148 -2147
  117. package/server/routes/sovereign.js +465 -385
  118. package/server/routes/universal.js +200 -185
  119. package/server/routes/wab-api.js +850 -501
  120. package/server/runtime/container-worker.js +111 -111
  121. package/server/runtime/container.js +448 -448
  122. package/server/runtime/distributed-worker.js +362 -362
  123. package/server/runtime/event-bus.js +210 -210
  124. package/server/runtime/index.js +253 -253
  125. package/server/runtime/queue.js +599 -599
  126. package/server/runtime/replay.js +666 -666
  127. package/server/runtime/sandbox.js +266 -266
  128. package/server/runtime/scheduler.js +534 -534
  129. package/server/runtime/session-engine.js +293 -293
  130. package/server/runtime/state-manager.js +188 -188
  131. package/server/security/cross-site-redactor.js +196 -0
  132. package/server/security/dry-run.js +180 -0
  133. package/server/security/human-gate-rate-limit.js +147 -0
  134. package/server/security/human-gate-transports.js +178 -0
  135. package/server/security/human-gate.js +281 -0
  136. package/server/security/index.js +368 -368
  137. package/server/security/intent-engine.js +245 -0
  138. package/server/security/reward-guard.js +171 -0
  139. package/server/security/rollback-store.js +239 -0
  140. package/server/security/token-scope.js +404 -0
  141. package/server/security/url-policy.js +139 -0
  142. package/server/services/agent-chat.js +506 -506
  143. package/server/services/agent-learning.js +601 -575
  144. package/server/services/agent-memory.js +625 -625
  145. package/server/services/agent-mesh.js +555 -539
  146. package/server/services/agent-symphony.js +717 -717
  147. package/server/services/agent-tasks.js +1807 -1807
  148. package/server/services/api-key-engine.js +292 -261
  149. package/server/services/cluster.js +894 -894
  150. package/server/services/commander.js +738 -738
  151. package/server/services/edge-compute.js +440 -440
  152. package/server/services/email.js +204 -204
  153. package/server/services/hosted-runtime.js +205 -205
  154. package/server/services/lfd.js +635 -635
  155. package/server/services/local-ai.js +389 -389
  156. package/server/services/marketplace.js +270 -270
  157. package/server/services/metering.js +182 -182
  158. package/server/services/modules/affiliate-intelligence.js +93 -93
  159. package/server/services/modules/agent-firewall.js +90 -90
  160. package/server/services/modules/bounty.js +89 -89
  161. package/server/services/modules/collective-bargaining.js +92 -92
  162. package/server/services/modules/dark-pattern.js +66 -66
  163. package/server/services/modules/gov-intelligence.js +45 -45
  164. package/server/services/modules/neural.js +55 -55
  165. package/server/services/modules/notary.js +49 -49
  166. package/server/services/modules/price-time-machine.js +86 -86
  167. package/server/services/modules/protocol.js +104 -104
  168. package/server/services/negotiation.js +439 -439
  169. package/server/services/plugins.js +771 -771
  170. package/server/services/price-intelligence.js +566 -566
  171. package/server/services/price-shield.js +1137 -1137
  172. package/server/services/reputation.js +465 -465
  173. package/server/services/search-engine.js +357 -357
  174. package/server/services/security.js +513 -513
  175. package/server/services/self-healing.js +843 -843
  176. package/server/services/sovereign-shield.js +542 -0
  177. package/server/services/stripe.js +192 -192
  178. package/server/services/swarm.js +788 -788
  179. package/server/services/universal-scraper.js +662 -661
  180. package/server/services/verification.js +481 -481
  181. package/server/services/vision.js +1163 -1163
  182. package/server/utils/cache.js +125 -125
  183. package/server/utils/migrate.js +81 -81
  184. package/server/utils/safe-fetch.js +228 -0
  185. package/server/utils/secureFields.js +50 -50
  186. package/server/ws.js +161 -161
  187. package/templates/artisan-marketplace.yaml +104 -104
  188. package/templates/book-price-scout.yaml +98 -98
  189. package/templates/electronics-price-tracker.yaml +108 -108
  190. package/templates/flight-deal-hunter.yaml +113 -113
  191. package/templates/freelancer-direct.yaml +116 -116
  192. package/templates/grocery-price-compare.yaml +93 -93
  193. package/templates/hotel-direct-booking.yaml +113 -113
  194. package/templates/local-services.yaml +98 -98
  195. package/templates/olive-oil-tunisia.yaml +88 -88
  196. package/templates/organic-farm-fresh.yaml +101 -101
  197. package/templates/restaurant-direct.yaml +97 -97
  198. package/public/score.html +0 -263
  199. package/server/migrations/006_growth_suite.sql +0 -138
  200. package/server/routes/growth.js +0 -962
  201. package/server/services/fairness-engine.js +0 -409
  202. package/server/services/fairness.js +0 -420
@@ -1,2147 +1,2148 @@
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 { featureGate, usageLimit } = require('../middleware/featureGate');
32
- const { listPlans, getPlan, USAGE_PRICING, MARKETPLACE } = require('../config/plans');
33
- const metering = require('../services/metering');
34
- const { marketplace } = require('../services/marketplace');
35
- const { hostedRuntime } = require('../services/hosted-runtime');
36
- const { sessionEngine } = require('../runtime/session-engine');
37
- const vision = require('../services/vision');
38
- const { lfdEngine } = require('../services/lfd');
39
- const { cluster, distributor } = require('../services/cluster');
40
-
41
- // ═══════════════════════════════════════════════════════════════════════════
42
- // AUTH MIDDLEWARE
43
- // ═══════════════════════════════════════════════════════════════════════════
44
-
45
- /**
46
- * Authenticate requests via API key or session token.
47
- * Public endpoints (protocol info, agent registration, health) bypass auth.
48
- */
49
- const PUBLIC_PATHS = [
50
- '/protocol',
51
- '/agents/register',
52
- '/agents/authenticate',
53
- '/observability/health',
54
- '/llm/models',
55
- '/llm/status',
56
- '/registry/commands',
57
- '/registry/sites',
58
- '/registry/templates',
59
- '/plans',
60
- '/marketplace',
61
- '/recipes',
62
- '/vision/models',
63
- '/vision/extraction-script',
64
- '/cluster/status',
65
- ];
66
-
67
- function authMiddleware(req, res, next) {
68
- // Allow public GET endpoints
69
- const matchesPublic = PUBLIC_PATHS.some(p =>
70
- req.path === p || (req.method === 'GET' && req.path.startsWith(p))
71
- );
72
- if (matchesPublic) return next();
73
-
74
- // Check session token
75
- const authHeader = req.headers['authorization'];
76
- if (authHeader && authHeader.startsWith('Bearer ')) {
77
- const token = authHeader.slice(7);
78
- const session = identity.validateSession(token);
79
- if (session) {
80
- req.agentId = session.agentId;
81
- req.session = session;
82
- return next();
83
- }
84
- }
85
-
86
- // Check API key
87
- const apiKey = req.headers['x-wab-key'];
88
- if (apiKey) {
89
- const ip = req.ip || req.connection?.remoteAddress;
90
- const session = identity.authenticate(apiKey, ip);
91
- if (session) {
92
- req.agentId = session.agentId;
93
- req.session = session;
94
- return next();
95
- }
96
- }
97
-
98
- // Check agent ID header (for internal/trusted calls)
99
- const agentHeader = req.headers['x-wab-agent'];
100
- if (agentHeader) {
101
- const agent = identity.getAgent(agentHeader);
102
- if (agent && agent.status === 'active') {
103
- req.agentId = agentHeader;
104
- return next();
105
- }
106
- }
107
-
108
- // No auth on non-mutation GET requests (read-only)
109
- if (req.method === 'GET') return next();
110
-
111
- metrics.increment('auth.rejected');
112
- return res.status(401).json({ error: 'Authentication required. Provide X-WAB-Key or Authorization: Bearer <token>' });
113
- }
114
-
115
- router.use(authMiddleware);
116
- router.use(featureGate);
117
-
118
- // ═══════════════════════════════════════════════════════════════════════════
119
- // PROTOCOL ENDPOINTS
120
- // ═══════════════════════════════════════════════════════════════════════════
121
-
122
- /**
123
- * Protocol info & capabilities
124
- */
125
- router.get('/protocol', (req, res) => {
126
- res.json({
127
- protocol: protocol.PROTOCOL_NAME,
128
- version: protocol.PROTOCOL_VERSION,
129
- commands: protocol.schema.listCommands().map(c => ({
130
- name: c.name,
131
- version: c.version,
132
- category: c.category,
133
- description: c.description,
134
- capabilities: c.capabilities,
135
- })),
136
- capabilities: Object.keys(protocol.schema.Capabilities),
137
- permissionLevels: protocol.schema.PermissionLevels,
138
- });
139
- });
140
-
141
- /**
142
- * Process a protocol message
143
- */
144
- router.post('/protocol/message', async (req, res) => {
145
- const endTimer = metrics.startTimer('api.protocol.message.duration');
146
- try {
147
- const msg = req.body;
148
- if (!msg || !msg.command) {
149
- return res.status(400).json({ error: 'Invalid protocol message' });
150
- }
151
-
152
- // Create proper protocol request if not already
153
- const request = msg.protocol === 'wabp' ? msg : protocol.createRequest(msg.command, msg.payload || msg.params || {}, {
154
- agentId: msg.agentId,
155
- traceId: msg.traceId,
156
- });
157
-
158
- const response = await protocolHandler.process(request);
159
- endTimer();
160
- metrics.increment('api.protocol.messages', 1, { command: msg.command });
161
- res.json(response);
162
- } catch (err) {
163
- endTimer();
164
- res.status(500).json({ error: err.message });
165
- }
166
- });
167
-
168
- // ═══════════════════════════════════════════════════════════════════════════
169
- // AGENT IDENTITY & AUTH
170
- // ═══════════════════════════════════════════════════════════════════════════
171
-
172
- /**
173
- * Register a new agent
174
- */
175
- router.post('/agents/register', (req, res) => {
176
- try {
177
- const { name, type, capabilities, publicKey, metadata } = req.body;
178
- if (!name || !type) return res.status(400).json({ error: 'name and type required' });
179
-
180
- const result = identity.register(name, type, { capabilities, publicKey, metadata });
181
- metrics.increment('agents.registered');
182
- logger.info('Agent registered', { agentId: result.agentId, name, type });
183
-
184
- res.json({
185
- agentId: result.agentId,
186
- apiKey: result.apiKey, // Only returned once!
187
- message: 'Store your API key securely. It cannot be recovered.',
188
- });
189
- } catch (err) {
190
- res.status(500).json({ error: err.message });
191
- }
192
- });
193
-
194
- /**
195
- * Authenticate agent
196
- */
197
- router.post('/agents/authenticate', (req, res) => {
198
- const { apiKey } = req.body;
199
- if (!apiKey) return res.status(400).json({ error: 'apiKey required' });
200
-
201
- const ip = req.ip || req.connection?.remoteAddress;
202
- const session = identity.authenticate(apiKey, ip);
203
- if (!session) {
204
- metrics.increment('agents.auth.failed');
205
- return res.status(401).json({ error: 'Invalid API key or agent revoked' });
206
- }
207
-
208
- metrics.increment('agents.auth.success');
209
- res.json(session);
210
- });
211
-
212
- /**
213
- * Get agent info
214
- */
215
- router.get('/agents/:agentId', (req, res) => {
216
- const agent = identity.getAgent(req.params.agentId);
217
- if (!agent) return res.status(404).json({ error: 'Agent not found' });
218
- res.json(agent);
219
- });
220
-
221
- /**
222
- * List agents
223
- */
224
- router.get('/agents', (req, res) => {
225
- const agents = identity.listAgents({ type: req.query.type, status: req.query.status || 'active' });
226
- res.json({ agents, total: agents.length });
227
- });
228
-
229
- /**
230
- * Negotiate capabilities
231
- */
232
- router.post('/agents/:agentId/capabilities', (req, res) => {
233
- const { capabilities, siteId, constraints } = req.body;
234
- if (!capabilities || !Array.isArray(capabilities)) {
235
- return res.status(400).json({ error: 'capabilities array required' });
236
- }
237
-
238
- const result = protocol.negotiator.negotiate(req.params.agentId, capabilities, siteId, constraints || {});
239
- res.json(result);
240
- });
241
-
242
- /**
243
- * Revoke agent
244
- */
245
- router.delete('/agents/:agentId', (req, res) => {
246
- identity.revoke(req.params.agentId);
247
- protocol.negotiator.revokeAgent(req.params.agentId);
248
- logger.info('Agent revoked', { agentId: req.params.agentId });
249
- res.json({ success: true });
250
- });
251
-
252
- // ═══════════════════════════════════════════════════════════════════════════
253
- // TASK MANAGEMENT (RUNTIME)
254
- // ═══════════════════════════════════════════════════════════════════════════
255
-
256
- /**
257
- * Submit a task
258
- */
259
- router.post('/tasks', usageLimit('tasksPerDay'), (req, res) => {
260
- try {
261
- const result = runtime.submitTask(req.body);
262
- metrics.increment('tasks.submitted', 1, { type: req.body.type });
263
- res.json(result);
264
- } catch (err) {
265
- res.status(400).json({ error: err.message });
266
- }
267
- });
268
-
269
- /**
270
- * Get task status
271
- */
272
- router.get('/tasks/:taskId', (req, res) => {
273
- const task = runtime.scheduler.getTask(req.params.taskId);
274
- if (!task) return res.status(404).json({ error: 'Task not found' });
275
- res.json(task);
276
- });
277
-
278
- /**
279
- * List tasks
280
- */
281
- router.get('/tasks', (req, res) => {
282
- const tasks = runtime.scheduler.listTasks(req.query.state, parseInt(req.query.limit) || 50);
283
- res.json({ tasks, total: tasks.length });
284
- });
285
-
286
- /**
287
- * Cancel a task
288
- */
289
- router.delete('/tasks/:taskId', (req, res) => {
290
- const success = runtime.scheduler.cancel(req.params.taskId);
291
- res.json({ success });
292
- });
293
-
294
- /**
295
- * Pause a task
296
- */
297
- router.post('/tasks/:taskId/pause', (req, res) => {
298
- const success = runtime.scheduler.pause(req.params.taskId);
299
- res.json({ success });
300
- });
301
-
302
- /**
303
- * Resume a task
304
- */
305
- router.post('/tasks/:taskId/resume', (req, res) => {
306
- const success = runtime.scheduler.resume(req.params.taskId);
307
- res.json({ success });
308
- });
309
-
310
- // ═══════════════════════════════════════════════════════════════════════════
311
- // EXECUTION (DATA PLANE)
312
- // ═══════════════════════════════════════════════════════════════════════════
313
-
314
- /**
315
- * Execute a semantic action
316
- */
317
- router.post('/execute', usageLimit('executionsPerDay'), async (req, res) => {
318
- try {
319
- const result = await executor.execute(req.body);
320
- res.json(result);
321
- } catch (err) {
322
- res.status(500).json({ error: err.message });
323
- }
324
- });
325
-
326
- /**
327
- * Execute semantic action (domain.action style)
328
- */
329
- router.post('/execute/semantic', async (req, res) => {
330
- try {
331
- const { domain, action, params, siteId, agentId, siteDomain } = req.body;
332
- if (!domain || !action) return res.status(400).json({ error: 'domain and action required' });
333
-
334
- const result = await executor.execute({
335
- type: 'semantic',
336
- domain,
337
- action,
338
- params: params || {},
339
- siteId,
340
- agentId,
341
- siteDomain,
342
- });
343
- res.json(result);
344
- } catch (err) {
345
- res.status(500).json({ error: err.message });
346
- }
347
- });
348
-
349
- /**
350
- * Execute a pipeline
351
- */
352
- router.post('/execute/pipeline', async (req, res) => {
353
- try {
354
- const result = await executor.execute({ ...req.body, type: 'pipeline' });
355
- res.json(result);
356
- } catch (err) {
357
- res.status(500).json({ error: err.message });
358
- }
359
- });
360
-
361
- /**
362
- * Resolve a semantic action (without executing)
363
- */
364
- router.get('/execute/resolve', (req, res) => {
365
- const { domain, action, siteDomain } = req.query;
366
- if (!domain || !action) return res.status(400).json({ error: 'domain and action required' });
367
- const impl = executor.resolver.resolve(siteDomain || '*', `${domain}.${action}`);
368
- if (!impl) return res.status(404).json({ error: 'No implementation found' });
369
- res.json(impl);
370
- });
371
-
372
- // ═══════════════════════════════════════════════════════════════════════════
373
- // CONTROL PLANE
374
- // ═══════════════════════════════════════════════════════════════════════════
375
-
376
- /**
377
- * Deploy an agent
378
- */
379
- router.post('/deployments', (req, res) => {
380
- try {
381
- const { agentId, config } = req.body;
382
- if (!agentId) return res.status(400).json({ error: 'agentId required' });
383
- const deployment = agentManager.deploy(agentId, config || {});
384
- res.json(deployment);
385
- } catch (err) {
386
- res.status(400).json({ error: err.message });
387
- }
388
- });
389
-
390
- /**
391
- * List deployments
392
- */
393
- router.get('/deployments', (req, res) => {
394
- const deployments = agentManager.listDeployments({
395
- status: req.query.status,
396
- agentId: req.query.agentId,
397
- });
398
- res.json({ deployments, total: deployments.length });
399
- });
400
-
401
- /**
402
- * Create a policy
403
- */
404
- router.post('/policies', (req, res) => {
405
- try {
406
- const policy = policyEngine.createPolicy(req.body);
407
- res.json(policy);
408
- } catch (err) {
409
- res.status(400).json({ error: err.message });
410
- }
411
- });
412
-
413
- /**
414
- * Bind policy to entity
415
- */
416
- router.post('/policies/:policyId/bind', (req, res) => {
417
- const { entityId } = req.body;
418
- if (!entityId) return res.status(400).json({ error: 'entityId required' });
419
- policyEngine.bind(entityId, req.params.policyId);
420
- res.json({ success: true });
421
- });
422
-
423
- /**
424
- * Evaluate policies
425
- */
426
- router.post('/policies/evaluate', (req, res) => {
427
- const { entityId, action, context } = req.body;
428
- if (!entityId || !action) return res.status(400).json({ error: 'entityId and action required' });
429
- const result = policyEngine.evaluate(entityId, action, context || {});
430
- res.json(result);
431
- });
432
-
433
- /**
434
- * List policies
435
- */
436
- router.get('/policies', (req, res) => {
437
- const policies = policyEngine.listPolicies(req.query.entityId);
438
- res.json({ policies, total: policies.length });
439
- });
440
-
441
- // ═══════════════════════════════════════════════════════════════════════════
442
- // SITE ISOLATION
443
- // ═══════════════════════════════════════════════════════════════════════════
444
-
445
- /**
446
- * Configure site isolation
447
- */
448
- router.post('/isolation/:siteId', (req, res) => {
449
- isolation.configure(req.params.siteId, req.body);
450
- res.json({ success: true });
451
- });
452
-
453
- /**
454
- * Get site isolation config
455
- */
456
- router.get('/isolation/:siteId', (req, res) => {
457
- const config = isolation.getConfig(req.params.siteId);
458
- if (!config) return res.status(404).json({ error: 'No isolation config' });
459
- res.json(config);
460
- });
461
-
462
- // ═══════════════════════════════════════════════════════════════════════════
463
- // OBSERVABILITY
464
- // ═══════════════════════════════════════════════════════════════════════════
465
-
466
- /**
467
- * Get metrics snapshot
468
- */
469
- router.get('/observability/metrics', (req, res) => {
470
- res.json(metrics.snapshot());
471
- });
472
-
473
- /**
474
- * Get specific metric
475
- */
476
- router.get('/observability/metrics/:name', (req, res) => {
477
- const h = metrics.getHistogram(req.params.name);
478
- if (h) return res.json({ type: 'histogram', name: req.params.name, ...h });
479
-
480
- const c = metrics.getCounter(req.params.name);
481
- if (c) return res.json({ type: 'counter', name: req.params.name, value: c });
482
-
483
- const g = metrics.getGauge(req.params.name);
484
- if (g) return res.json({ type: 'gauge', name: req.params.name, value: g });
485
-
486
- res.status(404).json({ error: 'Metric not found' });
487
- });
488
-
489
- /**
490
- * List traces
491
- */
492
- router.get('/observability/traces', (req, res) => {
493
- const traces = tracer.listTraces(
494
- parseInt(req.query.limit) || 50,
495
- { status: req.query.status, name: req.query.name, since: parseInt(req.query.since) || undefined }
496
- );
497
- res.json({ traces, total: traces.length });
498
- });
499
-
500
- /**
501
- * Get trace details
502
- */
503
- router.get('/observability/traces/:traceId', (req, res) => {
504
- const trace = tracer.getTrace(req.params.traceId);
505
- if (!trace) return res.status(404).json({ error: 'Trace not found' });
506
- res.json(trace);
507
- });
508
-
509
- /**
510
- * Query logs
511
- */
512
- router.get('/observability/logs', (req, res) => {
513
- const logs = logger.query({
514
- level: req.query.level,
515
- traceId: req.query.traceId,
516
- agentId: req.query.agentId,
517
- since: parseInt(req.query.since) || undefined,
518
- message: req.query.message,
519
- }, parseInt(req.query.limit) || 100);
520
- res.json({ logs, total: logs.length });
521
- });
522
-
523
- /**
524
- * Runtime health
525
- */
526
- router.get('/observability/health', (req, res) => {
527
- const health = runtime.getHealth();
528
- health.identity = identity.getStats();
529
- health.registry = {
530
- commands: commandRegistry.getStats(),
531
- sites: siteRegistry.getStats(),
532
- templates: templateRegistry.getStats(),
533
- };
534
- health.executor = executor.getStats();
535
- health.llm = llm.getStatus();
536
- health.adapters = adapterManager.getStats();
537
- health.replay = replayEngine.getStats();
538
- health.sessions = sessionEngine.getStats();
539
- health.failures = failureAnalyzer.getStats();
540
- health.certification = certificationEngine.getStats();
541
- health.marketplace = marketplace.getStats();
542
- health.hostedRuntime = hostedRuntime.getStats();
543
- health.metering = metering.getStats();
544
- health.lfd = lfdEngine.getStats();
545
- health.cluster = cluster.getClusterStatus();
546
- res.json(health);
547
- });
548
-
549
- // ═══════════════════════════════════════════════════════════════════════════
550
- // REGISTRY
551
- // ═══════════════════════════════════════════════════════════════════════════
552
-
553
- /**
554
- * Register a command
555
- */
556
- router.post('/registry/commands', (req, res) => {
557
- try {
558
- const { siteId, ...command } = req.body;
559
- if (!siteId) return res.status(400).json({ error: 'siteId required' });
560
- const entry = commandRegistry.register(siteId, command);
561
- res.json(entry);
562
- } catch (err) {
563
- res.status(400).json({ error: err.message });
564
- }
565
- });
566
-
567
- /**
568
- * Search commands
569
- */
570
- router.get('/registry/commands', (req, res) => {
571
- const results = commandRegistry.search({
572
- siteId: req.query.siteId,
573
- category: req.query.category,
574
- name: req.query.name,
575
- tag: req.query.tag,
576
- capability: req.query.capability,
577
- limit: parseInt(req.query.limit) || 50,
578
- });
579
- res.json({ commands: results, total: results.length });
580
- });
581
-
582
- /**
583
- * Register a site
584
- */
585
- router.post('/registry/sites', (req, res) => {
586
- const { domain, ...info } = req.body;
587
- if (!domain) return res.status(400).json({ error: 'domain required' });
588
- const entry = siteRegistry.register(domain, info);
589
- res.json(entry);
590
- });
591
-
592
- /**
593
- * Search sites
594
- */
595
- router.get('/registry/sites', (req, res) => {
596
- const results = siteRegistry.search({
597
- tier: req.query.tier,
598
- capability: req.query.capability,
599
- name: req.query.name,
600
- verified: req.query.verified === 'true' ? true : undefined,
601
- limit: parseInt(req.query.limit) || 50,
602
- });
603
- res.json({ sites: results, total: results.length });
604
- });
605
-
606
- /**
607
- * Get site info
608
- */
609
- router.get('/registry/sites/:domain', (req, res) => {
610
- const site = siteRegistry.getSite(req.params.domain);
611
- if (!site) return res.status(404).json({ error: 'Site not found' });
612
- res.json(site);
613
- });
614
-
615
- /**
616
- * Register a template
617
- */
618
- router.post('/registry/templates', (req, res) => {
619
- try {
620
- const entry = templateRegistry.register(req.body);
621
- res.json(entry);
622
- } catch (err) {
623
- res.status(400).json({ error: err.message });
624
- }
625
- });
626
-
627
- /**
628
- * Search templates
629
- */
630
- router.get('/registry/templates', (req, res) => {
631
- const results = templateRegistry.search({
632
- category: req.query.category,
633
- name: req.query.name,
634
- tag: req.query.tag,
635
- limit: parseInt(req.query.limit) || 50,
636
- });
637
- res.json({ templates: results, total: results.length });
638
- });
639
-
640
- /**
641
- * Get template
642
- */
643
- router.get('/registry/templates/:templateId', (req, res) => {
644
- const tmpl = templateRegistry.getTemplate(req.params.templateId);
645
- if (!tmpl) return res.status(404).json({ error: 'Template not found' });
646
- templateRegistry.trackDownload(req.params.templateId);
647
- res.json(tmpl);
648
- });
649
-
650
- // ═══════════════════════════════════════════════════════════════════════════
651
- // LLM
652
- // ═══════════════════════════════════════════════════════════════════════════
653
-
654
- /**
655
- * LLM completion
656
- */
657
- router.post('/llm/complete', usageLimit('executionsPerDay'), async (req, res) => {
658
- try {
659
- const result = await llm.complete(req.body.prompt, req.body.options || req.body);
660
- metrics.increment('llm.api.requests');
661
- res.json(result);
662
- } catch (err) {
663
- res.status(500).json({ error: err.message });
664
- }
665
- });
666
-
667
- /**
668
- * LLM models
669
- */
670
- router.get('/llm/models', (req, res) => {
671
- res.json({ models: llm.listModels() });
672
- });
673
-
674
- /**
675
- * LLM status
676
- */
677
- router.get('/llm/status', (req, res) => {
678
- res.json(llm.getStatus());
679
- });
680
-
681
- /**
682
- * LLM embeddings
683
- */
684
- router.post('/llm/embed', async (req, res) => {
685
- try {
686
- const result = await llm.embed(req.body.text, req.body.options || {});
687
- res.json(result);
688
- } catch (err) {
689
- res.status(500).json({ error: err.message });
690
- }
691
- });
692
-
693
- // ═══════════════════════════════════════════════════════════════════════════
694
- // COMMAND SIGNING
695
- // ═══════════════════════════════════════════════════════════════════════════
696
-
697
- /**
698
- * Sign a command
699
- */
700
- router.post('/sign', (req, res) => {
701
- const { payload, agentId } = req.body;
702
- if (!payload || !agentId) return res.status(400).json({ error: 'payload and agentId required' });
703
- const signature = signer.sign(payload, agentId);
704
- res.json(signature);
705
- });
706
-
707
- /**
708
- * Verify a signed command
709
- */
710
- router.post('/verify', (req, res) => {
711
- const { payload, agentId, nonce, timestamp, signature } = req.body;
712
- const result = signer.verify(payload, agentId, nonce, timestamp, signature);
713
- res.json(result);
714
- });
715
-
716
- // ═══════════════════════════════════════════════════════════════════════════
717
- // EVENT STREAM (SSE)
718
- // ═══════════════════════════════════════════════════════════════════════════
719
-
720
- /**
721
- * Server-Sent Events for real-time updates
722
- */
723
- router.get('/events', (req, res) => {
724
- res.writeHead(200, {
725
- 'Content-Type': 'text/event-stream',
726
- 'Cache-Control': 'no-cache',
727
- 'Connection': 'keep-alive',
728
- });
729
-
730
- const filter = req.query.filter; // e.g., 'task.*' or 'agent.*'
731
-
732
- const subId = bus.on(filter || '*', (data, meta) => {
733
- res.write(`event: ${meta.event || 'message'}\n`);
734
- res.write(`data: ${JSON.stringify(data)}\n\n`);
735
- });
736
-
737
- req.on('close', () => {
738
- bus.off(subId);
739
- res.end();
740
- });
741
- });
742
-
743
- // ═══════════════════════════════════════════════════════════════════════════
744
- // Protocol Handler Setup
745
- // ═══════════════════════════════════════════════════════════════════════════
746
-
747
- const protocolHandler = new protocol.ProtocolHandler();
748
-
749
- // Wire protocol commands to runtime
750
- protocolHandler.handle('wab.discover', async (payload) => {
751
- const commands = commandRegistry.search({ siteId: payload.siteId, category: payload.category });
752
- return {
753
- actions: commands.map(c => ({
754
- name: c.name,
755
- category: c.category,
756
- params: c.input,
757
- capabilities: c.capabilities,
758
- })),
759
- meta: {
760
- protocol: protocol.PROTOCOL_VERSION,
761
- timestamp: Date.now(),
762
- },
763
- };
764
- });
765
-
766
- protocolHandler.handle('wab.execute', async (payload, ctx) => {
767
- const result = await executor.execute({
768
- type: 'semantic',
769
- domain: payload.domain || 'general',
770
- action: payload.action,
771
- params: payload.params,
772
- agentId: ctx.message.agentId,
773
- });
774
- return result;
775
- });
776
-
777
- protocolHandler.handle('wab.task.submit', async (payload) => {
778
- return runtime.submitTask(payload);
779
- });
780
-
781
- protocolHandler.handle('wab.task.status', async (payload) => {
782
- return runtime.scheduler.getTask(payload.taskId);
783
- });
784
-
785
- protocolHandler.handle('wab.agent.register', async (payload) => {
786
- const result = identity.register(payload.name, payload.type, {
787
- capabilities: payload.capabilities,
788
- publicKey: payload.publicKey,
789
- metadata: payload.metadata,
790
- });
791
-
792
- // Negotiate requested capabilities
793
- const negotiation = protocol.negotiator.negotiate(
794
- result.agentId,
795
- payload.capabilities,
796
- payload.siteId || '*'
797
- );
798
-
799
- return {
800
- agentId: result.agentId,
801
- token: result.apiKey,
802
- grantedCapabilities: negotiation.granted,
803
- expiresAt: negotiation.grant?.constraints?.expiresAt || Date.now() + 3600_000,
804
- };
805
- });
806
-
807
- protocolHandler.handle('wab.ai.infer', async (payload) => {
808
- return llm.complete(payload.prompt, {
809
- model: payload.model,
810
- provider: payload.provider,
811
- ...payload.options,
812
- });
813
- });
814
-
815
- protocolHandler.handle('wab.commerce.compare', async (payload) => {
816
- return executor.execute({
817
- type: 'parallel',
818
- tasks: (payload.sources || []).map(url => ({
819
- type: 'extraction',
820
- params: { url, query: payload.query },
821
- })),
822
- });
823
- });
824
-
825
- // ═══════════════════════════════════════════════════════════════════════════
826
- // ADAPTERS
827
- // ═══════════════════════════════════════════════════════════════════════════
828
-
829
- /**
830
- * List adapters
831
- */
832
- router.get('/adapters', (req, res) => {
833
- res.json({ adapters: adapterManager.list() });
834
- });
835
-
836
- /**
837
- * Adapter stats
838
- */
839
- router.get('/adapters/stats', (req, res) => {
840
- res.json(adapterManager.getStats());
841
- });
842
-
843
- /**
844
- * MCP: list tools
845
- */
846
- router.get('/adapters/mcp/tools', (req, res) => {
847
- const commands = protocol.schema.listCommands();
848
- res.json(mcpAdapter.handleListTools(commands));
849
- });
850
-
851
- /**
852
- * MCP: call tool
853
- */
854
- router.post('/adapters/mcp/call', async (req, res) => {
855
- try {
856
- const result = await mcpAdapter.handleCallTool(req.body, async (wapReq) => {
857
- const request = protocol.createRequest(wapReq.command, wapReq.payload);
858
- return protocolHandler.process(request);
859
- });
860
- res.json(result);
861
- } catch (err) {
862
- res.status(500).json({ error: err.message });
863
- }
864
- });
865
-
866
- /**
867
- * REST adapter: register endpoint
868
- */
869
- router.post('/adapters/rest/endpoints', (req, res) => {
870
- try {
871
- const endpoint = restAdapter.registerEndpoint(req.body.id, req.body);
872
- res.json(endpoint);
873
- } catch (err) {
874
- res.status(400).json({ error: err.message });
875
- }
876
- });
877
-
878
- /**
879
- * REST adapter: list endpoints
880
- */
881
- router.get('/adapters/rest/endpoints', (req, res) => {
882
- res.json({ endpoints: restAdapter.listEndpoints() });
883
- });
884
-
885
- /**
886
- * REST adapter: execute
887
- */
888
- router.post('/adapters/rest/execute', async (req, res) => {
889
- try {
890
- const result = await restAdapter.execute(req.body.endpoint, req.body.params);
891
- res.json(result);
892
- } catch (err) {
893
- res.status(500).json({ error: err.message });
894
- }
895
- });
896
-
897
- /**
898
- * Browser adapter: list semantic mappings
899
- */
900
- router.get('/adapters/browser/mappings', (req, res) => {
901
- res.json({ mappings: browserAdapter.listMappings() });
902
- });
903
-
904
- /**
905
- * Browser adapter: resolve semantic action
906
- */
907
- router.post('/adapters/browser/resolve', (req, res) => {
908
- const { domain, action, params } = req.body;
909
- const plan = browserAdapter.fromWAP({ domain, action, params });
910
- if (!plan) return res.status(404).json({ error: 'No mapping for this semantic action' });
911
- res.json(plan);
912
- });
913
-
914
- /**
915
- * Browser adapter: register mapping
916
- */
917
- router.post('/adapters/browser/mappings', (req, res) => {
918
- const { domainAction, plan } = req.body;
919
- if (!domainAction || !plan) return res.status(400).json({ error: 'domainAction and plan required' });
920
- browserAdapter.registerMapping(domainAction, plan);
921
- res.json({ success: true });
922
- });
923
-
924
- // ═══════════════════════════════════════════════════════════════════════════
925
- // REPLAY ENGINE
926
- // ═══════════════════════════════════════════════════════════════════════════
927
-
928
- /**
929
- * List recordings
930
- */
931
- router.get('/replay/recordings', (req, res) => {
932
- res.json({ recordings: replayEngine.listRecordings(parseInt(req.query.limit) || 50) });
933
- });
934
-
935
- /**
936
- * Get recording
937
- */
938
- router.get('/replay/recordings/:taskId', (req, res) => {
939
- const rec = replayEngine.getRecording(req.params.taskId);
940
- if (!rec) return res.status(404).json({ error: 'Recording not found' });
941
- res.json(rec);
942
- });
943
-
944
- /**
945
- * Replay a task
946
- */
947
- router.post('/replay/:taskId', async (req, res) => {
948
- try {
949
- const result = await replayEngine.replay(req.params.taskId, {
950
- verify: req.body.verify !== false,
951
- continueOnMismatch: !!req.body.continueOnMismatch,
952
- });
953
- res.json(result);
954
- } catch (err) {
955
- res.status(400).json({ error: err.message });
956
- }
957
- });
958
-
959
- /**
960
- * Diff two recordings
961
- */
962
- router.get('/replay/diff/:taskId1/:taskId2', (req, res) => {
963
- const diff = replayEngine.diff(req.params.taskId1, req.params.taskId2);
964
- if (!diff) return res.status(404).json({ error: 'One or both recordings not found' });
965
- res.json(diff);
966
- });
967
-
968
- /**
969
- * Replay stats
970
- */
971
- router.get('/replay/stats', (req, res) => {
972
- res.json(replayEngine.getStats());
973
- });
974
-
975
- // ═══════════════════════════════════════════════════════════════════════════
976
- // SESSION ENGINE
977
- // ═══════════════════════════════════════════════════════════════════════════
978
-
979
- /**
980
- * Create browser session
981
- */
982
- router.post('/sessions', (req, res) => {
983
- const session = sessionEngine.create(req.body);
984
- res.json(session);
985
- });
986
-
987
- /**
988
- * List sessions
989
- */
990
- router.get('/sessions', (req, res) => {
991
- const sessions = sessionEngine.list({
992
- agentId: req.query.agentId,
993
- siteId: req.query.siteId,
994
- state: req.query.state,
995
- }, parseInt(req.query.limit) || 50);
996
- res.json({ sessions, total: sessions.length });
997
- });
998
-
999
- /**
1000
- * Get session
1001
- */
1002
- router.get('/sessions/:sessionId', (req, res) => {
1003
- const session = sessionEngine.get(req.params.sessionId);
1004
- if (!session) return res.status(404).json({ error: 'Session not found or expired' });
1005
- res.json(session);
1006
- });
1007
-
1008
- /**
1009
- * Export session
1010
- */
1011
- router.get('/sessions/:sessionId/export', (req, res) => {
1012
- const data = sessionEngine.export(req.params.sessionId);
1013
- if (!data) return res.status(404).json({ error: 'Session not found' });
1014
- res.json(data);
1015
- });
1016
-
1017
- /**
1018
- * Import session
1019
- */
1020
- router.post('/sessions/import', (req, res) => {
1021
- const session = sessionEngine.import(req.body);
1022
- res.json(session);
1023
- });
1024
-
1025
- /**
1026
- * Set cookies
1027
- */
1028
- router.post('/sessions/:sessionId/cookies', (req, res) => {
1029
- sessionEngine.setCookies(req.params.sessionId, req.body.cookies || []);
1030
- res.json({ success: true });
1031
- });
1032
-
1033
- /**
1034
- * Get cookies
1035
- */
1036
- router.get('/sessions/:sessionId/cookies', (req, res) => {
1037
- const cookies = sessionEngine.getCookies(req.params.sessionId, req.query.domain);
1038
- res.json({ cookies });
1039
- });
1040
-
1041
- /**
1042
- * Set storage
1043
- */
1044
- router.post('/sessions/:sessionId/storage', (req, res) => {
1045
- const { key, value, type } = req.body;
1046
- sessionEngine.setStorage(req.params.sessionId, key, value, type);
1047
- res.json({ success: true });
1048
- });
1049
-
1050
- /**
1051
- * Destroy session
1052
- */
1053
- router.delete('/sessions/:sessionId', (req, res) => {
1054
- sessionEngine.destroy(req.params.sessionId);
1055
- res.json({ success: true });
1056
- });
1057
-
1058
- // ═══════════════════════════════════════════════════════════════════════════
1059
- // FAILURE ANALYSIS
1060
- // ═══════════════════════════════════════════════════════════════════════════
1061
-
1062
- /**
1063
- * Query failures
1064
- */
1065
- router.get('/failures', (req, res) => {
1066
- const failures = failureAnalyzer.query({
1067
- classification: req.query.classification,
1068
- severity: req.query.severity,
1069
- agentId: req.query.agentId,
1070
- taskId: req.query.taskId,
1071
- retryable: req.query.retryable === 'true' ? true : req.query.retryable === 'false' ? false : undefined,
1072
- since: parseInt(req.query.since) || undefined,
1073
- }, parseInt(req.query.limit) || 50);
1074
- res.json({ failures, total: failures.length });
1075
- });
1076
-
1077
- /**
1078
- * Get failure
1079
- */
1080
- router.get('/failures/:failureId', (req, res) => {
1081
- const failure = failureAnalyzer.getFailure(req.params.failureId);
1082
- if (!failure) return res.status(404).json({ error: 'Failure not found' });
1083
- res.json(failure);
1084
- });
1085
-
1086
- /**
1087
- * Get failure patterns
1088
- */
1089
- router.get('/failures/analysis/patterns', (req, res) => {
1090
- res.json({ patterns: failureAnalyzer.getPatterns() });
1091
- });
1092
-
1093
- /**
1094
- * Get failure summary
1095
- */
1096
- router.get('/failures/analysis/summary', (req, res) => {
1097
- res.json(failureAnalyzer.getSummary(parseInt(req.query.since) || 0));
1098
- });
1099
-
1100
- /**
1101
- * Classify a failure manually
1102
- */
1103
- router.post('/failures/classify', (req, res) => {
1104
- const { error, context } = req.body;
1105
- if (!error) return res.status(400).json({ error: 'error object required' });
1106
- const classification = failureAnalyzer.classify(error, context || {});
1107
- res.json(classification);
1108
- });
1109
-
1110
- // ═══════════════════════════════════════════════════════════════════════════
1111
- // CERTIFICATION
1112
- // ═══════════════════════════════════════════════════════════════════════════
1113
-
1114
- /**
1115
- * Verify a site
1116
- */
1117
- router.post('/certification/verify', async (req, res) => {
1118
- try {
1119
- const { domain, probeData } = req.body;
1120
- if (!domain) return res.status(400).json({ error: 'domain required' });
1121
- const result = await certificationEngine.verify(domain, probeData || {});
1122
- res.json(result);
1123
- } catch (err) {
1124
- res.status(500).json({ error: err.message });
1125
- }
1126
- });
1127
-
1128
- /**
1129
- * Get certificate
1130
- */
1131
- router.get('/certification/:domain', (req, res) => {
1132
- const cert = certificationEngine.getCertificate(req.params.domain);
1133
- if (!cert) return res.status(404).json({ error: 'No active certificate for this domain' });
1134
- res.json(cert);
1135
- });
1136
-
1137
- /**
1138
- * List certificates
1139
- */
1140
- router.get('/certification', (req, res) => {
1141
- const certs = certificationEngine.listCertificates({
1142
- level: req.query.level,
1143
- minScore: parseInt(req.query.minScore) || undefined,
1144
- }, parseInt(req.query.limit) || 50);
1145
- res.json({ certificates: certs, total: certs.length });
1146
- });
1147
-
1148
- /**
1149
- * Revoke certificate
1150
- */
1151
- router.delete('/certification/:domain', (req, res) => {
1152
- certificationEngine.revoke(req.params.domain);
1153
- res.json({ success: true });
1154
- });
1155
-
1156
- // ═══════════════════════════════════════════════════════════════════════════
1157
- // PLANS & PRICING
1158
- // ═══════════════════════════════════════════════════════════════════════════
1159
-
1160
- /**
1161
- * List available plans
1162
- */
1163
- router.get('/plans', (req, res) => {
1164
- const plans = listPlans().map(p => ({
1165
- id: p.id,
1166
- name: p.name,
1167
- price: p.price,
1168
- interval: p.interval,
1169
- description: p.description,
1170
- limits: p.limits,
1171
- features: Object.entries(p.features)
1172
- .filter(([, v]) => v === true)
1173
- .map(([k]) => k),
1174
- }));
1175
- res.json({ plans, usagePricing: USAGE_PRICING });
1176
- });
1177
-
1178
- /**
1179
- * Get specific plan details
1180
- */
1181
- router.get('/plans/:planId', (req, res) => {
1182
- const plan = getPlan(req.params.planId);
1183
- if (!plan || plan.id === 'free' && req.params.planId !== 'free') {
1184
- return res.status(404).json({ error: 'Plan not found' });
1185
- }
1186
- res.json(plan);
1187
- });
1188
-
1189
- // ═══════════════════════════════════════════════════════════════════════════
1190
- // USAGE METERING
1191
- // ═══════════════════════════════════════════════════════════════════════════
1192
-
1193
- /**
1194
- * Get usage for current agent
1195
- */
1196
- router.get('/usage', (req, res) => {
1197
- const entityId = req.agentId || req.ip;
1198
- const tier = req.agentTier || req.session?.tier || 'free';
1199
- res.json(metering.getUsage(entityId, tier));
1200
- });
1201
-
1202
- /**
1203
- * Get billing summary (overages)
1204
- */
1205
- router.get('/usage/billing', (req, res) => {
1206
- const entityId = req.agentId || req.ip;
1207
- res.json(metering.getBillingSummary(entityId));
1208
- });
1209
-
1210
- /**
1211
- * Get metering stats (admin)
1212
- */
1213
- router.get('/usage/stats', (req, res) => {
1214
- res.json(metering.getStats());
1215
- });
1216
-
1217
- // ═══════════════════════════════════════════════════════════════════════════
1218
- // MARKETPLACE
1219
- // ═══════════════════════════════════════════════════════════════════════════
1220
-
1221
- /**
1222
- * Search marketplace
1223
- */
1224
- router.get('/marketplace', (req, res) => {
1225
- const listings = marketplace.search({
1226
- type: req.query.type,
1227
- category: req.query.category,
1228
- query: req.query.q,
1229
- tag: req.query.tag,
1230
- free: req.query.free === 'true',
1231
- paid: req.query.paid === 'true',
1232
- minRating: req.query.minRating ? parseFloat(req.query.minRating) : undefined,
1233
- sortBy: req.query.sortBy,
1234
- }, parseInt(req.query.limit) || 50);
1235
- res.json({ listings, total: listings.length });
1236
- });
1237
-
1238
- /**
1239
- * Get listing
1240
- */
1241
- router.get('/marketplace/:listingId', (req, res) => {
1242
- const listing = marketplace.getListing(req.params.listingId);
1243
- if (!listing) return res.status(404).json({ error: 'Listing not found' });
1244
- res.json(listing);
1245
- });
1246
-
1247
- /**
1248
- * Get reviews
1249
- */
1250
- router.get('/marketplace/:listingId/reviews', (req, res) => {
1251
- res.json({ reviews: marketplace.getReviews(req.params.listingId) });
1252
- });
1253
-
1254
- /**
1255
- * Publish listing
1256
- */
1257
- router.post('/marketplace/publish', (req, res) => {
1258
- try {
1259
- const listing = marketplace.publish({
1260
- ...req.body,
1261
- sellerId: req.agentId || req.body.sellerId,
1262
- });
1263
- res.json(listing);
1264
- } catch (err) {
1265
- res.status(400).json({ error: err.message });
1266
- }
1267
- });
1268
-
1269
- /**
1270
- * Purchase/install listing
1271
- */
1272
- router.post('/marketplace/:listingId/purchase', (req, res) => {
1273
- try {
1274
- const buyerId = req.agentId || req.body.buyerId;
1275
- if (!buyerId) return res.status(400).json({ error: 'buyerId required' });
1276
- const purchase = marketplace.purchase(req.params.listingId, buyerId);
1277
- res.json(purchase);
1278
- } catch (err) {
1279
- res.status(400).json({ error: err.message });
1280
- }
1281
- });
1282
-
1283
- /**
1284
- * Add review
1285
- */
1286
- router.post('/marketplace/:listingId/review', (req, res) => {
1287
- try {
1288
- const review = marketplace.addReview(req.params.listingId, {
1289
- userId: req.agentId || req.body.userId,
1290
- rating: req.body.rating,
1291
- comment: req.body.comment,
1292
- });
1293
- res.json(review);
1294
- } catch (err) {
1295
- res.status(400).json({ error: err.message });
1296
- }
1297
- });
1298
-
1299
- /**
1300
- * Get my purchases
1301
- */
1302
- router.get('/marketplace/my/purchases', (req, res) => {
1303
- const buyerId = req.agentId || req.query.buyerId;
1304
- res.json({ purchases: marketplace.getPurchases(buyerId) });
1305
- });
1306
-
1307
- /**
1308
- * Get seller earnings
1309
- */
1310
- router.get('/marketplace/my/earnings', (req, res) => {
1311
- const sellerId = req.agentId || req.query.sellerId;
1312
- res.json(marketplace.getEarnings(sellerId));
1313
- });
1314
-
1315
- /**
1316
- * Admin: pending listings
1317
- */
1318
- router.get('/marketplace/admin/pending', (req, res) => {
1319
- res.json({ listings: marketplace.getPendingListings() });
1320
- });
1321
-
1322
- /**
1323
- * Admin: approve listing
1324
- */
1325
- router.post('/marketplace/admin/:listingId/approve', (req, res) => {
1326
- try {
1327
- const listing = marketplace.approve(req.params.listingId);
1328
- res.json(listing);
1329
- } catch (err) {
1330
- res.status(400).json({ error: err.message });
1331
- }
1332
- });
1333
-
1334
- /**
1335
- * Admin: reject listing
1336
- */
1337
- router.post('/marketplace/admin/:listingId/reject', (req, res) => {
1338
- try {
1339
- const listing = marketplace.reject(req.params.listingId, req.body.reason);
1340
- res.json(listing);
1341
- } catch (err) {
1342
- res.status(400).json({ error: err.message });
1343
- }
1344
- });
1345
-
1346
- /**
1347
- * Marketplace stats
1348
- */
1349
- router.get('/marketplace/stats', (req, res) => {
1350
- res.json(marketplace.getStats());
1351
- });
1352
-
1353
- // ═══════════════════════════════════════════════════════════════════════════
1354
- // HOSTED RUNTIME
1355
- // ═══════════════════════════════════════════════════════════════════════════
1356
-
1357
- /**
1358
- * Launch hosted instance
1359
- */
1360
- router.post('/hosted/launch', (req, res) => {
1361
- try {
1362
- const instance = hostedRuntime.launch({
1363
- agentId: req.agentId || req.body.agentId,
1364
- tier: req.agentTier || req.session?.tier || 'starter',
1365
- region: req.body.region,
1366
- cpu: req.body.cpu,
1367
- memory: req.body.memory,
1368
- timeout: req.body.timeout,
1369
- });
1370
- res.json(instance);
1371
- } catch (err) {
1372
- res.status(400).json({ error: err.message });
1373
- }
1374
- });
1375
-
1376
- /**
1377
- * Execute on hosted instance
1378
- */
1379
- router.post('/hosted/:instanceId/execute', async (req, res) => {
1380
- try {
1381
- const execution = await hostedRuntime.execute(req.params.instanceId, req.body);
1382
- res.json(execution);
1383
- } catch (err) {
1384
- res.status(400).json({ error: err.message });
1385
- }
1386
- });
1387
-
1388
- /**
1389
- * Complete execution
1390
- */
1391
- router.post('/hosted/executions/:executionId/complete', (req, res) => {
1392
- const execution = hostedRuntime.completeExecution(
1393
- req.params.executionId,
1394
- req.body.result,
1395
- req.body.error ? new Error(req.body.error) : null
1396
- );
1397
- if (!execution) return res.status(404).json({ error: 'Execution not found' });
1398
- res.json(execution);
1399
- });
1400
-
1401
- /**
1402
- * Stop hosted instance
1403
- */
1404
- router.post('/hosted/:instanceId/stop', (req, res) => {
1405
- const success = hostedRuntime.stop(req.params.instanceId);
1406
- res.json({ success });
1407
- });
1408
-
1409
- /**
1410
- * Get hosted instance
1411
- */
1412
- router.get('/hosted/:instanceId', (req, res) => {
1413
- const instance = hostedRuntime.getInstance(req.params.instanceId);
1414
- if (!instance) return res.status(404).json({ error: 'Instance not found' });
1415
- res.json(instance);
1416
- });
1417
-
1418
- /**
1419
- * List instances
1420
- */
1421
- router.get('/hosted', (req, res) => {
1422
- const instances = hostedRuntime.listInstances({
1423
- agentId: req.query.agentId,
1424
- status: req.query.status,
1425
- region: req.query.region,
1426
- }, parseInt(req.query.limit) || 50);
1427
- res.json({ instances, total: instances.length });
1428
- });
1429
-
1430
- /**
1431
- * List executions for instance
1432
- */
1433
- router.get('/hosted/:instanceId/executions', (req, res) => {
1434
- const executions = hostedRuntime.listExecutions(
1435
- req.params.instanceId,
1436
- parseInt(req.query.limit) || 50
1437
- );
1438
- res.json({ executions, total: executions.length });
1439
- });
1440
-
1441
- /**
1442
- * Get compute usage
1443
- */
1444
- router.get('/hosted/usage/:agentId', (req, res) => {
1445
- res.json(hostedRuntime.getComputeUsage(req.params.agentId));
1446
- });
1447
-
1448
- /**
1449
- * Hosted runtime stats
1450
- */
1451
- router.get('/hosted/stats', (req, res) => {
1452
- res.json(hostedRuntime.getStats());
1453
- });
1454
-
1455
- // ═══════════════════════════════════════════════════════════════════════════
1456
- // LOCAL VISION ENGINE (Self-contained — no external API)
1457
- // ═══════════════════════════════════════════════════════════════════════════
1458
-
1459
- /**
1460
- * Analyze page DOM locally (no external API calls)
1461
- */
1462
- router.post('/vision/analyze-dom', async (req, res) => {
1463
- try {
1464
- const siteId = req.body.siteId || req.agentId || 'default';
1465
- const result = await vision.analyzePageDOM(siteId, {
1466
- domSnapshot: req.body.domSnapshot,
1467
- url: req.body.url,
1468
- });
1469
- res.json(result);
1470
- } catch (err) {
1471
- res.status(400).json({ error: err.message });
1472
- }
1473
- });
1474
-
1475
- /**
1476
- * Get DOM extraction script to inject into pages
1477
- */
1478
- router.get('/vision/extraction-script', (req, res) => {
1479
- res.json({ script: vision.getDomExtractionScript() });
1480
- });
1481
-
1482
- /**
1483
- * Find elements in cached vision data
1484
- */
1485
- router.get('/vision/elements', (req, res) => {
1486
- const siteId = req.query.siteId || req.agentId || 'default';
1487
- const results = vision.findElement(siteId, req.query.url, {
1488
- description: req.query.q,
1489
- type: req.query.type,
1490
- label: req.query.label,
1491
- });
1492
- res.json({ elements: results, total: results.length });
1493
- });
1494
-
1495
- /**
1496
- * Vision history
1497
- */
1498
- router.get('/vision/history', (req, res) => {
1499
- const siteId = req.query.siteId || req.agentId || 'default';
1500
- const history = vision.getVisionHistory(siteId, {
1501
- limit: parseInt(req.query.limit) || 50,
1502
- url: req.query.url,
1503
- });
1504
- res.json({ history, total: history.length });
1505
- });
1506
-
1507
- /**
1508
- * Supported vision models
1509
- */
1510
- router.get('/vision/models', (req, res) => {
1511
- res.json({ models: vision.getSupportedModels() });
1512
- });
1513
-
1514
- // ═══════════════════════════════════════════════════════════════════════════
1515
- // LEARNING FROM DEMONSTRATION (LfD)
1516
- // ═══════════════════════════════════════════════════════════════════════════
1517
-
1518
- /**
1519
- * Start a recording session
1520
- */
1521
- router.post('/lfd/record', (req, res) => {
1522
- try {
1523
- const session = lfdEngine.startRecording({
1524
- name: req.body.name,
1525
- description: req.body.description,
1526
- agentId: req.agentId || req.body.agentId,
1527
- startUrl: req.body.startUrl,
1528
- tags: req.body.tags,
1529
- });
1530
- res.json(session);
1531
- } catch (err) {
1532
- res.status(400).json({ error: err.message });
1533
- }
1534
- });
1535
-
1536
- /**
1537
- * Record events into a session
1538
- */
1539
- router.post('/lfd/:sessionId/events', (req, res) => {
1540
- try {
1541
- const events = req.body.events || [req.body];
1542
- const results = events.map(evt => lfdEngine.recordEvent(req.params.sessionId, evt));
1543
- res.json({ recorded: results.filter(Boolean).length });
1544
- } catch (err) {
1545
- res.status(400).json({ error: err.message });
1546
- }
1547
- });
1548
-
1549
- /**
1550
- * Record a DOM snapshot
1551
- */
1552
- router.post('/lfd/:sessionId/snapshot', (req, res) => {
1553
- try {
1554
- lfdEngine.recordSnapshot(req.params.sessionId, req.body);
1555
- res.json({ success: true });
1556
- } catch (err) {
1557
- res.status(400).json({ error: err.message });
1558
- }
1559
- });
1560
-
1561
- /**
1562
- * Pause recording
1563
- */
1564
- router.post('/lfd/:sessionId/pause', (req, res) => {
1565
- try { res.json(lfdEngine.pauseRecording(req.params.sessionId)); }
1566
- catch (err) { res.status(400).json({ error: err.message }); }
1567
- });
1568
-
1569
- /**
1570
- * Resume recording
1571
- */
1572
- router.post('/lfd/:sessionId/resume', (req, res) => {
1573
- try { res.json(lfdEngine.resumeRecording(req.params.sessionId)); }
1574
- catch (err) { res.status(400).json({ error: err.message }); }
1575
- });
1576
-
1577
- /**
1578
- * Stop recording and generate recipe
1579
- */
1580
- router.post('/lfd/:sessionId/stop', (req, res) => {
1581
- try { res.json(lfdEngine.stopRecording(req.params.sessionId)); }
1582
- catch (err) { res.status(400).json({ error: err.message }); }
1583
- });
1584
-
1585
- /**
1586
- * Cancel recording
1587
- */
1588
- router.post('/lfd/:sessionId/cancel', (req, res) => {
1589
- try { res.json(lfdEngine.cancelRecording(req.params.sessionId)); }
1590
- catch (err) { res.status(400).json({ error: err.message }); }
1591
- });
1592
-
1593
- /**
1594
- * Get recording details
1595
- */
1596
- router.get('/lfd/:sessionId', (req, res) => {
1597
- const recording = lfdEngine.getRecording(req.params.sessionId);
1598
- if (!recording) return res.status(404).json({ error: 'Recording not found' });
1599
- res.json(recording);
1600
- });
1601
-
1602
- /**
1603
- * List recordings
1604
- */
1605
- router.get('/lfd', (req, res) => {
1606
- res.json({ recordings: lfdEngine.listRecordings(parseInt(req.query.limit) || 50) });
1607
- });
1608
-
1609
- /**
1610
- * Get recording script to inject into pages
1611
- */
1612
- router.get('/lfd/:sessionId/script', (req, res) => {
1613
- const serverUrl = `${req.protocol}://${req.get('host')}`;
1614
- res.json({ script: lfdEngine.getRecordingScript(req.params.sessionId, serverUrl) });
1615
- });
1616
-
1617
- // ── Recipes ──
1618
-
1619
- /**
1620
- * List recipes
1621
- */
1622
- router.get('/recipes', (req, res) => {
1623
- const recipes = lfdEngine.listRecipes({
1624
- domain: req.query.domain,
1625
- tag: req.query.tag,
1626
- query: req.query.q,
1627
- }, parseInt(req.query.limit) || 50);
1628
- res.json({ recipes, total: recipes.length });
1629
- });
1630
-
1631
- /**
1632
- * Get recipe
1633
- */
1634
- router.get('/recipes/:recipeId', (req, res) => {
1635
- const recipe = lfdEngine.getRecipe(req.params.recipeId);
1636
- if (!recipe) return res.status(404).json({ error: 'Recipe not found' });
1637
- res.json(recipe);
1638
- });
1639
-
1640
- /**
1641
- * Save/import recipe manually
1642
- */
1643
- router.post('/recipes', (req, res) => {
1644
- try { res.json(lfdEngine.saveRecipe(req.body)); }
1645
- catch (err) { res.status(400).json({ error: err.message }); }
1646
- });
1647
-
1648
- /**
1649
- * Delete recipe
1650
- */
1651
- router.delete('/recipes/:recipeId', (req, res) => {
1652
- const deleted = lfdEngine.deleteRecipe(req.params.recipeId);
1653
- res.json({ deleted });
1654
- });
1655
-
1656
- /**
1657
- * Execute a recipe
1658
- */
1659
- router.post('/recipes/:recipeId/execute', (req, res) => {
1660
- try {
1661
- const execution = lfdEngine.executeRecipe(req.params.recipeId, {
1662
- variables: req.body.variables,
1663
- speed: req.body.speed,
1664
- stopOnError: req.body.stopOnError,
1665
- skipWaits: req.body.skipWaits,
1666
- humanInTheLoop: req.body.humanInTheLoop,
1667
- });
1668
- res.json(execution);
1669
- } catch (err) {
1670
- res.status(400).json({ error: err.message });
1671
- }
1672
- });
1673
-
1674
- /**
1675
- * Get next step in execution
1676
- */
1677
- router.get('/executions/:executionId/next', (req, res) => {
1678
- const step = lfdEngine.getNextStep(req.params.executionId);
1679
- if (!step) {
1680
- const exec = lfdEngine.getExecution(req.params.executionId);
1681
- return res.json({ done: true, status: exec?.status || 'unknown' });
1682
- }
1683
- res.json(step);
1684
- });
1685
-
1686
- /**
1687
- * Report step result
1688
- */
1689
- router.post('/executions/:executionId/steps/:stepIndex', (req, res) => {
1690
- const exec = lfdEngine.reportStep(
1691
- req.params.executionId,
1692
- parseInt(req.params.stepIndex),
1693
- { success: req.body.success, error: req.body.error, duration: req.body.duration, selectorUsed: req.body.selectorUsed }
1694
- );
1695
- if (!exec) return res.status(404).json({ error: 'Execution not found' });
1696
- res.json({ status: exec.status, currentStep: exec.currentStep, totalSteps: exec.totalSteps });
1697
- });
1698
-
1699
- /**
1700
- * Pause execution
1701
- */
1702
- router.post('/executions/:executionId/pause', (req, res) => {
1703
- const exec = lfdEngine.pauseExecution(req.params.executionId);
1704
- if (!exec) return res.status(404).json({ error: 'Execution not found' });
1705
- res.json({ status: exec.status });
1706
- });
1707
-
1708
- /**
1709
- * Resume execution
1710
- */
1711
- router.post('/executions/:executionId/resume', (req, res) => {
1712
- const exec = lfdEngine.resumeExecution(req.params.executionId);
1713
- if (!exec) return res.status(404).json({ error: 'Execution not found' });
1714
- res.json({ status: exec.status });
1715
- });
1716
-
1717
- /**
1718
- * Abort execution
1719
- */
1720
- router.post('/executions/:executionId/abort', (req, res) => {
1721
- const exec = lfdEngine.abortExecution(req.params.executionId);
1722
- if (!exec) return res.status(404).json({ error: 'Execution not found' });
1723
- res.json({ status: exec.status });
1724
- });
1725
-
1726
- /**
1727
- * Get execution details
1728
- */
1729
- router.get('/executions/:executionId', (req, res) => {
1730
- const exec = lfdEngine.getExecution(req.params.executionId);
1731
- if (!exec) return res.status(404).json({ error: 'Execution not found' });
1732
- res.json(exec);
1733
- });
1734
-
1735
- /**
1736
- * List executions
1737
- */
1738
- router.get('/executions', (req, res) => {
1739
- res.json({ executions: lfdEngine.listExecutions(parseInt(req.query.limit) || 50) });
1740
- });
1741
-
1742
- /**
1743
- * LfD stats
1744
- */
1745
- router.get('/lfd/stats', (req, res) => {
1746
- res.json(lfdEngine.getStats());
1747
- });
1748
-
1749
- // ═══════════════════════════════════════════════════════════════════════════
1750
- // CLUSTER — DISTRIBUTED EXECUTION & WORKER NODES
1751
- // ═══════════════════════════════════════════════════════════════════════════
1752
-
1753
- /**
1754
- * Get cluster status (public)
1755
- */
1756
- router.get('/cluster/status', (req, res) => {
1757
- res.json(cluster.getClusterStatus());
1758
- });
1759
-
1760
- /**
1761
- * Register a worker node
1762
- */
1763
- router.post('/cluster/nodes', (req, res) => {
1764
- try {
1765
- const result = cluster.registerNode({
1766
- name: req.body.name,
1767
- endpoint: req.body.endpoint,
1768
- region: req.body.region,
1769
- zone: req.body.zone,
1770
- role: req.body.role,
1771
- capacity: req.body.capacity,
1772
- tags: req.body.tags,
1773
- hardware: req.body.hardware,
1774
- version: req.body.version,
1775
- secret: req.body.secret,
1776
- });
1777
- res.json(result);
1778
- } catch (err) {
1779
- res.status(400).json({ error: err.message });
1780
- }
1781
- });
1782
-
1783
- /**
1784
- * List cluster nodes
1785
- */
1786
- router.get('/cluster/nodes', (req, res) => {
1787
- const nodes = cluster.listNodes({
1788
- region: req.query.region,
1789
- active: req.query.active === 'true',
1790
- });
1791
- res.json({ nodes });
1792
- });
1793
-
1794
- /**
1795
- * Get a specific node
1796
- */
1797
- router.get('/cluster/nodes/:nodeId', (req, res) => {
1798
- const node = cluster.getNode(req.params.nodeId);
1799
- if (!node) return res.status(404).json({ error: 'Node not found' });
1800
- res.json(node);
1801
- });
1802
-
1803
- /**
1804
- * Remove a node
1805
- */
1806
- router.delete('/cluster/nodes/:nodeId', (req, res) => {
1807
- const result = cluster.deregisterNode(req.params.nodeId);
1808
- if (!result) return res.status(404).json({ error: 'Node not found' });
1809
- res.json(result);
1810
- });
1811
-
1812
- /**
1813
- * Worker heartbeat
1814
- */
1815
- router.post('/cluster/nodes/:nodeId/heartbeat', (req, res) => {
1816
- const result = cluster.heartbeat(req.params.nodeId, {
1817
- capacityUsed: req.body.capacityUsed,
1818
- capacityTotal: req.body.capacityTotal,
1819
- hardware: req.body.hardware,
1820
- tags: req.body.tags,
1821
- version: req.body.version,
1822
- });
1823
- if (!result) return res.status(404).json({ error: 'Node not found' });
1824
- res.json(result);
1825
- });
1826
-
1827
- /**
1828
- * Drain a node (stop new tasks, wait for running)
1829
- */
1830
- router.post('/cluster/nodes/:nodeId/drain', (req, res) => {
1831
- const result = cluster.drainNode(req.params.nodeId);
1832
- if (!result) return res.status(404).json({ error: 'Node not found' });
1833
- res.json(result);
1834
- });
1835
-
1836
- /**
1837
- * Cordon a node (prevent scheduling)
1838
- */
1839
- router.post('/cluster/nodes/:nodeId/cordon', (req, res) => {
1840
- const result = cluster.cordonNode(req.params.nodeId);
1841
- if (!result) return res.status(404).json({ error: 'Node not found' });
1842
- res.json(result);
1843
- });
1844
-
1845
- /**
1846
- * Uncordon a node (allow scheduling again)
1847
- */
1848
- router.post('/cluster/nodes/:nodeId/uncordon', (req, res) => {
1849
- const result = cluster.uncordonNode(req.params.nodeId);
1850
- if (!result) return res.status(404).json({ error: 'Node not found' });
1851
- res.json(result);
1852
- });
1853
-
1854
- /**
1855
- * Submit a task for distributed execution
1856
- */
1857
- router.post('/cluster/tasks', (req, res) => {
1858
- try {
1859
- const result = distributor.submit({
1860
- type: req.body.type,
1861
- objective: req.body.objective,
1862
- params: req.body.params,
1863
- priority: req.body.priority,
1864
- affinityTags: req.body.affinityTags,
1865
- affinityRegion: req.body.affinityRegion,
1866
- timeout: req.body.timeout,
1867
- maxAttempts: req.body.maxAttempts,
1868
- externalId: req.body.externalId,
1869
- });
1870
- res.json(result);
1871
- } catch (err) {
1872
- res.status(400).json({ error: err.message });
1873
- }
1874
- });
1875
-
1876
- /**
1877
- * Get task details
1878
- */
1879
- router.get('/cluster/tasks/:taskId', (req, res) => {
1880
- const task = cluster.getTask(req.params.taskId);
1881
- if (!task) return res.status(404).json({ error: 'Task not found' });
1882
- res.json(task);
1883
- });
1884
-
1885
- /**
1886
- * List tasks
1887
- */
1888
- router.get('/cluster/tasks', (req, res) => {
1889
- const tasks = cluster.listTasks({
1890
- status: req.query.status,
1891
- nodeId: req.query.nodeId,
1892
- limit: parseInt(req.query.limit) || 50,
1893
- });
1894
- res.json({ tasks });
1895
- });
1896
-
1897
- /**
1898
- * Worker pulls tasks (poll-based)
1899
- */
1900
- router.post('/cluster/nodes/:nodeId/pull', (req, res) => {
1901
- const tasks = distributor.pullTasks(req.params.nodeId, parseInt(req.body.limit) || 5);
1902
- res.json({ tasks });
1903
- });
1904
-
1905
- /**
1906
- * Worker reports task started
1907
- */
1908
- router.post('/cluster/tasks/:taskId/started', (req, res) => {
1909
- const result = cluster.reportTaskStarted(req.params.taskId);
1910
- if (!result) return res.status(404).json({ error: 'Task not found' });
1911
- res.json(result);
1912
- });
1913
-
1914
- /**
1915
- * Worker reports task completed
1916
- */
1917
- router.post('/cluster/tasks/:taskId/completed', (req, res) => {
1918
- const result = cluster.reportTaskCompleted(req.params.taskId, req.body.result);
1919
- if (!result) return res.status(404).json({ error: 'Task not found' });
1920
- res.json(result);
1921
- });
1922
-
1923
- /**
1924
- * Worker reports task failed
1925
- */
1926
- router.post('/cluster/tasks/:taskId/failed', (req, res) => {
1927
- const result = cluster.reportTaskFailed(req.params.taskId, req.body.error);
1928
- if (!result) return res.status(404).json({ error: 'Task not found' });
1929
- res.json(result);
1930
- });
1931
-
1932
- /**
1933
- * Get cluster events log
1934
- */
1935
- router.get('/cluster/events', (req, res) => {
1936
- const events = cluster.getEvents(
1937
- parseInt(req.query.limit) || 100,
1938
- req.query.nodeId || null
1939
- );
1940
- res.json({ events });
1941
- });
1942
-
1943
- // ═══════════════════════════════════════════════════════════════════════════
1944
- // CONTAINER ISOLATION
1945
- // ═══════════════════════════════════════════════════════════════════════════
1946
-
1947
- let containerRunner;
1948
- try { containerRunner = require('../runtime/container').containerRunner; } catch {}
1949
-
1950
- /**
1951
- * Run a task in an isolated container
1952
- */
1953
- router.post('/containers/run', async (req, res) => {
1954
- if (!containerRunner) return res.status(501).json({ error: 'Container isolation not available' });
1955
- try {
1956
- const result = await containerRunner.runInProcess(
1957
- req.body.taskId || `ctr_task_${Date.now()}`,
1958
- req.body.code || '',
1959
- {
1960
- params: req.body.params || {},
1961
- timeout: req.body.timeout || 60000,
1962
- maxMemory: req.body.maxMemory || 256 * 1024 * 1024,
1963
- allowNetwork: req.body.allowNetwork !== false,
1964
- }
1965
- );
1966
- res.json(result);
1967
- } catch (err) {
1968
- res.status(500).json({ error: err.message });
1969
- }
1970
- });
1971
-
1972
- /**
1973
- * List active containers
1974
- */
1975
- router.get('/containers', (req, res) => {
1976
- if (!containerRunner) return res.json({ containers: [] });
1977
- res.json({ containers: containerRunner.listContainers() });
1978
- });
1979
-
1980
- /**
1981
- * Get container details
1982
- */
1983
- router.get('/containers/:containerId', (req, res) => {
1984
- if (!containerRunner) return res.status(404).json({ error: 'Not found' });
1985
- const c = containerRunner.getContainer(req.params.containerId);
1986
- if (!c) return res.status(404).json({ error: 'Container not found' });
1987
- res.json(c);
1988
- });
1989
-
1990
- /**
1991
- * Kill a container
1992
- */
1993
- router.post('/containers/:containerId/kill', (req, res) => {
1994
- if (!containerRunner) return res.status(404).json({ error: 'Not found' });
1995
- const ok = containerRunner.kill(req.params.containerId);
1996
- res.json({ success: ok });
1997
- });
1998
-
1999
- /**
2000
- * Container stats
2001
- */
2002
- router.get('/containers/stats/summary', (req, res) => {
2003
- if (!containerRunner) return res.json({ active: 0 });
2004
- res.json(containerRunner.getStats());
2005
- });
2006
-
2007
- /**
2008
- * Check Docker availability
2009
- */
2010
- router.get('/containers/docker/status', (req, res) => {
2011
- if (!containerRunner) return res.json({ available: false });
2012
- res.json({ available: containerRunner.isDockerAvailable() });
2013
- });
2014
-
2015
- // ═══════════════════════════════════════════════════════════════════════════
2016
- // EXTERNAL QUEUE MANAGEMENT
2017
- // ═══════════════════════════════════════════════════════════════════════════
2018
-
2019
- let queueModule;
2020
- try { queueModule = require('../runtime/queue'); } catch {}
2021
-
2022
- /**
2023
- * Queue stats
2024
- */
2025
- router.get('/queue/stats', (req, res) => {
2026
- if (!queueModule) return res.json({ backend: 'memory' });
2027
- const q = queueModule.createQueue('scheduler');
2028
- res.json(q.getStats());
2029
- });
2030
-
2031
- /**
2032
- * Purge completed items from queue
2033
- */
2034
- router.post('/queue/purge', (req, res) => {
2035
- if (!queueModule) return res.json({ purged: 0 });
2036
- const q = queueModule.createQueue('scheduler');
2037
- const purged = q.purgeCompleted(parseInt(req.body.maxAge) || 3600_000);
2038
- res.json({ purged });
2039
- });
2040
-
2041
- // ═══════════════════════════════════════════════════════════════════════════
2042
- // ENHANCED REPLAY
2043
- // ═══════════════════════════════════════════════════════════════════════════
2044
-
2045
- /**
2046
- * Export a recording (full data for download)
2047
- */
2048
- router.get('/replay/recordings/:taskId/export', (req, res) => {
2049
- const data = replayEngine.exportRecording(req.params.taskId);
2050
- if (!data) return res.status(404).json({ error: 'Recording not found' });
2051
- res.json(data);
2052
- });
2053
-
2054
- /**
2055
- * Import a recording
2056
- */
2057
- router.post('/replay/recordings/import', (req, res) => {
2058
- try {
2059
- const id = replayEngine.importRecording(req.body);
2060
- res.json({ success: true, recordingId: id });
2061
- } catch (err) {
2062
- res.status(400).json({ error: err.message });
2063
- }
2064
- });
2065
-
2066
- /**
2067
- * Delete a recording
2068
- */
2069
- router.delete('/replay/recordings/:taskId', (req, res) => {
2070
- replayEngine.deleteRecording(req.params.taskId);
2071
- res.json({ success: true });
2072
- });
2073
-
2074
- /**
2075
- * Replay from a specific checkpoint
2076
- */
2077
- router.post('/replay/:taskId/from-checkpoint', async (req, res) => {
2078
- try {
2079
- const result = await replayEngine.replay(req.params.taskId, {
2080
- verify: req.body.verify !== false,
2081
- continueOnMismatch: !!req.body.continueOnMismatch,
2082
- fromCheckpoint: req.body.checkpoint,
2083
- });
2084
- res.json(result);
2085
- } catch (err) {
2086
- res.status(400).json({ error: err.message });
2087
- }
2088
- });
2089
-
2090
- /**
2091
- * Purge old recordings
2092
- */
2093
- router.post('/replay/purge', (req, res) => {
2094
- const maxAge = parseInt(req.body.maxAge) || 7 * 24 * 3600_000;
2095
- replayEngine.purgeOld(maxAge);
2096
- res.json({ success: true });
2097
- });
2098
-
2099
- // ═══════════════════════════════════════════════════════════════════════════
2100
- // WORKER PULL ENDPOINT (for distributed workers)
2101
- // ═══════════════════════════════════════════════════════════════════════════
2102
-
2103
- /**
2104
- * Workers pull tasks from here
2105
- */
2106
- router.post('/cluster/nodes/:nodeId/pull', (req, res) => {
2107
- const limit = parseInt(req.body.limit) || 5;
2108
- // Fetch pending tasks from the cluster task distributor
2109
- const tasks = [];
2110
- try {
2111
- const pending = distributor.getPendingTasks ? distributor.getPendingTasks(req.params.nodeId, limit) : [];
2112
- tasks.push(...pending);
2113
- } catch {}
2114
- res.json({ tasks });
2115
- });
2116
-
2117
- /**
2118
- * Worker reports task started
2119
- */
2120
- router.post('/cluster/tasks/:taskId/started', (req, res) => {
2121
- bus.emit('cluster.task.started', { taskId: req.params.taskId, nodeId: req.body.nodeId });
2122
- res.json({ ok: true });
2123
- });
2124
-
2125
- /**
2126
- * Worker reports task completed
2127
- */
2128
- router.post('/cluster/tasks/:taskId/completed', (req, res) => {
2129
- bus.emit('cluster.task.completed', {
2130
- taskId: req.params.taskId,
2131
- result: req.body.result,
2132
- });
2133
- res.json({ ok: true });
2134
- });
2135
-
2136
- /**
2137
- * Worker reports task failed
2138
- */
2139
- router.post('/cluster/tasks/:taskId/failed', (req, res) => {
2140
- bus.emit('cluster.task.failed', {
2141
- taskId: req.params.taskId,
2142
- error: req.body.error,
2143
- });
2144
- res.json({ ok: true });
2145
- });
2146
-
2147
- module.exports = router;
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 { featureGate, usageLimit } = require('../middleware/featureGate');
32
+ const { sensitiveActionGate } = require('../middleware/sensitiveAction');
33
+ const { listPlans, getPlan, USAGE_PRICING, MARKETPLACE } = require('../config/plans');
34
+ const metering = require('../services/metering');
35
+ const { marketplace } = require('../services/marketplace');
36
+ const { hostedRuntime } = require('../services/hosted-runtime');
37
+ const { sessionEngine } = require('../runtime/session-engine');
38
+ const vision = require('../services/vision');
39
+ const { lfdEngine } = require('../services/lfd');
40
+ const { cluster, distributor } = require('../services/cluster');
41
+
42
+ // ═══════════════════════════════════════════════════════════════════════════
43
+ // AUTH MIDDLEWARE
44
+ // ═══════════════════════════════════════════════════════════════════════════
45
+
46
+ /**
47
+ * Authenticate requests via API key or session token.
48
+ * Public endpoints (protocol info, agent registration, health) bypass auth.
49
+ */
50
+ const PUBLIC_PATHS = [
51
+ '/protocol',
52
+ '/agents/register',
53
+ '/agents/authenticate',
54
+ '/observability/health',
55
+ '/llm/models',
56
+ '/llm/status',
57
+ '/registry/commands',
58
+ '/registry/sites',
59
+ '/registry/templates',
60
+ '/plans',
61
+ '/marketplace',
62
+ '/recipes',
63
+ '/vision/models',
64
+ '/vision/extraction-script',
65
+ '/cluster/status',
66
+ ];
67
+
68
+ function authMiddleware(req, res, next) {
69
+ // Allow public GET endpoints
70
+ const matchesPublic = PUBLIC_PATHS.some(p =>
71
+ req.path === p || (req.method === 'GET' && req.path.startsWith(p))
72
+ );
73
+ if (matchesPublic) return next();
74
+
75
+ // Check session token
76
+ const authHeader = req.headers['authorization'];
77
+ if (authHeader && authHeader.startsWith('Bearer ')) {
78
+ const token = authHeader.slice(7);
79
+ const session = identity.validateSession(token);
80
+ if (session) {
81
+ req.agentId = session.agentId;
82
+ req.session = session;
83
+ return next();
84
+ }
85
+ }
86
+
87
+ // Check API key
88
+ const apiKey = req.headers['x-wab-key'];
89
+ if (apiKey) {
90
+ const ip = req.ip || req.connection?.remoteAddress;
91
+ const session = identity.authenticate(apiKey, ip);
92
+ if (session) {
93
+ req.agentId = session.agentId;
94
+ req.session = session;
95
+ return next();
96
+ }
97
+ }
98
+
99
+ // Check agent ID header (for internal/trusted calls)
100
+ const agentHeader = req.headers['x-wab-agent'];
101
+ if (agentHeader) {
102
+ const agent = identity.getAgent(agentHeader);
103
+ if (agent && agent.status === 'active') {
104
+ req.agentId = agentHeader;
105
+ return next();
106
+ }
107
+ }
108
+
109
+ // No auth on non-mutation GET requests (read-only)
110
+ if (req.method === 'GET') return next();
111
+
112
+ metrics.increment('auth.rejected');
113
+ return res.status(401).json({ error: 'Authentication required. Provide X-WAB-Key or Authorization: Bearer <token>' });
114
+ }
115
+
116
+ router.use(authMiddleware);
117
+ router.use(featureGate);
118
+
119
+ // ═══════════════════════════════════════════════════════════════════════════
120
+ // PROTOCOL ENDPOINTS
121
+ // ═══════════════════════════════════════════════════════════════════════════
122
+
123
+ /**
124
+ * Protocol info & capabilities
125
+ */
126
+ router.get('/protocol', (req, res) => {
127
+ res.json({
128
+ protocol: protocol.PROTOCOL_NAME,
129
+ version: protocol.PROTOCOL_VERSION,
130
+ commands: protocol.schema.listCommands().map(c => ({
131
+ name: c.name,
132
+ version: c.version,
133
+ category: c.category,
134
+ description: c.description,
135
+ capabilities: c.capabilities,
136
+ })),
137
+ capabilities: Object.keys(protocol.schema.Capabilities),
138
+ permissionLevels: protocol.schema.PermissionLevels,
139
+ });
140
+ });
141
+
142
+ /**
143
+ * Process a protocol message
144
+ */
145
+ router.post('/protocol/message', async (req, res) => {
146
+ const endTimer = metrics.startTimer('api.protocol.message.duration');
147
+ try {
148
+ const msg = req.body;
149
+ if (!msg || !msg.command) {
150
+ return res.status(400).json({ error: 'Invalid protocol message' });
151
+ }
152
+
153
+ // Create proper protocol request if not already
154
+ const request = msg.protocol === 'wabp' ? msg : protocol.createRequest(msg.command, msg.payload || msg.params || {}, {
155
+ agentId: msg.agentId,
156
+ traceId: msg.traceId,
157
+ });
158
+
159
+ const response = await protocolHandler.process(request);
160
+ endTimer();
161
+ metrics.increment('api.protocol.messages', 1, { command: msg.command });
162
+ res.json(response);
163
+ } catch (err) {
164
+ endTimer();
165
+ res.status(500).json({ error: err.message });
166
+ }
167
+ });
168
+
169
+ // ═══════════════════════════════════════════════════════════════════════════
170
+ // AGENT IDENTITY & AUTH
171
+ // ═══════════════════════════════════════════════════════════════════════════
172
+
173
+ /**
174
+ * Register a new agent
175
+ */
176
+ router.post('/agents/register', (req, res) => {
177
+ try {
178
+ const { name, type, capabilities, publicKey, metadata } = req.body;
179
+ if (!name || !type) return res.status(400).json({ error: 'name and type required' });
180
+
181
+ const result = identity.register(name, type, { capabilities, publicKey, metadata });
182
+ metrics.increment('agents.registered');
183
+ logger.info('Agent registered', { agentId: result.agentId, name, type });
184
+
185
+ res.json({
186
+ agentId: result.agentId,
187
+ apiKey: result.apiKey, // Only returned once!
188
+ message: 'Store your API key securely. It cannot be recovered.',
189
+ });
190
+ } catch (err) {
191
+ res.status(500).json({ error: err.message });
192
+ }
193
+ });
194
+
195
+ /**
196
+ * Authenticate agent
197
+ */
198
+ router.post('/agents/authenticate', (req, res) => {
199
+ const { apiKey } = req.body;
200
+ if (!apiKey) return res.status(400).json({ error: 'apiKey required' });
201
+
202
+ const ip = req.ip || req.connection?.remoteAddress;
203
+ const session = identity.authenticate(apiKey, ip);
204
+ if (!session) {
205
+ metrics.increment('agents.auth.failed');
206
+ return res.status(401).json({ error: 'Invalid API key or agent revoked' });
207
+ }
208
+
209
+ metrics.increment('agents.auth.success');
210
+ res.json(session);
211
+ });
212
+
213
+ /**
214
+ * Get agent info
215
+ */
216
+ router.get('/agents/:agentId', (req, res) => {
217
+ const agent = identity.getAgent(req.params.agentId);
218
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
219
+ res.json(agent);
220
+ });
221
+
222
+ /**
223
+ * List agents
224
+ */
225
+ router.get('/agents', (req, res) => {
226
+ const agents = identity.listAgents({ type: req.query.type, status: req.query.status || 'active' });
227
+ res.json({ agents, total: agents.length });
228
+ });
229
+
230
+ /**
231
+ * Negotiate capabilities
232
+ */
233
+ router.post('/agents/:agentId/capabilities', (req, res) => {
234
+ const { capabilities, siteId, constraints } = req.body;
235
+ if (!capabilities || !Array.isArray(capabilities)) {
236
+ return res.status(400).json({ error: 'capabilities array required' });
237
+ }
238
+
239
+ const result = protocol.negotiator.negotiate(req.params.agentId, capabilities, siteId, constraints || {});
240
+ res.json(result);
241
+ });
242
+
243
+ /**
244
+ * Revoke agent
245
+ */
246
+ router.delete('/agents/:agentId', (req, res) => {
247
+ identity.revoke(req.params.agentId);
248
+ protocol.negotiator.revokeAgent(req.params.agentId);
249
+ logger.info('Agent revoked', { agentId: req.params.agentId });
250
+ res.json({ success: true });
251
+ });
252
+
253
+ // ═══════════════════════════════════════════════════════════════════════════
254
+ // TASK MANAGEMENT (RUNTIME)
255
+ // ═══════════════════════════════════════════════════════════════════════════
256
+
257
+ /**
258
+ * Submit a task
259
+ */
260
+ router.post('/tasks', usageLimit('tasksPerDay'), sensitiveActionGate, (req, res) => {
261
+ try {
262
+ const result = runtime.submitTask(req.body);
263
+ metrics.increment('tasks.submitted', 1, { type: req.body.type });
264
+ res.json(result);
265
+ } catch (err) {
266
+ res.status(400).json({ error: err.message });
267
+ }
268
+ });
269
+
270
+ /**
271
+ * Get task status
272
+ */
273
+ router.get('/tasks/:taskId', (req, res) => {
274
+ const task = runtime.scheduler.getTask(req.params.taskId);
275
+ if (!task) return res.status(404).json({ error: 'Task not found' });
276
+ res.json(task);
277
+ });
278
+
279
+ /**
280
+ * List tasks
281
+ */
282
+ router.get('/tasks', (req, res) => {
283
+ const tasks = runtime.scheduler.listTasks(req.query.state, parseInt(req.query.limit) || 50);
284
+ res.json({ tasks, total: tasks.length });
285
+ });
286
+
287
+ /**
288
+ * Cancel a task
289
+ */
290
+ router.delete('/tasks/:taskId', (req, res) => {
291
+ const success = runtime.scheduler.cancel(req.params.taskId);
292
+ res.json({ success });
293
+ });
294
+
295
+ /**
296
+ * Pause a task
297
+ */
298
+ router.post('/tasks/:taskId/pause', (req, res) => {
299
+ const success = runtime.scheduler.pause(req.params.taskId);
300
+ res.json({ success });
301
+ });
302
+
303
+ /**
304
+ * Resume a task
305
+ */
306
+ router.post('/tasks/:taskId/resume', (req, res) => {
307
+ const success = runtime.scheduler.resume(req.params.taskId);
308
+ res.json({ success });
309
+ });
310
+
311
+ // ═══════════════════════════════════════════════════════════════════════════
312
+ // EXECUTION (DATA PLANE)
313
+ // ═══════════════════════════════════════════════════════════════════════════
314
+
315
+ /**
316
+ * Execute a semantic action
317
+ */
318
+ router.post('/execute', usageLimit('executionsPerDay'), sensitiveActionGate, async (req, res) => {
319
+ try {
320
+ const result = await executor.execute(req.body);
321
+ res.json(result);
322
+ } catch (err) {
323
+ res.status(500).json({ error: err.message });
324
+ }
325
+ });
326
+
327
+ /**
328
+ * Execute semantic action (domain.action style)
329
+ */
330
+ router.post('/execute/semantic', sensitiveActionGate, async (req, res) => {
331
+ try {
332
+ const { domain, action, params, siteId, agentId, siteDomain } = req.body;
333
+ if (!domain || !action) return res.status(400).json({ error: 'domain and action required' });
334
+
335
+ const result = await executor.execute({
336
+ type: 'semantic',
337
+ domain,
338
+ action,
339
+ params: params || {},
340
+ siteId,
341
+ agentId,
342
+ siteDomain,
343
+ });
344
+ res.json(result);
345
+ } catch (err) {
346
+ res.status(500).json({ error: err.message });
347
+ }
348
+ });
349
+
350
+ /**
351
+ * Execute a pipeline
352
+ */
353
+ router.post('/execute/pipeline', sensitiveActionGate, async (req, res) => {
354
+ try {
355
+ const result = await executor.execute({ ...req.body, type: 'pipeline' });
356
+ res.json(result);
357
+ } catch (err) {
358
+ res.status(500).json({ error: err.message });
359
+ }
360
+ });
361
+
362
+ /**
363
+ * Resolve a semantic action (without executing)
364
+ */
365
+ router.get('/execute/resolve', (req, res) => {
366
+ const { domain, action, siteDomain } = req.query;
367
+ if (!domain || !action) return res.status(400).json({ error: 'domain and action required' });
368
+ const impl = executor.resolver.resolve(siteDomain || '*', `${domain}.${action}`);
369
+ if (!impl) return res.status(404).json({ error: 'No implementation found' });
370
+ res.json(impl);
371
+ });
372
+
373
+ // ═══════════════════════════════════════════════════════════════════════════
374
+ // CONTROL PLANE
375
+ // ═══════════════════════════════════════════════════════════════════════════
376
+
377
+ /**
378
+ * Deploy an agent
379
+ */
380
+ router.post('/deployments', (req, res) => {
381
+ try {
382
+ const { agentId, config } = req.body;
383
+ if (!agentId) return res.status(400).json({ error: 'agentId required' });
384
+ const deployment = agentManager.deploy(agentId, config || {});
385
+ res.json(deployment);
386
+ } catch (err) {
387
+ res.status(400).json({ error: err.message });
388
+ }
389
+ });
390
+
391
+ /**
392
+ * List deployments
393
+ */
394
+ router.get('/deployments', (req, res) => {
395
+ const deployments = agentManager.listDeployments({
396
+ status: req.query.status,
397
+ agentId: req.query.agentId,
398
+ });
399
+ res.json({ deployments, total: deployments.length });
400
+ });
401
+
402
+ /**
403
+ * Create a policy
404
+ */
405
+ router.post('/policies', (req, res) => {
406
+ try {
407
+ const policy = policyEngine.createPolicy(req.body);
408
+ res.json(policy);
409
+ } catch (err) {
410
+ res.status(400).json({ error: err.message });
411
+ }
412
+ });
413
+
414
+ /**
415
+ * Bind policy to entity
416
+ */
417
+ router.post('/policies/:policyId/bind', (req, res) => {
418
+ const { entityId } = req.body;
419
+ if (!entityId) return res.status(400).json({ error: 'entityId required' });
420
+ policyEngine.bind(entityId, req.params.policyId);
421
+ res.json({ success: true });
422
+ });
423
+
424
+ /**
425
+ * Evaluate policies
426
+ */
427
+ router.post('/policies/evaluate', (req, res) => {
428
+ const { entityId, action, context } = req.body;
429
+ if (!entityId || !action) return res.status(400).json({ error: 'entityId and action required' });
430
+ const result = policyEngine.evaluate(entityId, action, context || {});
431
+ res.json(result);
432
+ });
433
+
434
+ /**
435
+ * List policies
436
+ */
437
+ router.get('/policies', (req, res) => {
438
+ const policies = policyEngine.listPolicies(req.query.entityId);
439
+ res.json({ policies, total: policies.length });
440
+ });
441
+
442
+ // ═══════════════════════════════════════════════════════════════════════════
443
+ // SITE ISOLATION
444
+ // ═══════════════════════════════════════════════════════════════════════════
445
+
446
+ /**
447
+ * Configure site isolation
448
+ */
449
+ router.post('/isolation/:siteId', (req, res) => {
450
+ isolation.configure(req.params.siteId, req.body);
451
+ res.json({ success: true });
452
+ });
453
+
454
+ /**
455
+ * Get site isolation config
456
+ */
457
+ router.get('/isolation/:siteId', (req, res) => {
458
+ const config = isolation.getConfig(req.params.siteId);
459
+ if (!config) return res.status(404).json({ error: 'No isolation config' });
460
+ res.json(config);
461
+ });
462
+
463
+ // ═══════════════════════════════════════════════════════════════════════════
464
+ // OBSERVABILITY
465
+ // ═══════════════════════════════════════════════════════════════════════════
466
+
467
+ /**
468
+ * Get metrics snapshot
469
+ */
470
+ router.get('/observability/metrics', (req, res) => {
471
+ res.json(metrics.snapshot());
472
+ });
473
+
474
+ /**
475
+ * Get specific metric
476
+ */
477
+ router.get('/observability/metrics/:name', (req, res) => {
478
+ const h = metrics.getHistogram(req.params.name);
479
+ if (h) return res.json({ type: 'histogram', name: req.params.name, ...h });
480
+
481
+ const c = metrics.getCounter(req.params.name);
482
+ if (c) return res.json({ type: 'counter', name: req.params.name, value: c });
483
+
484
+ const g = metrics.getGauge(req.params.name);
485
+ if (g) return res.json({ type: 'gauge', name: req.params.name, value: g });
486
+
487
+ res.status(404).json({ error: 'Metric not found' });
488
+ });
489
+
490
+ /**
491
+ * List traces
492
+ */
493
+ router.get('/observability/traces', (req, res) => {
494
+ const traces = tracer.listTraces(
495
+ parseInt(req.query.limit) || 50,
496
+ { status: req.query.status, name: req.query.name, since: parseInt(req.query.since) || undefined }
497
+ );
498
+ res.json({ traces, total: traces.length });
499
+ });
500
+
501
+ /**
502
+ * Get trace details
503
+ */
504
+ router.get('/observability/traces/:traceId', (req, res) => {
505
+ const trace = tracer.getTrace(req.params.traceId);
506
+ if (!trace) return res.status(404).json({ error: 'Trace not found' });
507
+ res.json(trace);
508
+ });
509
+
510
+ /**
511
+ * Query logs
512
+ */
513
+ router.get('/observability/logs', (req, res) => {
514
+ const logs = logger.query({
515
+ level: req.query.level,
516
+ traceId: req.query.traceId,
517
+ agentId: req.query.agentId,
518
+ since: parseInt(req.query.since) || undefined,
519
+ message: req.query.message,
520
+ }, parseInt(req.query.limit) || 100);
521
+ res.json({ logs, total: logs.length });
522
+ });
523
+
524
+ /**
525
+ * Runtime health
526
+ */
527
+ router.get('/observability/health', (req, res) => {
528
+ const health = runtime.getHealth();
529
+ health.identity = identity.getStats();
530
+ health.registry = {
531
+ commands: commandRegistry.getStats(),
532
+ sites: siteRegistry.getStats(),
533
+ templates: templateRegistry.getStats(),
534
+ };
535
+ health.executor = executor.getStats();
536
+ health.llm = llm.getStatus();
537
+ health.adapters = adapterManager.getStats();
538
+ health.replay = replayEngine.getStats();
539
+ health.sessions = sessionEngine.getStats();
540
+ health.failures = failureAnalyzer.getStats();
541
+ health.certification = certificationEngine.getStats();
542
+ health.marketplace = marketplace.getStats();
543
+ health.hostedRuntime = hostedRuntime.getStats();
544
+ health.metering = metering.getStats();
545
+ health.lfd = lfdEngine.getStats();
546
+ health.cluster = cluster.getClusterStatus();
547
+ res.json(health);
548
+ });
549
+
550
+ // ═══════════════════════════════════════════════════════════════════════════
551
+ // REGISTRY
552
+ // ═══════════════════════════════════════════════════════════════════════════
553
+
554
+ /**
555
+ * Register a command
556
+ */
557
+ router.post('/registry/commands', (req, res) => {
558
+ try {
559
+ const { siteId, ...command } = req.body;
560
+ if (!siteId) return res.status(400).json({ error: 'siteId required' });
561
+ const entry = commandRegistry.register(siteId, command);
562
+ res.json(entry);
563
+ } catch (err) {
564
+ res.status(400).json({ error: err.message });
565
+ }
566
+ });
567
+
568
+ /**
569
+ * Search commands
570
+ */
571
+ router.get('/registry/commands', (req, res) => {
572
+ const results = commandRegistry.search({
573
+ siteId: req.query.siteId,
574
+ category: req.query.category,
575
+ name: req.query.name,
576
+ tag: req.query.tag,
577
+ capability: req.query.capability,
578
+ limit: parseInt(req.query.limit) || 50,
579
+ });
580
+ res.json({ commands: results, total: results.length });
581
+ });
582
+
583
+ /**
584
+ * Register a site
585
+ */
586
+ router.post('/registry/sites', (req, res) => {
587
+ const { domain, ...info } = req.body;
588
+ if (!domain) return res.status(400).json({ error: 'domain required' });
589
+ const entry = siteRegistry.register(domain, info);
590
+ res.json(entry);
591
+ });
592
+
593
+ /**
594
+ * Search sites
595
+ */
596
+ router.get('/registry/sites', (req, res) => {
597
+ const results = siteRegistry.search({
598
+ tier: req.query.tier,
599
+ capability: req.query.capability,
600
+ name: req.query.name,
601
+ verified: req.query.verified === 'true' ? true : undefined,
602
+ limit: parseInt(req.query.limit) || 50,
603
+ });
604
+ res.json({ sites: results, total: results.length });
605
+ });
606
+
607
+ /**
608
+ * Get site info
609
+ */
610
+ router.get('/registry/sites/:domain', (req, res) => {
611
+ const site = siteRegistry.getSite(req.params.domain);
612
+ if (!site) return res.status(404).json({ error: 'Site not found' });
613
+ res.json(site);
614
+ });
615
+
616
+ /**
617
+ * Register a template
618
+ */
619
+ router.post('/registry/templates', (req, res) => {
620
+ try {
621
+ const entry = templateRegistry.register(req.body);
622
+ res.json(entry);
623
+ } catch (err) {
624
+ res.status(400).json({ error: err.message });
625
+ }
626
+ });
627
+
628
+ /**
629
+ * Search templates
630
+ */
631
+ router.get('/registry/templates', (req, res) => {
632
+ const results = templateRegistry.search({
633
+ category: req.query.category,
634
+ name: req.query.name,
635
+ tag: req.query.tag,
636
+ limit: parseInt(req.query.limit) || 50,
637
+ });
638
+ res.json({ templates: results, total: results.length });
639
+ });
640
+
641
+ /**
642
+ * Get template
643
+ */
644
+ router.get('/registry/templates/:templateId', (req, res) => {
645
+ const tmpl = templateRegistry.getTemplate(req.params.templateId);
646
+ if (!tmpl) return res.status(404).json({ error: 'Template not found' });
647
+ templateRegistry.trackDownload(req.params.templateId);
648
+ res.json(tmpl);
649
+ });
650
+
651
+ // ═══════════════════════════════════════════════════════════════════════════
652
+ // LLM
653
+ // ═══════════════════════════════════════════════════════════════════════════
654
+
655
+ /**
656
+ * LLM completion
657
+ */
658
+ router.post('/llm/complete', usageLimit('executionsPerDay'), async (req, res) => {
659
+ try {
660
+ const result = await llm.complete(req.body.prompt, req.body.options || req.body);
661
+ metrics.increment('llm.api.requests');
662
+ res.json(result);
663
+ } catch (err) {
664
+ res.status(500).json({ error: err.message });
665
+ }
666
+ });
667
+
668
+ /**
669
+ * LLM models
670
+ */
671
+ router.get('/llm/models', (req, res) => {
672
+ res.json({ models: llm.listModels() });
673
+ });
674
+
675
+ /**
676
+ * LLM status
677
+ */
678
+ router.get('/llm/status', (req, res) => {
679
+ res.json(llm.getStatus());
680
+ });
681
+
682
+ /**
683
+ * LLM embeddings
684
+ */
685
+ router.post('/llm/embed', async (req, res) => {
686
+ try {
687
+ const result = await llm.embed(req.body.text, req.body.options || {});
688
+ res.json(result);
689
+ } catch (err) {
690
+ res.status(500).json({ error: err.message });
691
+ }
692
+ });
693
+
694
+ // ═══════════════════════════════════════════════════════════════════════════
695
+ // COMMAND SIGNING
696
+ // ═══════════════════════════════════════════════════════════════════════════
697
+
698
+ /**
699
+ * Sign a command
700
+ */
701
+ router.post('/sign', (req, res) => {
702
+ const { payload, agentId } = req.body;
703
+ if (!payload || !agentId) return res.status(400).json({ error: 'payload and agentId required' });
704
+ const signature = signer.sign(payload, agentId);
705
+ res.json(signature);
706
+ });
707
+
708
+ /**
709
+ * Verify a signed command
710
+ */
711
+ router.post('/verify', (req, res) => {
712
+ const { payload, agentId, nonce, timestamp, signature } = req.body;
713
+ const result = signer.verify(payload, agentId, nonce, timestamp, signature);
714
+ res.json(result);
715
+ });
716
+
717
+ // ═══════════════════════════════════════════════════════════════════════════
718
+ // EVENT STREAM (SSE)
719
+ // ═══════════════════════════════════════════════════════════════════════════
720
+
721
+ /**
722
+ * Server-Sent Events for real-time updates
723
+ */
724
+ router.get('/events', (req, res) => {
725
+ res.writeHead(200, {
726
+ 'Content-Type': 'text/event-stream',
727
+ 'Cache-Control': 'no-cache',
728
+ 'Connection': 'keep-alive',
729
+ });
730
+
731
+ const filter = req.query.filter; // e.g., 'task.*' or 'agent.*'
732
+
733
+ const subId = bus.on(filter || '*', (data, meta) => {
734
+ res.write(`event: ${meta.event || 'message'}\n`);
735
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
736
+ });
737
+
738
+ req.on('close', () => {
739
+ bus.off(subId);
740
+ res.end();
741
+ });
742
+ });
743
+
744
+ // ═══════════════════════════════════════════════════════════════════════════
745
+ // Protocol Handler Setup
746
+ // ═══════════════════════════════════════════════════════════════════════════
747
+
748
+ const protocolHandler = new protocol.ProtocolHandler();
749
+
750
+ // Wire protocol commands to runtime
751
+ protocolHandler.handle('wab.discover', async (payload) => {
752
+ const commands = commandRegistry.search({ siteId: payload.siteId, category: payload.category });
753
+ return {
754
+ actions: commands.map(c => ({
755
+ name: c.name,
756
+ category: c.category,
757
+ params: c.input,
758
+ capabilities: c.capabilities,
759
+ })),
760
+ meta: {
761
+ protocol: protocol.PROTOCOL_VERSION,
762
+ timestamp: Date.now(),
763
+ },
764
+ };
765
+ });
766
+
767
+ protocolHandler.handle('wab.execute', async (payload, ctx) => {
768
+ const result = await executor.execute({
769
+ type: 'semantic',
770
+ domain: payload.domain || 'general',
771
+ action: payload.action,
772
+ params: payload.params,
773
+ agentId: ctx.message.agentId,
774
+ });
775
+ return result;
776
+ });
777
+
778
+ protocolHandler.handle('wab.task.submit', async (payload) => {
779
+ return runtime.submitTask(payload);
780
+ });
781
+
782
+ protocolHandler.handle('wab.task.status', async (payload) => {
783
+ return runtime.scheduler.getTask(payload.taskId);
784
+ });
785
+
786
+ protocolHandler.handle('wab.agent.register', async (payload) => {
787
+ const result = identity.register(payload.name, payload.type, {
788
+ capabilities: payload.capabilities,
789
+ publicKey: payload.publicKey,
790
+ metadata: payload.metadata,
791
+ });
792
+
793
+ // Negotiate requested capabilities
794
+ const negotiation = protocol.negotiator.negotiate(
795
+ result.agentId,
796
+ payload.capabilities,
797
+ payload.siteId || '*'
798
+ );
799
+
800
+ return {
801
+ agentId: result.agentId,
802
+ token: result.apiKey,
803
+ grantedCapabilities: negotiation.granted,
804
+ expiresAt: negotiation.grant?.constraints?.expiresAt || Date.now() + 3600_000,
805
+ };
806
+ });
807
+
808
+ protocolHandler.handle('wab.ai.infer', async (payload) => {
809
+ return llm.complete(payload.prompt, {
810
+ model: payload.model,
811
+ provider: payload.provider,
812
+ ...payload.options,
813
+ });
814
+ });
815
+
816
+ protocolHandler.handle('wab.commerce.compare', async (payload) => {
817
+ return executor.execute({
818
+ type: 'parallel',
819
+ tasks: (payload.sources || []).map(url => ({
820
+ type: 'extraction',
821
+ params: { url, query: payload.query },
822
+ })),
823
+ });
824
+ });
825
+
826
+ // ═══════════════════════════════════════════════════════════════════════════
827
+ // ADAPTERS
828
+ // ═══════════════════════════════════════════════════════════════════════════
829
+
830
+ /**
831
+ * List adapters
832
+ */
833
+ router.get('/adapters', (req, res) => {
834
+ res.json({ adapters: adapterManager.list() });
835
+ });
836
+
837
+ /**
838
+ * Adapter stats
839
+ */
840
+ router.get('/adapters/stats', (req, res) => {
841
+ res.json(adapterManager.getStats());
842
+ });
843
+
844
+ /**
845
+ * MCP: list tools
846
+ */
847
+ router.get('/adapters/mcp/tools', (req, res) => {
848
+ const commands = protocol.schema.listCommands();
849
+ res.json(mcpAdapter.handleListTools(commands));
850
+ });
851
+
852
+ /**
853
+ * MCP: call tool
854
+ */
855
+ router.post('/adapters/mcp/call', async (req, res) => {
856
+ try {
857
+ const result = await mcpAdapter.handleCallTool(req.body, async (wapReq) => {
858
+ const request = protocol.createRequest(wapReq.command, wapReq.payload);
859
+ return protocolHandler.process(request);
860
+ });
861
+ res.json(result);
862
+ } catch (err) {
863
+ res.status(500).json({ error: err.message });
864
+ }
865
+ });
866
+
867
+ /**
868
+ * REST adapter: register endpoint
869
+ */
870
+ router.post('/adapters/rest/endpoints', (req, res) => {
871
+ try {
872
+ const endpoint = restAdapter.registerEndpoint(req.body.id, req.body);
873
+ res.json(endpoint);
874
+ } catch (err) {
875
+ res.status(400).json({ error: err.message });
876
+ }
877
+ });
878
+
879
+ /**
880
+ * REST adapter: list endpoints
881
+ */
882
+ router.get('/adapters/rest/endpoints', (req, res) => {
883
+ res.json({ endpoints: restAdapter.listEndpoints() });
884
+ });
885
+
886
+ /**
887
+ * REST adapter: execute
888
+ */
889
+ router.post('/adapters/rest/execute', async (req, res) => {
890
+ try {
891
+ const result = await restAdapter.execute(req.body.endpoint, req.body.params);
892
+ res.json(result);
893
+ } catch (err) {
894
+ res.status(500).json({ error: err.message });
895
+ }
896
+ });
897
+
898
+ /**
899
+ * Browser adapter: list semantic mappings
900
+ */
901
+ router.get('/adapters/browser/mappings', (req, res) => {
902
+ res.json({ mappings: browserAdapter.listMappings() });
903
+ });
904
+
905
+ /**
906
+ * Browser adapter: resolve semantic action
907
+ */
908
+ router.post('/adapters/browser/resolve', (req, res) => {
909
+ const { domain, action, params } = req.body;
910
+ const plan = browserAdapter.fromWAP({ domain, action, params });
911
+ if (!plan) return res.status(404).json({ error: 'No mapping for this semantic action' });
912
+ res.json(plan);
913
+ });
914
+
915
+ /**
916
+ * Browser adapter: register mapping
917
+ */
918
+ router.post('/adapters/browser/mappings', (req, res) => {
919
+ const { domainAction, plan } = req.body;
920
+ if (!domainAction || !plan) return res.status(400).json({ error: 'domainAction and plan required' });
921
+ browserAdapter.registerMapping(domainAction, plan);
922
+ res.json({ success: true });
923
+ });
924
+
925
+ // ═══════════════════════════════════════════════════════════════════════════
926
+ // REPLAY ENGINE
927
+ // ═══════════════════════════════════════════════════════════════════════════
928
+
929
+ /**
930
+ * List recordings
931
+ */
932
+ router.get('/replay/recordings', (req, res) => {
933
+ res.json({ recordings: replayEngine.listRecordings(parseInt(req.query.limit) || 50) });
934
+ });
935
+
936
+ /**
937
+ * Get recording
938
+ */
939
+ router.get('/replay/recordings/:taskId', (req, res) => {
940
+ const rec = replayEngine.getRecording(req.params.taskId);
941
+ if (!rec) return res.status(404).json({ error: 'Recording not found' });
942
+ res.json(rec);
943
+ });
944
+
945
+ /**
946
+ * Replay a task
947
+ */
948
+ router.post('/replay/:taskId', async (req, res) => {
949
+ try {
950
+ const result = await replayEngine.replay(req.params.taskId, {
951
+ verify: req.body.verify !== false,
952
+ continueOnMismatch: !!req.body.continueOnMismatch,
953
+ });
954
+ res.json(result);
955
+ } catch (err) {
956
+ res.status(400).json({ error: err.message });
957
+ }
958
+ });
959
+
960
+ /**
961
+ * Diff two recordings
962
+ */
963
+ router.get('/replay/diff/:taskId1/:taskId2', (req, res) => {
964
+ const diff = replayEngine.diff(req.params.taskId1, req.params.taskId2);
965
+ if (!diff) return res.status(404).json({ error: 'One or both recordings not found' });
966
+ res.json(diff);
967
+ });
968
+
969
+ /**
970
+ * Replay stats
971
+ */
972
+ router.get('/replay/stats', (req, res) => {
973
+ res.json(replayEngine.getStats());
974
+ });
975
+
976
+ // ═══════════════════════════════════════════════════════════════════════════
977
+ // SESSION ENGINE
978
+ // ═══════════════════════════════════════════════════════════════════════════
979
+
980
+ /**
981
+ * Create browser session
982
+ */
983
+ router.post('/sessions', (req, res) => {
984
+ const session = sessionEngine.create(req.body);
985
+ res.json(session);
986
+ });
987
+
988
+ /**
989
+ * List sessions
990
+ */
991
+ router.get('/sessions', (req, res) => {
992
+ const sessions = sessionEngine.list({
993
+ agentId: req.query.agentId,
994
+ siteId: req.query.siteId,
995
+ state: req.query.state,
996
+ }, parseInt(req.query.limit) || 50);
997
+ res.json({ sessions, total: sessions.length });
998
+ });
999
+
1000
+ /**
1001
+ * Get session
1002
+ */
1003
+ router.get('/sessions/:sessionId', (req, res) => {
1004
+ const session = sessionEngine.get(req.params.sessionId);
1005
+ if (!session) return res.status(404).json({ error: 'Session not found or expired' });
1006
+ res.json(session);
1007
+ });
1008
+
1009
+ /**
1010
+ * Export session
1011
+ */
1012
+ router.get('/sessions/:sessionId/export', (req, res) => {
1013
+ const data = sessionEngine.export(req.params.sessionId);
1014
+ if (!data) return res.status(404).json({ error: 'Session not found' });
1015
+ res.json(data);
1016
+ });
1017
+
1018
+ /**
1019
+ * Import session
1020
+ */
1021
+ router.post('/sessions/import', (req, res) => {
1022
+ const session = sessionEngine.import(req.body);
1023
+ res.json(session);
1024
+ });
1025
+
1026
+ /**
1027
+ * Set cookies
1028
+ */
1029
+ router.post('/sessions/:sessionId/cookies', (req, res) => {
1030
+ sessionEngine.setCookies(req.params.sessionId, req.body.cookies || []);
1031
+ res.json({ success: true });
1032
+ });
1033
+
1034
+ /**
1035
+ * Get cookies
1036
+ */
1037
+ router.get('/sessions/:sessionId/cookies', (req, res) => {
1038
+ const cookies = sessionEngine.getCookies(req.params.sessionId, req.query.domain);
1039
+ res.json({ cookies });
1040
+ });
1041
+
1042
+ /**
1043
+ * Set storage
1044
+ */
1045
+ router.post('/sessions/:sessionId/storage', (req, res) => {
1046
+ const { key, value, type } = req.body;
1047
+ sessionEngine.setStorage(req.params.sessionId, key, value, type);
1048
+ res.json({ success: true });
1049
+ });
1050
+
1051
+ /**
1052
+ * Destroy session
1053
+ */
1054
+ router.delete('/sessions/:sessionId', (req, res) => {
1055
+ sessionEngine.destroy(req.params.sessionId);
1056
+ res.json({ success: true });
1057
+ });
1058
+
1059
+ // ═══════════════════════════════════════════════════════════════════════════
1060
+ // FAILURE ANALYSIS
1061
+ // ═══════════════════════════════════════════════════════════════════════════
1062
+
1063
+ /**
1064
+ * Query failures
1065
+ */
1066
+ router.get('/failures', (req, res) => {
1067
+ const failures = failureAnalyzer.query({
1068
+ classification: req.query.classification,
1069
+ severity: req.query.severity,
1070
+ agentId: req.query.agentId,
1071
+ taskId: req.query.taskId,
1072
+ retryable: req.query.retryable === 'true' ? true : req.query.retryable === 'false' ? false : undefined,
1073
+ since: parseInt(req.query.since) || undefined,
1074
+ }, parseInt(req.query.limit) || 50);
1075
+ res.json({ failures, total: failures.length });
1076
+ });
1077
+
1078
+ /**
1079
+ * Get failure
1080
+ */
1081
+ router.get('/failures/:failureId', (req, res) => {
1082
+ const failure = failureAnalyzer.getFailure(req.params.failureId);
1083
+ if (!failure) return res.status(404).json({ error: 'Failure not found' });
1084
+ res.json(failure);
1085
+ });
1086
+
1087
+ /**
1088
+ * Get failure patterns
1089
+ */
1090
+ router.get('/failures/analysis/patterns', (req, res) => {
1091
+ res.json({ patterns: failureAnalyzer.getPatterns() });
1092
+ });
1093
+
1094
+ /**
1095
+ * Get failure summary
1096
+ */
1097
+ router.get('/failures/analysis/summary', (req, res) => {
1098
+ res.json(failureAnalyzer.getSummary(parseInt(req.query.since) || 0));
1099
+ });
1100
+
1101
+ /**
1102
+ * Classify a failure manually
1103
+ */
1104
+ router.post('/failures/classify', (req, res) => {
1105
+ const { error, context } = req.body;
1106
+ if (!error) return res.status(400).json({ error: 'error object required' });
1107
+ const classification = failureAnalyzer.classify(error, context || {});
1108
+ res.json(classification);
1109
+ });
1110
+
1111
+ // ═══════════════════════════════════════════════════════════════════════════
1112
+ // CERTIFICATION
1113
+ // ═══════════════════════════════════════════════════════════════════════════
1114
+
1115
+ /**
1116
+ * Verify a site
1117
+ */
1118
+ router.post('/certification/verify', async (req, res) => {
1119
+ try {
1120
+ const { domain, probeData } = req.body;
1121
+ if (!domain) return res.status(400).json({ error: 'domain required' });
1122
+ const result = await certificationEngine.verify(domain, probeData || {});
1123
+ res.json(result);
1124
+ } catch (err) {
1125
+ res.status(500).json({ error: err.message });
1126
+ }
1127
+ });
1128
+
1129
+ /**
1130
+ * Get certificate
1131
+ */
1132
+ router.get('/certification/:domain', (req, res) => {
1133
+ const cert = certificationEngine.getCertificate(req.params.domain);
1134
+ if (!cert) return res.status(404).json({ error: 'No active certificate for this domain' });
1135
+ res.json(cert);
1136
+ });
1137
+
1138
+ /**
1139
+ * List certificates
1140
+ */
1141
+ router.get('/certification', (req, res) => {
1142
+ const certs = certificationEngine.listCertificates({
1143
+ level: req.query.level,
1144
+ minScore: parseInt(req.query.minScore) || undefined,
1145
+ }, parseInt(req.query.limit) || 50);
1146
+ res.json({ certificates: certs, total: certs.length });
1147
+ });
1148
+
1149
+ /**
1150
+ * Revoke certificate
1151
+ */
1152
+ router.delete('/certification/:domain', (req, res) => {
1153
+ certificationEngine.revoke(req.params.domain);
1154
+ res.json({ success: true });
1155
+ });
1156
+
1157
+ // ═══════════════════════════════════════════════════════════════════════════
1158
+ // PLANS & PRICING
1159
+ // ═══════════════════════════════════════════════════════════════════════════
1160
+
1161
+ /**
1162
+ * List available plans
1163
+ */
1164
+ router.get('/plans', (req, res) => {
1165
+ const plans = listPlans().map(p => ({
1166
+ id: p.id,
1167
+ name: p.name,
1168
+ price: p.price,
1169
+ interval: p.interval,
1170
+ description: p.description,
1171
+ limits: p.limits,
1172
+ features: Object.entries(p.features)
1173
+ .filter(([, v]) => v === true)
1174
+ .map(([k]) => k),
1175
+ }));
1176
+ res.json({ plans, usagePricing: USAGE_PRICING });
1177
+ });
1178
+
1179
+ /**
1180
+ * Get specific plan details
1181
+ */
1182
+ router.get('/plans/:planId', (req, res) => {
1183
+ const plan = getPlan(req.params.planId);
1184
+ if (!plan || plan.id === 'free' && req.params.planId !== 'free') {
1185
+ return res.status(404).json({ error: 'Plan not found' });
1186
+ }
1187
+ res.json(plan);
1188
+ });
1189
+
1190
+ // ═══════════════════════════════════════════════════════════════════════════
1191
+ // USAGE METERING
1192
+ // ═══════════════════════════════════════════════════════════════════════════
1193
+
1194
+ /**
1195
+ * Get usage for current agent
1196
+ */
1197
+ router.get('/usage', (req, res) => {
1198
+ const entityId = req.agentId || req.ip;
1199
+ const tier = req.agentTier || req.session?.tier || 'free';
1200
+ res.json(metering.getUsage(entityId, tier));
1201
+ });
1202
+
1203
+ /**
1204
+ * Get billing summary (overages)
1205
+ */
1206
+ router.get('/usage/billing', (req, res) => {
1207
+ const entityId = req.agentId || req.ip;
1208
+ res.json(metering.getBillingSummary(entityId));
1209
+ });
1210
+
1211
+ /**
1212
+ * Get metering stats (admin)
1213
+ */
1214
+ router.get('/usage/stats', (req, res) => {
1215
+ res.json(metering.getStats());
1216
+ });
1217
+
1218
+ // ═══════════════════════════════════════════════════════════════════════════
1219
+ // MARKETPLACE
1220
+ // ═══════════════════════════════════════════════════════════════════════════
1221
+
1222
+ /**
1223
+ * Search marketplace
1224
+ */
1225
+ router.get('/marketplace', (req, res) => {
1226
+ const listings = marketplace.search({
1227
+ type: req.query.type,
1228
+ category: req.query.category,
1229
+ query: req.query.q,
1230
+ tag: req.query.tag,
1231
+ free: req.query.free === 'true',
1232
+ paid: req.query.paid === 'true',
1233
+ minRating: req.query.minRating ? parseFloat(req.query.minRating) : undefined,
1234
+ sortBy: req.query.sortBy,
1235
+ }, parseInt(req.query.limit) || 50);
1236
+ res.json({ listings, total: listings.length });
1237
+ });
1238
+
1239
+ /**
1240
+ * Get listing
1241
+ */
1242
+ router.get('/marketplace/:listingId', (req, res) => {
1243
+ const listing = marketplace.getListing(req.params.listingId);
1244
+ if (!listing) return res.status(404).json({ error: 'Listing not found' });
1245
+ res.json(listing);
1246
+ });
1247
+
1248
+ /**
1249
+ * Get reviews
1250
+ */
1251
+ router.get('/marketplace/:listingId/reviews', (req, res) => {
1252
+ res.json({ reviews: marketplace.getReviews(req.params.listingId) });
1253
+ });
1254
+
1255
+ /**
1256
+ * Publish listing
1257
+ */
1258
+ router.post('/marketplace/publish', (req, res) => {
1259
+ try {
1260
+ const listing = marketplace.publish({
1261
+ ...req.body,
1262
+ sellerId: req.agentId || req.body.sellerId,
1263
+ });
1264
+ res.json(listing);
1265
+ } catch (err) {
1266
+ res.status(400).json({ error: err.message });
1267
+ }
1268
+ });
1269
+
1270
+ /**
1271
+ * Purchase/install listing
1272
+ */
1273
+ router.post('/marketplace/:listingId/purchase', (req, res) => {
1274
+ try {
1275
+ const buyerId = req.agentId || req.body.buyerId;
1276
+ if (!buyerId) return res.status(400).json({ error: 'buyerId required' });
1277
+ const purchase = marketplace.purchase(req.params.listingId, buyerId);
1278
+ res.json(purchase);
1279
+ } catch (err) {
1280
+ res.status(400).json({ error: err.message });
1281
+ }
1282
+ });
1283
+
1284
+ /**
1285
+ * Add review
1286
+ */
1287
+ router.post('/marketplace/:listingId/review', (req, res) => {
1288
+ try {
1289
+ const review = marketplace.addReview(req.params.listingId, {
1290
+ userId: req.agentId || req.body.userId,
1291
+ rating: req.body.rating,
1292
+ comment: req.body.comment,
1293
+ });
1294
+ res.json(review);
1295
+ } catch (err) {
1296
+ res.status(400).json({ error: err.message });
1297
+ }
1298
+ });
1299
+
1300
+ /**
1301
+ * Get my purchases
1302
+ */
1303
+ router.get('/marketplace/my/purchases', (req, res) => {
1304
+ const buyerId = req.agentId || req.query.buyerId;
1305
+ res.json({ purchases: marketplace.getPurchases(buyerId) });
1306
+ });
1307
+
1308
+ /**
1309
+ * Get seller earnings
1310
+ */
1311
+ router.get('/marketplace/my/earnings', (req, res) => {
1312
+ const sellerId = req.agentId || req.query.sellerId;
1313
+ res.json(marketplace.getEarnings(sellerId));
1314
+ });
1315
+
1316
+ /**
1317
+ * Admin: pending listings
1318
+ */
1319
+ router.get('/marketplace/admin/pending', (req, res) => {
1320
+ res.json({ listings: marketplace.getPendingListings() });
1321
+ });
1322
+
1323
+ /**
1324
+ * Admin: approve listing
1325
+ */
1326
+ router.post('/marketplace/admin/:listingId/approve', (req, res) => {
1327
+ try {
1328
+ const listing = marketplace.approve(req.params.listingId);
1329
+ res.json(listing);
1330
+ } catch (err) {
1331
+ res.status(400).json({ error: err.message });
1332
+ }
1333
+ });
1334
+
1335
+ /**
1336
+ * Admin: reject listing
1337
+ */
1338
+ router.post('/marketplace/admin/:listingId/reject', (req, res) => {
1339
+ try {
1340
+ const listing = marketplace.reject(req.params.listingId, req.body.reason);
1341
+ res.json(listing);
1342
+ } catch (err) {
1343
+ res.status(400).json({ error: err.message });
1344
+ }
1345
+ });
1346
+
1347
+ /**
1348
+ * Marketplace stats
1349
+ */
1350
+ router.get('/marketplace/stats', (req, res) => {
1351
+ res.json(marketplace.getStats());
1352
+ });
1353
+
1354
+ // ═══════════════════════════════════════════════════════════════════════════
1355
+ // HOSTED RUNTIME
1356
+ // ═══════════════════════════════════════════════════════════════════════════
1357
+
1358
+ /**
1359
+ * Launch hosted instance
1360
+ */
1361
+ router.post('/hosted/launch', (req, res) => {
1362
+ try {
1363
+ const instance = hostedRuntime.launch({
1364
+ agentId: req.agentId || req.body.agentId,
1365
+ tier: req.agentTier || req.session?.tier || 'starter',
1366
+ region: req.body.region,
1367
+ cpu: req.body.cpu,
1368
+ memory: req.body.memory,
1369
+ timeout: req.body.timeout,
1370
+ });
1371
+ res.json(instance);
1372
+ } catch (err) {
1373
+ res.status(400).json({ error: err.message });
1374
+ }
1375
+ });
1376
+
1377
+ /**
1378
+ * Execute on hosted instance
1379
+ */
1380
+ router.post('/hosted/:instanceId/execute', async (req, res) => {
1381
+ try {
1382
+ const execution = await hostedRuntime.execute(req.params.instanceId, req.body);
1383
+ res.json(execution);
1384
+ } catch (err) {
1385
+ res.status(400).json({ error: err.message });
1386
+ }
1387
+ });
1388
+
1389
+ /**
1390
+ * Complete execution
1391
+ */
1392
+ router.post('/hosted/executions/:executionId/complete', (req, res) => {
1393
+ const execution = hostedRuntime.completeExecution(
1394
+ req.params.executionId,
1395
+ req.body.result,
1396
+ req.body.error ? new Error(req.body.error) : null
1397
+ );
1398
+ if (!execution) return res.status(404).json({ error: 'Execution not found' });
1399
+ res.json(execution);
1400
+ });
1401
+
1402
+ /**
1403
+ * Stop hosted instance
1404
+ */
1405
+ router.post('/hosted/:instanceId/stop', (req, res) => {
1406
+ const success = hostedRuntime.stop(req.params.instanceId);
1407
+ res.json({ success });
1408
+ });
1409
+
1410
+ /**
1411
+ * Get hosted instance
1412
+ */
1413
+ router.get('/hosted/:instanceId', (req, res) => {
1414
+ const instance = hostedRuntime.getInstance(req.params.instanceId);
1415
+ if (!instance) return res.status(404).json({ error: 'Instance not found' });
1416
+ res.json(instance);
1417
+ });
1418
+
1419
+ /**
1420
+ * List instances
1421
+ */
1422
+ router.get('/hosted', (req, res) => {
1423
+ const instances = hostedRuntime.listInstances({
1424
+ agentId: req.query.agentId,
1425
+ status: req.query.status,
1426
+ region: req.query.region,
1427
+ }, parseInt(req.query.limit) || 50);
1428
+ res.json({ instances, total: instances.length });
1429
+ });
1430
+
1431
+ /**
1432
+ * List executions for instance
1433
+ */
1434
+ router.get('/hosted/:instanceId/executions', (req, res) => {
1435
+ const executions = hostedRuntime.listExecutions(
1436
+ req.params.instanceId,
1437
+ parseInt(req.query.limit) || 50
1438
+ );
1439
+ res.json({ executions, total: executions.length });
1440
+ });
1441
+
1442
+ /**
1443
+ * Get compute usage
1444
+ */
1445
+ router.get('/hosted/usage/:agentId', (req, res) => {
1446
+ res.json(hostedRuntime.getComputeUsage(req.params.agentId));
1447
+ });
1448
+
1449
+ /**
1450
+ * Hosted runtime stats
1451
+ */
1452
+ router.get('/hosted/stats', (req, res) => {
1453
+ res.json(hostedRuntime.getStats());
1454
+ });
1455
+
1456
+ // ═══════════════════════════════════════════════════════════════════════════
1457
+ // LOCAL VISION ENGINE (Self-contained — no external API)
1458
+ // ═══════════════════════════════════════════════════════════════════════════
1459
+
1460
+ /**
1461
+ * Analyze page DOM locally (no external API calls)
1462
+ */
1463
+ router.post('/vision/analyze-dom', async (req, res) => {
1464
+ try {
1465
+ const siteId = req.body.siteId || req.agentId || 'default';
1466
+ const result = await vision.analyzePageDOM(siteId, {
1467
+ domSnapshot: req.body.domSnapshot,
1468
+ url: req.body.url,
1469
+ });
1470
+ res.json(result);
1471
+ } catch (err) {
1472
+ res.status(400).json({ error: err.message });
1473
+ }
1474
+ });
1475
+
1476
+ /**
1477
+ * Get DOM extraction script to inject into pages
1478
+ */
1479
+ router.get('/vision/extraction-script', (req, res) => {
1480
+ res.json({ script: vision.getDomExtractionScript() });
1481
+ });
1482
+
1483
+ /**
1484
+ * Find elements in cached vision data
1485
+ */
1486
+ router.get('/vision/elements', (req, res) => {
1487
+ const siteId = req.query.siteId || req.agentId || 'default';
1488
+ const results = vision.findElement(siteId, req.query.url, {
1489
+ description: req.query.q,
1490
+ type: req.query.type,
1491
+ label: req.query.label,
1492
+ });
1493
+ res.json({ elements: results, total: results.length });
1494
+ });
1495
+
1496
+ /**
1497
+ * Vision history
1498
+ */
1499
+ router.get('/vision/history', (req, res) => {
1500
+ const siteId = req.query.siteId || req.agentId || 'default';
1501
+ const history = vision.getVisionHistory(siteId, {
1502
+ limit: parseInt(req.query.limit) || 50,
1503
+ url: req.query.url,
1504
+ });
1505
+ res.json({ history, total: history.length });
1506
+ });
1507
+
1508
+ /**
1509
+ * Supported vision models
1510
+ */
1511
+ router.get('/vision/models', (req, res) => {
1512
+ res.json({ models: vision.getSupportedModels() });
1513
+ });
1514
+
1515
+ // ═══════════════════════════════════════════════════════════════════════════
1516
+ // LEARNING FROM DEMONSTRATION (LfD)
1517
+ // ═══════════════════════════════════════════════════════════════════════════
1518
+
1519
+ /**
1520
+ * Start a recording session
1521
+ */
1522
+ router.post('/lfd/record', (req, res) => {
1523
+ try {
1524
+ const session = lfdEngine.startRecording({
1525
+ name: req.body.name,
1526
+ description: req.body.description,
1527
+ agentId: req.agentId || req.body.agentId,
1528
+ startUrl: req.body.startUrl,
1529
+ tags: req.body.tags,
1530
+ });
1531
+ res.json(session);
1532
+ } catch (err) {
1533
+ res.status(400).json({ error: err.message });
1534
+ }
1535
+ });
1536
+
1537
+ /**
1538
+ * Record events into a session
1539
+ */
1540
+ router.post('/lfd/:sessionId/events', (req, res) => {
1541
+ try {
1542
+ const events = req.body.events || [req.body];
1543
+ const results = events.map(evt => lfdEngine.recordEvent(req.params.sessionId, evt));
1544
+ res.json({ recorded: results.filter(Boolean).length });
1545
+ } catch (err) {
1546
+ res.status(400).json({ error: err.message });
1547
+ }
1548
+ });
1549
+
1550
+ /**
1551
+ * Record a DOM snapshot
1552
+ */
1553
+ router.post('/lfd/:sessionId/snapshot', (req, res) => {
1554
+ try {
1555
+ lfdEngine.recordSnapshot(req.params.sessionId, req.body);
1556
+ res.json({ success: true });
1557
+ } catch (err) {
1558
+ res.status(400).json({ error: err.message });
1559
+ }
1560
+ });
1561
+
1562
+ /**
1563
+ * Pause recording
1564
+ */
1565
+ router.post('/lfd/:sessionId/pause', (req, res) => {
1566
+ try { res.json(lfdEngine.pauseRecording(req.params.sessionId)); }
1567
+ catch (err) { res.status(400).json({ error: err.message }); }
1568
+ });
1569
+
1570
+ /**
1571
+ * Resume recording
1572
+ */
1573
+ router.post('/lfd/:sessionId/resume', (req, res) => {
1574
+ try { res.json(lfdEngine.resumeRecording(req.params.sessionId)); }
1575
+ catch (err) { res.status(400).json({ error: err.message }); }
1576
+ });
1577
+
1578
+ /**
1579
+ * Stop recording and generate recipe
1580
+ */
1581
+ router.post('/lfd/:sessionId/stop', (req, res) => {
1582
+ try { res.json(lfdEngine.stopRecording(req.params.sessionId)); }
1583
+ catch (err) { res.status(400).json({ error: err.message }); }
1584
+ });
1585
+
1586
+ /**
1587
+ * Cancel recording
1588
+ */
1589
+ router.post('/lfd/:sessionId/cancel', (req, res) => {
1590
+ try { res.json(lfdEngine.cancelRecording(req.params.sessionId)); }
1591
+ catch (err) { res.status(400).json({ error: err.message }); }
1592
+ });
1593
+
1594
+ /**
1595
+ * Get recording details
1596
+ */
1597
+ router.get('/lfd/:sessionId', (req, res) => {
1598
+ const recording = lfdEngine.getRecording(req.params.sessionId);
1599
+ if (!recording) return res.status(404).json({ error: 'Recording not found' });
1600
+ res.json(recording);
1601
+ });
1602
+
1603
+ /**
1604
+ * List recordings
1605
+ */
1606
+ router.get('/lfd', (req, res) => {
1607
+ res.json({ recordings: lfdEngine.listRecordings(parseInt(req.query.limit) || 50) });
1608
+ });
1609
+
1610
+ /**
1611
+ * Get recording script to inject into pages
1612
+ */
1613
+ router.get('/lfd/:sessionId/script', (req, res) => {
1614
+ const serverUrl = `${req.protocol}://${req.get('host')}`;
1615
+ res.json({ script: lfdEngine.getRecordingScript(req.params.sessionId, serverUrl) });
1616
+ });
1617
+
1618
+ // ── Recipes ──
1619
+
1620
+ /**
1621
+ * List recipes
1622
+ */
1623
+ router.get('/recipes', (req, res) => {
1624
+ const recipes = lfdEngine.listRecipes({
1625
+ domain: req.query.domain,
1626
+ tag: req.query.tag,
1627
+ query: req.query.q,
1628
+ }, parseInt(req.query.limit) || 50);
1629
+ res.json({ recipes, total: recipes.length });
1630
+ });
1631
+
1632
+ /**
1633
+ * Get recipe
1634
+ */
1635
+ router.get('/recipes/:recipeId', (req, res) => {
1636
+ const recipe = lfdEngine.getRecipe(req.params.recipeId);
1637
+ if (!recipe) return res.status(404).json({ error: 'Recipe not found' });
1638
+ res.json(recipe);
1639
+ });
1640
+
1641
+ /**
1642
+ * Save/import recipe manually
1643
+ */
1644
+ router.post('/recipes', (req, res) => {
1645
+ try { res.json(lfdEngine.saveRecipe(req.body)); }
1646
+ catch (err) { res.status(400).json({ error: err.message }); }
1647
+ });
1648
+
1649
+ /**
1650
+ * Delete recipe
1651
+ */
1652
+ router.delete('/recipes/:recipeId', (req, res) => {
1653
+ const deleted = lfdEngine.deleteRecipe(req.params.recipeId);
1654
+ res.json({ deleted });
1655
+ });
1656
+
1657
+ /**
1658
+ * Execute a recipe
1659
+ */
1660
+ router.post('/recipes/:recipeId/execute', (req, res) => {
1661
+ try {
1662
+ const execution = lfdEngine.executeRecipe(req.params.recipeId, {
1663
+ variables: req.body.variables,
1664
+ speed: req.body.speed,
1665
+ stopOnError: req.body.stopOnError,
1666
+ skipWaits: req.body.skipWaits,
1667
+ humanInTheLoop: req.body.humanInTheLoop,
1668
+ });
1669
+ res.json(execution);
1670
+ } catch (err) {
1671
+ res.status(400).json({ error: err.message });
1672
+ }
1673
+ });
1674
+
1675
+ /**
1676
+ * Get next step in execution
1677
+ */
1678
+ router.get('/executions/:executionId/next', (req, res) => {
1679
+ const step = lfdEngine.getNextStep(req.params.executionId);
1680
+ if (!step) {
1681
+ const exec = lfdEngine.getExecution(req.params.executionId);
1682
+ return res.json({ done: true, status: exec?.status || 'unknown' });
1683
+ }
1684
+ res.json(step);
1685
+ });
1686
+
1687
+ /**
1688
+ * Report step result
1689
+ */
1690
+ router.post('/executions/:executionId/steps/:stepIndex', (req, res) => {
1691
+ const exec = lfdEngine.reportStep(
1692
+ req.params.executionId,
1693
+ parseInt(req.params.stepIndex),
1694
+ { success: req.body.success, error: req.body.error, duration: req.body.duration, selectorUsed: req.body.selectorUsed }
1695
+ );
1696
+ if (!exec) return res.status(404).json({ error: 'Execution not found' });
1697
+ res.json({ status: exec.status, currentStep: exec.currentStep, totalSteps: exec.totalSteps });
1698
+ });
1699
+
1700
+ /**
1701
+ * Pause execution
1702
+ */
1703
+ router.post('/executions/:executionId/pause', (req, res) => {
1704
+ const exec = lfdEngine.pauseExecution(req.params.executionId);
1705
+ if (!exec) return res.status(404).json({ error: 'Execution not found' });
1706
+ res.json({ status: exec.status });
1707
+ });
1708
+
1709
+ /**
1710
+ * Resume execution
1711
+ */
1712
+ router.post('/executions/:executionId/resume', (req, res) => {
1713
+ const exec = lfdEngine.resumeExecution(req.params.executionId);
1714
+ if (!exec) return res.status(404).json({ error: 'Execution not found' });
1715
+ res.json({ status: exec.status });
1716
+ });
1717
+
1718
+ /**
1719
+ * Abort execution
1720
+ */
1721
+ router.post('/executions/:executionId/abort', (req, res) => {
1722
+ const exec = lfdEngine.abortExecution(req.params.executionId);
1723
+ if (!exec) return res.status(404).json({ error: 'Execution not found' });
1724
+ res.json({ status: exec.status });
1725
+ });
1726
+
1727
+ /**
1728
+ * Get execution details
1729
+ */
1730
+ router.get('/executions/:executionId', (req, res) => {
1731
+ const exec = lfdEngine.getExecution(req.params.executionId);
1732
+ if (!exec) return res.status(404).json({ error: 'Execution not found' });
1733
+ res.json(exec);
1734
+ });
1735
+
1736
+ /**
1737
+ * List executions
1738
+ */
1739
+ router.get('/executions', (req, res) => {
1740
+ res.json({ executions: lfdEngine.listExecutions(parseInt(req.query.limit) || 50) });
1741
+ });
1742
+
1743
+ /**
1744
+ * LfD stats
1745
+ */
1746
+ router.get('/lfd/stats', (req, res) => {
1747
+ res.json(lfdEngine.getStats());
1748
+ });
1749
+
1750
+ // ═══════════════════════════════════════════════════════════════════════════
1751
+ // CLUSTER — DISTRIBUTED EXECUTION & WORKER NODES
1752
+ // ═══════════════════════════════════════════════════════════════════════════
1753
+
1754
+ /**
1755
+ * Get cluster status (public)
1756
+ */
1757
+ router.get('/cluster/status', (req, res) => {
1758
+ res.json(cluster.getClusterStatus());
1759
+ });
1760
+
1761
+ /**
1762
+ * Register a worker node
1763
+ */
1764
+ router.post('/cluster/nodes', (req, res) => {
1765
+ try {
1766
+ const result = cluster.registerNode({
1767
+ name: req.body.name,
1768
+ endpoint: req.body.endpoint,
1769
+ region: req.body.region,
1770
+ zone: req.body.zone,
1771
+ role: req.body.role,
1772
+ capacity: req.body.capacity,
1773
+ tags: req.body.tags,
1774
+ hardware: req.body.hardware,
1775
+ version: req.body.version,
1776
+ secret: req.body.secret,
1777
+ });
1778
+ res.json(result);
1779
+ } catch (err) {
1780
+ res.status(400).json({ error: err.message });
1781
+ }
1782
+ });
1783
+
1784
+ /**
1785
+ * List cluster nodes
1786
+ */
1787
+ router.get('/cluster/nodes', (req, res) => {
1788
+ const nodes = cluster.listNodes({
1789
+ region: req.query.region,
1790
+ active: req.query.active === 'true',
1791
+ });
1792
+ res.json({ nodes });
1793
+ });
1794
+
1795
+ /**
1796
+ * Get a specific node
1797
+ */
1798
+ router.get('/cluster/nodes/:nodeId', (req, res) => {
1799
+ const node = cluster.getNode(req.params.nodeId);
1800
+ if (!node) return res.status(404).json({ error: 'Node not found' });
1801
+ res.json(node);
1802
+ });
1803
+
1804
+ /**
1805
+ * Remove a node
1806
+ */
1807
+ router.delete('/cluster/nodes/:nodeId', (req, res) => {
1808
+ const result = cluster.deregisterNode(req.params.nodeId);
1809
+ if (!result) return res.status(404).json({ error: 'Node not found' });
1810
+ res.json(result);
1811
+ });
1812
+
1813
+ /**
1814
+ * Worker heartbeat
1815
+ */
1816
+ router.post('/cluster/nodes/:nodeId/heartbeat', (req, res) => {
1817
+ const result = cluster.heartbeat(req.params.nodeId, {
1818
+ capacityUsed: req.body.capacityUsed,
1819
+ capacityTotal: req.body.capacityTotal,
1820
+ hardware: req.body.hardware,
1821
+ tags: req.body.tags,
1822
+ version: req.body.version,
1823
+ });
1824
+ if (!result) return res.status(404).json({ error: 'Node not found' });
1825
+ res.json(result);
1826
+ });
1827
+
1828
+ /**
1829
+ * Drain a node (stop new tasks, wait for running)
1830
+ */
1831
+ router.post('/cluster/nodes/:nodeId/drain', (req, res) => {
1832
+ const result = cluster.drainNode(req.params.nodeId);
1833
+ if (!result) return res.status(404).json({ error: 'Node not found' });
1834
+ res.json(result);
1835
+ });
1836
+
1837
+ /**
1838
+ * Cordon a node (prevent scheduling)
1839
+ */
1840
+ router.post('/cluster/nodes/:nodeId/cordon', (req, res) => {
1841
+ const result = cluster.cordonNode(req.params.nodeId);
1842
+ if (!result) return res.status(404).json({ error: 'Node not found' });
1843
+ res.json(result);
1844
+ });
1845
+
1846
+ /**
1847
+ * Uncordon a node (allow scheduling again)
1848
+ */
1849
+ router.post('/cluster/nodes/:nodeId/uncordon', (req, res) => {
1850
+ const result = cluster.uncordonNode(req.params.nodeId);
1851
+ if (!result) return res.status(404).json({ error: 'Node not found' });
1852
+ res.json(result);
1853
+ });
1854
+
1855
+ /**
1856
+ * Submit a task for distributed execution
1857
+ */
1858
+ router.post('/cluster/tasks', (req, res) => {
1859
+ try {
1860
+ const result = distributor.submit({
1861
+ type: req.body.type,
1862
+ objective: req.body.objective,
1863
+ params: req.body.params,
1864
+ priority: req.body.priority,
1865
+ affinityTags: req.body.affinityTags,
1866
+ affinityRegion: req.body.affinityRegion,
1867
+ timeout: req.body.timeout,
1868
+ maxAttempts: req.body.maxAttempts,
1869
+ externalId: req.body.externalId,
1870
+ });
1871
+ res.json(result);
1872
+ } catch (err) {
1873
+ res.status(400).json({ error: err.message });
1874
+ }
1875
+ });
1876
+
1877
+ /**
1878
+ * Get task details
1879
+ */
1880
+ router.get('/cluster/tasks/:taskId', (req, res) => {
1881
+ const task = cluster.getTask(req.params.taskId);
1882
+ if (!task) return res.status(404).json({ error: 'Task not found' });
1883
+ res.json(task);
1884
+ });
1885
+
1886
+ /**
1887
+ * List tasks
1888
+ */
1889
+ router.get('/cluster/tasks', (req, res) => {
1890
+ const tasks = cluster.listTasks({
1891
+ status: req.query.status,
1892
+ nodeId: req.query.nodeId,
1893
+ limit: parseInt(req.query.limit) || 50,
1894
+ });
1895
+ res.json({ tasks });
1896
+ });
1897
+
1898
+ /**
1899
+ * Worker pulls tasks (poll-based)
1900
+ */
1901
+ router.post('/cluster/nodes/:nodeId/pull', (req, res) => {
1902
+ const tasks = distributor.pullTasks(req.params.nodeId, parseInt(req.body.limit) || 5);
1903
+ res.json({ tasks });
1904
+ });
1905
+
1906
+ /**
1907
+ * Worker reports task started
1908
+ */
1909
+ router.post('/cluster/tasks/:taskId/started', (req, res) => {
1910
+ const result = cluster.reportTaskStarted(req.params.taskId);
1911
+ if (!result) return res.status(404).json({ error: 'Task not found' });
1912
+ res.json(result);
1913
+ });
1914
+
1915
+ /**
1916
+ * Worker reports task completed
1917
+ */
1918
+ router.post('/cluster/tasks/:taskId/completed', (req, res) => {
1919
+ const result = cluster.reportTaskCompleted(req.params.taskId, req.body.result);
1920
+ if (!result) return res.status(404).json({ error: 'Task not found' });
1921
+ res.json(result);
1922
+ });
1923
+
1924
+ /**
1925
+ * Worker reports task failed
1926
+ */
1927
+ router.post('/cluster/tasks/:taskId/failed', (req, res) => {
1928
+ const result = cluster.reportTaskFailed(req.params.taskId, req.body.error);
1929
+ if (!result) return res.status(404).json({ error: 'Task not found' });
1930
+ res.json(result);
1931
+ });
1932
+
1933
+ /**
1934
+ * Get cluster events log
1935
+ */
1936
+ router.get('/cluster/events', (req, res) => {
1937
+ const events = cluster.getEvents(
1938
+ parseInt(req.query.limit) || 100,
1939
+ req.query.nodeId || null
1940
+ );
1941
+ res.json({ events });
1942
+ });
1943
+
1944
+ // ═══════════════════════════════════════════════════════════════════════════
1945
+ // CONTAINER ISOLATION
1946
+ // ═══════════════════════════════════════════════════════════════════════════
1947
+
1948
+ let containerRunner;
1949
+ try { containerRunner = require('../runtime/container').containerRunner; } catch {}
1950
+
1951
+ /**
1952
+ * Run a task in an isolated container
1953
+ */
1954
+ router.post('/containers/run', async (req, res) => {
1955
+ if (!containerRunner) return res.status(501).json({ error: 'Container isolation not available' });
1956
+ try {
1957
+ const result = await containerRunner.runInProcess(
1958
+ req.body.taskId || `ctr_task_${Date.now()}`,
1959
+ req.body.code || '',
1960
+ {
1961
+ params: req.body.params || {},
1962
+ timeout: req.body.timeout || 60000,
1963
+ maxMemory: req.body.maxMemory || 256 * 1024 * 1024,
1964
+ allowNetwork: req.body.allowNetwork !== false,
1965
+ }
1966
+ );
1967
+ res.json(result);
1968
+ } catch (err) {
1969
+ res.status(500).json({ error: err.message });
1970
+ }
1971
+ });
1972
+
1973
+ /**
1974
+ * List active containers
1975
+ */
1976
+ router.get('/containers', (req, res) => {
1977
+ if (!containerRunner) return res.json({ containers: [] });
1978
+ res.json({ containers: containerRunner.listContainers() });
1979
+ });
1980
+
1981
+ /**
1982
+ * Get container details
1983
+ */
1984
+ router.get('/containers/:containerId', (req, res) => {
1985
+ if (!containerRunner) return res.status(404).json({ error: 'Not found' });
1986
+ const c = containerRunner.getContainer(req.params.containerId);
1987
+ if (!c) return res.status(404).json({ error: 'Container not found' });
1988
+ res.json(c);
1989
+ });
1990
+
1991
+ /**
1992
+ * Kill a container
1993
+ */
1994
+ router.post('/containers/:containerId/kill', (req, res) => {
1995
+ if (!containerRunner) return res.status(404).json({ error: 'Not found' });
1996
+ const ok = containerRunner.kill(req.params.containerId);
1997
+ res.json({ success: ok });
1998
+ });
1999
+
2000
+ /**
2001
+ * Container stats
2002
+ */
2003
+ router.get('/containers/stats/summary', (req, res) => {
2004
+ if (!containerRunner) return res.json({ active: 0 });
2005
+ res.json(containerRunner.getStats());
2006
+ });
2007
+
2008
+ /**
2009
+ * Check Docker availability
2010
+ */
2011
+ router.get('/containers/docker/status', (req, res) => {
2012
+ if (!containerRunner) return res.json({ available: false });
2013
+ res.json({ available: containerRunner.isDockerAvailable() });
2014
+ });
2015
+
2016
+ // ═══════════════════════════════════════════════════════════════════════════
2017
+ // EXTERNAL QUEUE MANAGEMENT
2018
+ // ═══════════════════════════════════════════════════════════════════════════
2019
+
2020
+ let queueModule;
2021
+ try { queueModule = require('../runtime/queue'); } catch {}
2022
+
2023
+ /**
2024
+ * Queue stats
2025
+ */
2026
+ router.get('/queue/stats', (req, res) => {
2027
+ if (!queueModule) return res.json({ backend: 'memory' });
2028
+ const q = queueModule.createQueue('scheduler');
2029
+ res.json(q.getStats());
2030
+ });
2031
+
2032
+ /**
2033
+ * Purge completed items from queue
2034
+ */
2035
+ router.post('/queue/purge', (req, res) => {
2036
+ if (!queueModule) return res.json({ purged: 0 });
2037
+ const q = queueModule.createQueue('scheduler');
2038
+ const purged = q.purgeCompleted(parseInt(req.body.maxAge) || 3600_000);
2039
+ res.json({ purged });
2040
+ });
2041
+
2042
+ // ═══════════════════════════════════════════════════════════════════════════
2043
+ // ENHANCED REPLAY
2044
+ // ═══════════════════════════════════════════════════════════════════════════
2045
+
2046
+ /**
2047
+ * Export a recording (full data for download)
2048
+ */
2049
+ router.get('/replay/recordings/:taskId/export', (req, res) => {
2050
+ const data = replayEngine.exportRecording(req.params.taskId);
2051
+ if (!data) return res.status(404).json({ error: 'Recording not found' });
2052
+ res.json(data);
2053
+ });
2054
+
2055
+ /**
2056
+ * Import a recording
2057
+ */
2058
+ router.post('/replay/recordings/import', (req, res) => {
2059
+ try {
2060
+ const id = replayEngine.importRecording(req.body);
2061
+ res.json({ success: true, recordingId: id });
2062
+ } catch (err) {
2063
+ res.status(400).json({ error: err.message });
2064
+ }
2065
+ });
2066
+
2067
+ /**
2068
+ * Delete a recording
2069
+ */
2070
+ router.delete('/replay/recordings/:taskId', (req, res) => {
2071
+ replayEngine.deleteRecording(req.params.taskId);
2072
+ res.json({ success: true });
2073
+ });
2074
+
2075
+ /**
2076
+ * Replay from a specific checkpoint
2077
+ */
2078
+ router.post('/replay/:taskId/from-checkpoint', async (req, res) => {
2079
+ try {
2080
+ const result = await replayEngine.replay(req.params.taskId, {
2081
+ verify: req.body.verify !== false,
2082
+ continueOnMismatch: !!req.body.continueOnMismatch,
2083
+ fromCheckpoint: req.body.checkpoint,
2084
+ });
2085
+ res.json(result);
2086
+ } catch (err) {
2087
+ res.status(400).json({ error: err.message });
2088
+ }
2089
+ });
2090
+
2091
+ /**
2092
+ * Purge old recordings
2093
+ */
2094
+ router.post('/replay/purge', (req, res) => {
2095
+ const maxAge = parseInt(req.body.maxAge) || 7 * 24 * 3600_000;
2096
+ replayEngine.purgeOld(maxAge);
2097
+ res.json({ success: true });
2098
+ });
2099
+
2100
+ // ═══════════════════════════════════════════════════════════════════════════
2101
+ // WORKER PULL ENDPOINT (for distributed workers)
2102
+ // ═══════════════════════════════════════════════════════════════════════════
2103
+
2104
+ /**
2105
+ * Workers pull tasks from here
2106
+ */
2107
+ router.post('/cluster/nodes/:nodeId/pull', (req, res) => {
2108
+ const limit = parseInt(req.body.limit) || 5;
2109
+ // Fetch pending tasks from the cluster task distributor
2110
+ const tasks = [];
2111
+ try {
2112
+ const pending = distributor.getPendingTasks ? distributor.getPendingTasks(req.params.nodeId, limit) : [];
2113
+ tasks.push(...pending);
2114
+ } catch {}
2115
+ res.json({ tasks });
2116
+ });
2117
+
2118
+ /**
2119
+ * Worker reports task started
2120
+ */
2121
+ router.post('/cluster/tasks/:taskId/started', (req, res) => {
2122
+ bus.emit('cluster.task.started', { taskId: req.params.taskId, nodeId: req.body.nodeId });
2123
+ res.json({ ok: true });
2124
+ });
2125
+
2126
+ /**
2127
+ * Worker reports task completed
2128
+ */
2129
+ router.post('/cluster/tasks/:taskId/completed', (req, res) => {
2130
+ bus.emit('cluster.task.completed', {
2131
+ taskId: req.params.taskId,
2132
+ result: req.body.result,
2133
+ });
2134
+ res.json({ ok: true });
2135
+ });
2136
+
2137
+ /**
2138
+ * Worker reports task failed
2139
+ */
2140
+ router.post('/cluster/tasks/:taskId/failed', (req, res) => {
2141
+ bus.emit('cluster.task.failed', {
2142
+ taskId: req.params.taskId,
2143
+ error: req.body.error,
2144
+ });
2145
+ res.json({ ok: true });
2146
+ });
2147
+
2148
+ module.exports = router;