web-agent-bridge 3.4.0 → 3.8.1

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