web-agent-bridge 3.4.0 → 3.9.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 (315) hide show
  1. package/LICENSE +84 -84
  2. package/README.ar.md +1565 -1304
  3. package/README.md +171 -298
  4. package/bin/agent-runner.js +474 -474
  5. package/bin/cli.js +237 -237
  6. package/bin/wab-init.js +244 -223
  7. package/bin/wab.js +80 -80
  8. package/examples/azure-dns-wab.js +83 -83
  9. package/examples/bidi-agent.js +119 -119
  10. package/examples/cloudflare-wab-dns.js +121 -121
  11. package/examples/cpanel-wab-dns.js +114 -114
  12. package/examples/cross-site-agent.js +91 -91
  13. package/examples/dns-discovery-agent.js +166 -166
  14. package/examples/gcp-dns-wab.js +76 -76
  15. package/examples/governance-agent.js +169 -169
  16. package/examples/mcp-agent.js +94 -94
  17. package/examples/next-app-router/README.md +44 -44
  18. package/examples/plesk-wab-dns.js +103 -103
  19. package/examples/puppeteer-agent.js +108 -108
  20. package/examples/route53-wab-dns.js +144 -144
  21. package/examples/saas-dashboard/README.md +55 -55
  22. package/examples/safe-mode-agent.js +96 -96
  23. package/examples/self-discovery.js +106 -0
  24. package/examples/shopify-hydrogen/README.md +74 -74
  25. package/examples/vision-agent.js +171 -171
  26. package/examples/wab-sign.js +74 -74
  27. package/examples/wab-verify.js +60 -60
  28. package/examples/wordpress-elementor/README.md +77 -77
  29. package/package.json +93 -93
  30. package/public/.well-known/agent-tools.json +180 -180
  31. package/public/.well-known/ai-assets.json +59 -59
  32. package/public/.well-known/security.txt +8 -8
  33. package/public/.well-known/wab.json +28 -28
  34. package/public/activate.html +448 -368
  35. package/public/adopt.html +236 -0
  36. package/public/adoption-metrics.html +188 -188
  37. package/public/agent-workspace.html +359 -349
  38. package/public/ai.html +198 -198
  39. package/public/api.html +397 -413
  40. package/public/atp.html +171 -0
  41. package/public/azure-dns-integration.html +289 -289
  42. package/public/browser.html +486 -486
  43. package/public/cloudflare-integration.html +380 -380
  44. package/public/commander-dashboard.html +243 -243
  45. package/public/cookies.html +210 -210
  46. package/public/cpanel-integration.html +398 -398
  47. package/public/css/agent-workspace.css +1713 -1713
  48. package/public/css/premium.css +317 -317
  49. package/public/css/styles.css +1401 -1263
  50. package/public/dashboard-shieldlink.html +295 -0
  51. package/public/dashboard.html +711 -707
  52. package/public/dns.html +436 -436
  53. package/public/docs.html +588 -588
  54. package/public/enterprise-mesh.ar.html +80 -0
  55. package/public/enterprise-mesh.html +81 -0
  56. package/public/feed.xml +89 -89
  57. package/public/gcp-dns-integration.html +318 -318
  58. package/public/governance.ar.html +70 -0
  59. package/public/governance.html +69 -0
  60. package/public/growth.html +465 -465
  61. package/public/index.html +1372 -1266
  62. package/public/integrations.html +556 -556
  63. package/public/js/activate.js +449 -145
  64. package/public/js/agent-workspace.js +1740 -1740
  65. package/public/js/auth-nav.js +117 -65
  66. package/public/js/auth-redirect.js +12 -12
  67. package/public/js/cookie-consent.js +56 -56
  68. package/public/js/dns.js +438 -438
  69. package/public/js/wab-demo-page.js +721 -721
  70. package/public/js/ws-client.js +74 -74
  71. package/public/l-preview.html +242 -0
  72. package/public/llms-full.txt +360 -360
  73. package/public/llms.txt +125 -125
  74. package/public/login.html +85 -85
  75. package/public/mesh-dashboard.html +328 -328
  76. package/public/milestones.html +346 -0
  77. package/public/one-click.html +779 -0
  78. package/public/openapi.json +669 -669
  79. package/public/partners.ar.html +145 -0
  80. package/public/partners.html +143 -0
  81. package/public/phone-shield.html +281 -281
  82. package/public/plesk-integration.html +375 -375
  83. package/public/premium-dashboard.html +2489 -2489
  84. package/public/premium.html +793 -793
  85. package/public/privacy.html +297 -297
  86. package/public/provider-onboarding.html +172 -172
  87. package/public/provider-sandbox.html +134 -134
  88. package/public/providers.html +359 -359
  89. package/public/refusals.html +172 -0
  90. package/public/register.html +105 -105
  91. package/public/registrar-integrations.html +141 -141
  92. package/public/ring4.html +292 -0
  93. package/public/robots.txt +99 -99
  94. package/public/route53-integration.html +531 -531
  95. package/public/score.html +263 -0
  96. package/public/script/wab-consent.d.ts +36 -36
  97. package/public/script/wab-consent.js +104 -104
  98. package/public/script/wab-schema.js +131 -131
  99. package/public/script/wab.d.ts +108 -108
  100. package/public/script/wab.min.js +580 -580
  101. package/public/security.txt +8 -8
  102. package/public/shieldlink.html +244 -0
  103. package/public/shieldqr.html +231 -231
  104. package/public/sitemap.xml +13 -1
  105. package/public/terms.html +256 -256
  106. package/public/trust-graph-api.ar.html +92 -0
  107. package/public/trust-graph-api.html +91 -0
  108. package/public/wab-features.html +560 -0
  109. package/public/wab-trust.html +200 -200
  110. package/public/wab-truth.html +375 -0
  111. package/public/wab-vs-protocols.html +210 -210
  112. package/public/whitepaper.html +449 -449
  113. package/script/ai-agent-bridge.js +1754 -1754
  114. package/sdk/README.md +99 -99
  115. package/sdk/agent-mesh.js +449 -449
  116. package/sdk/atp.js +103 -0
  117. package/sdk/auto-discovery.js +301 -288
  118. package/sdk/commander.js +262 -262
  119. package/sdk/governance.js +262 -262
  120. package/sdk/index.d.ts +464 -464
  121. package/sdk/index.js +653 -649
  122. package/sdk/multi-agent.js +318 -318
  123. package/sdk/safe-mode.js +221 -221
  124. package/sdk/safety-shield.js +219 -219
  125. package/sdk/schema-discovery.js +83 -83
  126. package/server/adapters/index.js +520 -520
  127. package/server/config/plans.js +412 -367
  128. package/server/config/secrets.js +102 -102
  129. package/server/control-plane/index.js +301 -301
  130. package/server/data-plane/index.js +354 -354
  131. package/server/index.js +793 -670
  132. package/server/llm/index.js +404 -404
  133. package/server/middleware/adminAuth.js +35 -35
  134. package/server/middleware/api-tier.js +170 -0
  135. package/server/middleware/auth.js +50 -50
  136. package/server/middleware/featureGate.js +88 -88
  137. package/server/middleware/rateLimits.js +100 -100
  138. package/server/middleware/sensitiveAction.js +157 -157
  139. package/server/middleware/wab-trust.js +141 -0
  140. package/server/migrations/001_add_analytics_indexes.sql +7 -7
  141. package/server/migrations/002_premium_features.sql +418 -418
  142. package/server/migrations/003_ads_integer_cents.sql +33 -33
  143. package/server/migrations/004_agent_os.sql +158 -158
  144. package/server/migrations/005_marketplace_metering.sql +126 -126
  145. package/server/migrations/006_growth_suite.sql +138 -0
  146. package/server/migrations/007_governance.sql +106 -106
  147. package/server/migrations/008_plans.sql +144 -144
  148. package/server/migrations/009_shieldqr.sql +30 -30
  149. package/server/migrations/010_extended_trust.sql +33 -33
  150. package/server/migrations/011_outreach.sql +47 -0
  151. package/server/migrations/012_shieldlink.sql +116 -0
  152. package/server/migrations/013_ct_monitor.sql +13 -0
  153. package/server/migrations/014_wab_advanced_features.sql +128 -0
  154. package/server/migrations/015_wab_truth_layer.sql +101 -0
  155. package/server/migrations/016_ring4_external_trust.sql +84 -0
  156. package/server/migrations/017_ring4_extensions.sql +69 -0
  157. package/server/migrations/018_commercial_foundations.sql +167 -0
  158. package/server/migrations/019_unify_tier_constraints.sql +133 -0
  159. package/server/migrations/020_agent_transaction_primitive.sql +119 -0
  160. package/server/models/adapters/index.js +33 -33
  161. package/server/models/adapters/mysql.js +183 -183
  162. package/server/models/adapters/postgresql.js +172 -172
  163. package/server/models/adapters/sqlite.js +7 -7
  164. package/server/models/db.js +740 -740
  165. package/server/observability/failure-analysis.js +337 -337
  166. package/server/observability/index.js +394 -394
  167. package/server/protocol/capabilities.js +223 -223
  168. package/server/protocol/index.js +243 -243
  169. package/server/protocol/schema.js +584 -584
  170. package/server/registry/certification.js +271 -271
  171. package/server/registry/index.js +326 -326
  172. package/server/routes/activate.js +478 -0
  173. package/server/routes/admin-outreach.js +239 -0
  174. package/server/routes/admin-plans.js +76 -76
  175. package/server/routes/admin-premium.js +674 -673
  176. package/server/routes/admin-shieldlink.js +137 -0
  177. package/server/routes/admin-shieldqr.js +90 -90
  178. package/server/routes/admin-trust-monitor.js +139 -83
  179. package/server/routes/admin.js +550 -549
  180. package/server/routes/adopt.js +61 -0
  181. package/server/routes/ads.js +130 -130
  182. package/server/routes/agent-workspace.js +540 -540
  183. package/server/routes/api-keys.js +127 -0
  184. package/server/routes/api.js +150 -150
  185. package/server/routes/auth.js +71 -71
  186. package/server/routes/billing.js +57 -57
  187. package/server/routes/commander.js +316 -316
  188. package/server/routes/customer-shieldlink.js +133 -0
  189. package/server/routes/demo-showcase.js +332 -332
  190. package/server/routes/demo-store.js +154 -154
  191. package/server/routes/diagnose.js +373 -0
  192. package/server/routes/discovery.js +2348 -2348
  193. package/server/routes/enterprise-mesh.js +170 -0
  194. package/server/routes/gateway.js +173 -173
  195. package/server/routes/governance-saas.js +203 -0
  196. package/server/routes/governance.js +208 -208
  197. package/server/routes/growth.js +1048 -0
  198. package/server/routes/intent.js +328 -0
  199. package/server/routes/license.js +251 -251
  200. package/server/routes/mesh.js +469 -469
  201. package/server/routes/noscript.js +543 -543
  202. package/server/routes/partners.js +201 -0
  203. package/server/routes/plans.js +33 -33
  204. package/server/routes/premium-v2.js +686 -686
  205. package/server/routes/premium.js +724 -724
  206. package/server/routes/providers.js +650 -650
  207. package/server/routes/reputation.js +411 -0
  208. package/server/routes/ring4.js +885 -0
  209. package/server/routes/runtime.js +2148 -2148
  210. package/server/routes/shieldlink.js +70 -0
  211. package/server/routes/shieldqr.js +88 -88
  212. package/server/routes/sovereign.js +465 -465
  213. package/server/routes/transactions.js +233 -0
  214. package/server/routes/truth-layer.js +670 -0
  215. package/server/routes/universal.js +200 -200
  216. package/server/routes/unsubscribe.js +51 -0
  217. package/server/routes/wab-api.js +850 -850
  218. package/server/routes/wab-cache.js +282 -0
  219. package/server/runtime/container-worker.js +111 -111
  220. package/server/runtime/container.js +448 -448
  221. package/server/runtime/distributed-worker.js +362 -362
  222. package/server/runtime/event-bus.js +210 -210
  223. package/server/runtime/index.js +253 -253
  224. package/server/runtime/queue.js +599 -599
  225. package/server/runtime/replay.js +666 -666
  226. package/server/runtime/sandbox.js +266 -266
  227. package/server/runtime/scheduler.js +534 -534
  228. package/server/runtime/session-engine.js +293 -293
  229. package/server/runtime/state-manager.js +188 -188
  230. package/server/secrets/wab-signing-key.pem +3 -0
  231. package/server/secrets/wab-signing-pub.pem +3 -0
  232. package/server/security/cross-site-redactor.js +196 -196
  233. package/server/security/dry-run.js +180 -180
  234. package/server/security/human-gate-rate-limit.js +147 -147
  235. package/server/security/human-gate-transports.js +178 -178
  236. package/server/security/human-gate.js +281 -281
  237. package/server/security/index.js +368 -368
  238. package/server/security/intent-engine.js +245 -245
  239. package/server/security/reward-guard.js +171 -171
  240. package/server/security/rollback-store.js +239 -239
  241. package/server/security/token-scope.js +404 -404
  242. package/server/security/url-policy.js +139 -139
  243. package/server/services/adoption-agent.js +182 -0
  244. package/server/services/agent-chat.js +506 -506
  245. package/server/services/agent-learning.js +601 -601
  246. package/server/services/agent-memory.js +625 -625
  247. package/server/services/agent-mesh.js +555 -555
  248. package/server/services/agent-symphony.js +717 -717
  249. package/server/services/agent-tasks.js +1807 -1807
  250. package/server/services/api-key-engine.js +292 -292
  251. package/server/services/cluster.js +894 -894
  252. package/server/services/commander.js +738 -738
  253. package/server/services/edge-compute.js +440 -440
  254. package/server/services/email.js +233 -233
  255. package/server/services/fairness-engine.js +409 -0
  256. package/server/services/fairness.js +420 -0
  257. package/server/services/governance.js +466 -466
  258. package/server/services/hosted-runtime.js +205 -205
  259. package/server/services/lfd.js +635 -635
  260. package/server/services/local-ai.js +389 -389
  261. package/server/services/marketplace.js +270 -270
  262. package/server/services/metering.js +182 -182
  263. package/server/services/modules/affiliate-intelligence.js +93 -93
  264. package/server/services/modules/agent-firewall.js +90 -90
  265. package/server/services/modules/bounty.js +89 -89
  266. package/server/services/modules/collective-bargaining.js +92 -92
  267. package/server/services/modules/dark-pattern.js +66 -66
  268. package/server/services/modules/gov-intelligence.js +45 -45
  269. package/server/services/modules/neural.js +55 -55
  270. package/server/services/modules/notary.js +49 -49
  271. package/server/services/modules/price-time-machine.js +86 -86
  272. package/server/services/modules/protocol.js +104 -104
  273. package/server/services/negotiation.js +439 -439
  274. package/server/services/outreach-agent.js +312 -0
  275. package/server/services/plans.js +214 -214
  276. package/server/services/plugins.js +771 -771
  277. package/server/services/price-intelligence.js +566 -566
  278. package/server/services/price-shield.js +1137 -1137
  279. package/server/services/provider-clients.js +740 -740
  280. package/server/services/reputation.js +465 -465
  281. package/server/services/search-engine.js +357 -357
  282. package/server/services/security.js +513 -513
  283. package/server/services/self-healing.js +843 -843
  284. package/server/services/shieldlink.js +492 -0
  285. package/server/services/shieldqr.js +322 -322
  286. package/server/services/sovereign-shield.js +542 -542
  287. package/server/services/ssl-ct-monitor.js +224 -0
  288. package/server/services/ssl-inspector.js +42 -42
  289. package/server/services/ssl-monitor.js +167 -167
  290. package/server/services/stripe.js +206 -205
  291. package/server/services/swarm.js +788 -788
  292. package/server/services/transactions.js +525 -0
  293. package/server/services/universal-scraper.js +662 -662
  294. package/server/services/verification.js +481 -481
  295. package/server/services/vision.js +1163 -1163
  296. package/server/services/wab-crypto.js +178 -178
  297. package/server/utils/cache.js +125 -125
  298. package/server/utils/migrate.js +81 -81
  299. package/server/utils/safe-fetch.js +228 -228
  300. package/server/utils/secureFields.js +50 -50
  301. package/server/ws.js +161 -161
  302. package/templates/artisan-marketplace.yaml +104 -104
  303. package/templates/book-price-scout.yaml +98 -98
  304. package/templates/electronics-price-tracker.yaml +108 -108
  305. package/templates/flight-deal-hunter.yaml +113 -113
  306. package/templates/freelancer-direct.yaml +116 -116
  307. package/templates/grocery-price-compare.yaml +93 -93
  308. package/templates/hotel-direct-booking.yaml +113 -113
  309. package/templates/local-services.yaml +98 -98
  310. package/templates/olive-oil-tunisia.yaml +88 -88
  311. package/templates/organic-farm-fresh.yaml +101 -101
  312. package/templates/restaurant-direct.yaml +97 -97
  313. package/templates/ring4/banking-sovereign.yaml +55 -0
  314. package/templates/ring4/ecommerce-sovereign.yaml +58 -0
  315. package/templates/ring4/healthcare-sovereign.yaml +60 -0
@@ -1,2148 +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 { 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;
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;