web-agent-bridge 2.5.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,11 +19,94 @@ const router = express.Router();
19
19
  const protocol = require('../protocol');
20
20
  const { runtime, bus } = require('../runtime');
21
21
  const { logger, tracer, metrics } = require('../observability');
22
+ const { failureAnalyzer } = require('../observability/failure-analysis');
22
23
  const { identity, signer, isolation } = require('../security');
23
24
  const { agentManager, policyEngine } = require('../control-plane');
24
25
  const { executor } = require('../data-plane');
25
26
  const { llm } = require('../llm');
26
27
  const { commandRegistry, siteRegistry, templateRegistry } = require('../registry');
28
+ const { certificationEngine } = require('../registry/certification');
29
+ const { adapterManager, mcpAdapter, restAdapter, browserAdapter } = require('../adapters');
30
+ const { replayEngine } = require('../runtime/replay');
31
+ const { featureGate, usageLimit } = require('../middleware/featureGate');
32
+ const { listPlans, getPlan, USAGE_PRICING, MARKETPLACE } = require('../config/plans');
33
+ const metering = require('../services/metering');
34
+ const { marketplace } = require('../services/marketplace');
35
+ const { hostedRuntime } = require('../services/hosted-runtime');
36
+ const { sessionEngine } = require('../runtime/session-engine');
37
+
38
+ // ═══════════════════════════════════════════════════════════════════════════
39
+ // AUTH MIDDLEWARE
40
+ // ═══════════════════════════════════════════════════════════════════════════
41
+
42
+ /**
43
+ * Authenticate requests via API key or session token.
44
+ * Public endpoints (protocol info, agent registration, health) bypass auth.
45
+ */
46
+ const PUBLIC_PATHS = [
47
+ '/protocol',
48
+ '/agents/register',
49
+ '/agents/authenticate',
50
+ '/observability/health',
51
+ '/llm/models',
52
+ '/llm/status',
53
+ '/registry/commands',
54
+ '/registry/sites',
55
+ '/registry/templates',
56
+ '/plans',
57
+ '/marketplace',
58
+ ];
59
+
60
+ function authMiddleware(req, res, next) {
61
+ // Allow public GET endpoints
62
+ const matchesPublic = PUBLIC_PATHS.some(p =>
63
+ req.path === p || (req.method === 'GET' && req.path.startsWith(p))
64
+ );
65
+ if (matchesPublic) return next();
66
+
67
+ // Check session token
68
+ const authHeader = req.headers['authorization'];
69
+ if (authHeader && authHeader.startsWith('Bearer ')) {
70
+ const token = authHeader.slice(7);
71
+ const session = identity.validateSession(token);
72
+ if (session) {
73
+ req.agentId = session.agentId;
74
+ req.session = session;
75
+ return next();
76
+ }
77
+ }
78
+
79
+ // Check API key
80
+ const apiKey = req.headers['x-wab-key'];
81
+ if (apiKey) {
82
+ const ip = req.ip || req.connection?.remoteAddress;
83
+ const session = identity.authenticate(apiKey, ip);
84
+ if (session) {
85
+ req.agentId = session.agentId;
86
+ req.session = session;
87
+ return next();
88
+ }
89
+ }
90
+
91
+ // Check agent ID header (for internal/trusted calls)
92
+ const agentHeader = req.headers['x-wab-agent'];
93
+ if (agentHeader) {
94
+ const agent = identity.getAgent(agentHeader);
95
+ if (agent && agent.status === 'active') {
96
+ req.agentId = agentHeader;
97
+ return next();
98
+ }
99
+ }
100
+
101
+ // No auth on non-mutation GET requests (read-only)
102
+ if (req.method === 'GET') return next();
103
+
104
+ metrics.increment('auth.rejected');
105
+ return res.status(401).json({ error: 'Authentication required. Provide X-WAB-Key or Authorization: Bearer <token>' });
106
+ }
107
+
108
+ router.use(authMiddleware);
109
+ router.use(featureGate);
27
110
 
28
111
  // ═══════════════════════════════════════════════════════════════════════════
29
112
  // PROTOCOL ENDPOINTS
@@ -166,7 +249,7 @@ router.delete('/agents/:agentId', (req, res) => {
166
249
  /**
167
250
  * Submit a task
168
251
  */
169
- router.post('/tasks', (req, res) => {
252
+ router.post('/tasks', usageLimit('tasksPerDay'), (req, res) => {
170
253
  try {
171
254
  const result = runtime.submitTask(req.body);
172
255
  metrics.increment('tasks.submitted', 1, { type: req.body.type });
@@ -224,7 +307,7 @@ router.post('/tasks/:taskId/resume', (req, res) => {
224
307
  /**
225
308
  * Execute a semantic action
226
309
  */
227
- router.post('/execute', async (req, res) => {
310
+ router.post('/execute', usageLimit('executionsPerDay'), async (req, res) => {
228
311
  try {
229
312
  const result = await executor.execute(req.body);
230
313
  res.json(result);
@@ -443,6 +526,14 @@ router.get('/observability/health', (req, res) => {
443
526
  };
444
527
  health.executor = executor.getStats();
445
528
  health.llm = llm.getStatus();
529
+ health.adapters = adapterManager.getStats();
530
+ health.replay = replayEngine.getStats();
531
+ health.sessions = sessionEngine.getStats();
532
+ health.failures = failureAnalyzer.getStats();
533
+ health.certification = certificationEngine.getStats();
534
+ health.marketplace = marketplace.getStats();
535
+ health.hostedRuntime = hostedRuntime.getStats();
536
+ health.metering = metering.getStats();
446
537
  res.json(health);
447
538
  });
448
539
 
@@ -554,7 +645,7 @@ router.get('/registry/templates/:templateId', (req, res) => {
554
645
  /**
555
646
  * LLM completion
556
647
  */
557
- router.post('/llm/complete', async (req, res) => {
648
+ router.post('/llm/complete', usageLimit('executionsPerDay'), async (req, res) => {
558
649
  try {
559
650
  const result = await llm.complete(req.body.prompt, req.body.options || req.body);
560
651
  metrics.increment('llm.api.requests');
@@ -722,4 +813,634 @@ protocolHandler.handle('wab.commerce.compare', async (payload) => {
722
813
  });
723
814
  });
724
815
 
816
+ // ═══════════════════════════════════════════════════════════════════════════
817
+ // ADAPTERS
818
+ // ═══════════════════════════════════════════════════════════════════════════
819
+
820
+ /**
821
+ * List adapters
822
+ */
823
+ router.get('/adapters', (req, res) => {
824
+ res.json({ adapters: adapterManager.list() });
825
+ });
826
+
827
+ /**
828
+ * Adapter stats
829
+ */
830
+ router.get('/adapters/stats', (req, res) => {
831
+ res.json(adapterManager.getStats());
832
+ });
833
+
834
+ /**
835
+ * MCP: list tools
836
+ */
837
+ router.get('/adapters/mcp/tools', (req, res) => {
838
+ const commands = protocol.schema.listCommands();
839
+ res.json(mcpAdapter.handleListTools(commands));
840
+ });
841
+
842
+ /**
843
+ * MCP: call tool
844
+ */
845
+ router.post('/adapters/mcp/call', async (req, res) => {
846
+ try {
847
+ const result = await mcpAdapter.handleCallTool(req.body, async (wapReq) => {
848
+ const request = protocol.createRequest(wapReq.command, wapReq.payload);
849
+ return protocolHandler.process(request);
850
+ });
851
+ res.json(result);
852
+ } catch (err) {
853
+ res.status(500).json({ error: err.message });
854
+ }
855
+ });
856
+
857
+ /**
858
+ * REST adapter: register endpoint
859
+ */
860
+ router.post('/adapters/rest/endpoints', (req, res) => {
861
+ try {
862
+ const endpoint = restAdapter.registerEndpoint(req.body.id, req.body);
863
+ res.json(endpoint);
864
+ } catch (err) {
865
+ res.status(400).json({ error: err.message });
866
+ }
867
+ });
868
+
869
+ /**
870
+ * REST adapter: list endpoints
871
+ */
872
+ router.get('/adapters/rest/endpoints', (req, res) => {
873
+ res.json({ endpoints: restAdapter.listEndpoints() });
874
+ });
875
+
876
+ /**
877
+ * REST adapter: execute
878
+ */
879
+ router.post('/adapters/rest/execute', async (req, res) => {
880
+ try {
881
+ const result = await restAdapter.execute(req.body.endpoint, req.body.params);
882
+ res.json(result);
883
+ } catch (err) {
884
+ res.status(500).json({ error: err.message });
885
+ }
886
+ });
887
+
888
+ /**
889
+ * Browser adapter: list semantic mappings
890
+ */
891
+ router.get('/adapters/browser/mappings', (req, res) => {
892
+ res.json({ mappings: browserAdapter.listMappings() });
893
+ });
894
+
895
+ /**
896
+ * Browser adapter: resolve semantic action
897
+ */
898
+ router.post('/adapters/browser/resolve', (req, res) => {
899
+ const { domain, action, params } = req.body;
900
+ const plan = browserAdapter.fromWAP({ domain, action, params });
901
+ if (!plan) return res.status(404).json({ error: 'No mapping for this semantic action' });
902
+ res.json(plan);
903
+ });
904
+
905
+ /**
906
+ * Browser adapter: register mapping
907
+ */
908
+ router.post('/adapters/browser/mappings', (req, res) => {
909
+ const { domainAction, plan } = req.body;
910
+ if (!domainAction || !plan) return res.status(400).json({ error: 'domainAction and plan required' });
911
+ browserAdapter.registerMapping(domainAction, plan);
912
+ res.json({ success: true });
913
+ });
914
+
915
+ // ═══════════════════════════════════════════════════════════════════════════
916
+ // REPLAY ENGINE
917
+ // ═══════════════════════════════════════════════════════════════════════════
918
+
919
+ /**
920
+ * List recordings
921
+ */
922
+ router.get('/replay/recordings', (req, res) => {
923
+ res.json({ recordings: replayEngine.listRecordings(parseInt(req.query.limit) || 50) });
924
+ });
925
+
926
+ /**
927
+ * Get recording
928
+ */
929
+ router.get('/replay/recordings/:taskId', (req, res) => {
930
+ const rec = replayEngine.getRecording(req.params.taskId);
931
+ if (!rec) return res.status(404).json({ error: 'Recording not found' });
932
+ res.json(rec);
933
+ });
934
+
935
+ /**
936
+ * Replay a task
937
+ */
938
+ router.post('/replay/:taskId', async (req, res) => {
939
+ try {
940
+ const result = await replayEngine.replay(req.params.taskId, {
941
+ verify: req.body.verify !== false,
942
+ continueOnMismatch: !!req.body.continueOnMismatch,
943
+ });
944
+ res.json(result);
945
+ } catch (err) {
946
+ res.status(400).json({ error: err.message });
947
+ }
948
+ });
949
+
950
+ /**
951
+ * Diff two recordings
952
+ */
953
+ router.get('/replay/diff/:taskId1/:taskId2', (req, res) => {
954
+ const diff = replayEngine.diff(req.params.taskId1, req.params.taskId2);
955
+ if (!diff) return res.status(404).json({ error: 'One or both recordings not found' });
956
+ res.json(diff);
957
+ });
958
+
959
+ /**
960
+ * Replay stats
961
+ */
962
+ router.get('/replay/stats', (req, res) => {
963
+ res.json(replayEngine.getStats());
964
+ });
965
+
966
+ // ═══════════════════════════════════════════════════════════════════════════
967
+ // SESSION ENGINE
968
+ // ═══════════════════════════════════════════════════════════════════════════
969
+
970
+ /**
971
+ * Create browser session
972
+ */
973
+ router.post('/sessions', (req, res) => {
974
+ const session = sessionEngine.create(req.body);
975
+ res.json(session);
976
+ });
977
+
978
+ /**
979
+ * List sessions
980
+ */
981
+ router.get('/sessions', (req, res) => {
982
+ const sessions = sessionEngine.list({
983
+ agentId: req.query.agentId,
984
+ siteId: req.query.siteId,
985
+ state: req.query.state,
986
+ }, parseInt(req.query.limit) || 50);
987
+ res.json({ sessions, total: sessions.length });
988
+ });
989
+
990
+ /**
991
+ * Get session
992
+ */
993
+ router.get('/sessions/:sessionId', (req, res) => {
994
+ const session = sessionEngine.get(req.params.sessionId);
995
+ if (!session) return res.status(404).json({ error: 'Session not found or expired' });
996
+ res.json(session);
997
+ });
998
+
999
+ /**
1000
+ * Export session
1001
+ */
1002
+ router.get('/sessions/:sessionId/export', (req, res) => {
1003
+ const data = sessionEngine.export(req.params.sessionId);
1004
+ if (!data) return res.status(404).json({ error: 'Session not found' });
1005
+ res.json(data);
1006
+ });
1007
+
1008
+ /**
1009
+ * Import session
1010
+ */
1011
+ router.post('/sessions/import', (req, res) => {
1012
+ const session = sessionEngine.import(req.body);
1013
+ res.json(session);
1014
+ });
1015
+
1016
+ /**
1017
+ * Set cookies
1018
+ */
1019
+ router.post('/sessions/:sessionId/cookies', (req, res) => {
1020
+ sessionEngine.setCookies(req.params.sessionId, req.body.cookies || []);
1021
+ res.json({ success: true });
1022
+ });
1023
+
1024
+ /**
1025
+ * Get cookies
1026
+ */
1027
+ router.get('/sessions/:sessionId/cookies', (req, res) => {
1028
+ const cookies = sessionEngine.getCookies(req.params.sessionId, req.query.domain);
1029
+ res.json({ cookies });
1030
+ });
1031
+
1032
+ /**
1033
+ * Set storage
1034
+ */
1035
+ router.post('/sessions/:sessionId/storage', (req, res) => {
1036
+ const { key, value, type } = req.body;
1037
+ sessionEngine.setStorage(req.params.sessionId, key, value, type);
1038
+ res.json({ success: true });
1039
+ });
1040
+
1041
+ /**
1042
+ * Destroy session
1043
+ */
1044
+ router.delete('/sessions/:sessionId', (req, res) => {
1045
+ sessionEngine.destroy(req.params.sessionId);
1046
+ res.json({ success: true });
1047
+ });
1048
+
1049
+ // ═══════════════════════════════════════════════════════════════════════════
1050
+ // FAILURE ANALYSIS
1051
+ // ═══════════════════════════════════════════════════════════════════════════
1052
+
1053
+ /**
1054
+ * Query failures
1055
+ */
1056
+ router.get('/failures', (req, res) => {
1057
+ const failures = failureAnalyzer.query({
1058
+ classification: req.query.classification,
1059
+ severity: req.query.severity,
1060
+ agentId: req.query.agentId,
1061
+ taskId: req.query.taskId,
1062
+ retryable: req.query.retryable === 'true' ? true : req.query.retryable === 'false' ? false : undefined,
1063
+ since: parseInt(req.query.since) || undefined,
1064
+ }, parseInt(req.query.limit) || 50);
1065
+ res.json({ failures, total: failures.length });
1066
+ });
1067
+
1068
+ /**
1069
+ * Get failure
1070
+ */
1071
+ router.get('/failures/:failureId', (req, res) => {
1072
+ const failure = failureAnalyzer.getFailure(req.params.failureId);
1073
+ if (!failure) return res.status(404).json({ error: 'Failure not found' });
1074
+ res.json(failure);
1075
+ });
1076
+
1077
+ /**
1078
+ * Get failure patterns
1079
+ */
1080
+ router.get('/failures/analysis/patterns', (req, res) => {
1081
+ res.json({ patterns: failureAnalyzer.getPatterns() });
1082
+ });
1083
+
1084
+ /**
1085
+ * Get failure summary
1086
+ */
1087
+ router.get('/failures/analysis/summary', (req, res) => {
1088
+ res.json(failureAnalyzer.getSummary(parseInt(req.query.since) || 0));
1089
+ });
1090
+
1091
+ /**
1092
+ * Classify a failure manually
1093
+ */
1094
+ router.post('/failures/classify', (req, res) => {
1095
+ const { error, context } = req.body;
1096
+ if (!error) return res.status(400).json({ error: 'error object required' });
1097
+ const classification = failureAnalyzer.classify(error, context || {});
1098
+ res.json(classification);
1099
+ });
1100
+
1101
+ // ═══════════════════════════════════════════════════════════════════════════
1102
+ // CERTIFICATION
1103
+ // ═══════════════════════════════════════════════════════════════════════════
1104
+
1105
+ /**
1106
+ * Verify a site
1107
+ */
1108
+ router.post('/certification/verify', async (req, res) => {
1109
+ try {
1110
+ const { domain, probeData } = req.body;
1111
+ if (!domain) return res.status(400).json({ error: 'domain required' });
1112
+ const result = await certificationEngine.verify(domain, probeData || {});
1113
+ res.json(result);
1114
+ } catch (err) {
1115
+ res.status(500).json({ error: err.message });
1116
+ }
1117
+ });
1118
+
1119
+ /**
1120
+ * Get certificate
1121
+ */
1122
+ router.get('/certification/:domain', (req, res) => {
1123
+ const cert = certificationEngine.getCertificate(req.params.domain);
1124
+ if (!cert) return res.status(404).json({ error: 'No active certificate for this domain' });
1125
+ res.json(cert);
1126
+ });
1127
+
1128
+ /**
1129
+ * List certificates
1130
+ */
1131
+ router.get('/certification', (req, res) => {
1132
+ const certs = certificationEngine.listCertificates({
1133
+ level: req.query.level,
1134
+ minScore: parseInt(req.query.minScore) || undefined,
1135
+ }, parseInt(req.query.limit) || 50);
1136
+ res.json({ certificates: certs, total: certs.length });
1137
+ });
1138
+
1139
+ /**
1140
+ * Revoke certificate
1141
+ */
1142
+ router.delete('/certification/:domain', (req, res) => {
1143
+ certificationEngine.revoke(req.params.domain);
1144
+ res.json({ success: true });
1145
+ });
1146
+
1147
+ // ═══════════════════════════════════════════════════════════════════════════
1148
+ // PLANS & PRICING
1149
+ // ═══════════════════════════════════════════════════════════════════════════
1150
+
1151
+ /**
1152
+ * List available plans
1153
+ */
1154
+ router.get('/plans', (req, res) => {
1155
+ const plans = listPlans().map(p => ({
1156
+ id: p.id,
1157
+ name: p.name,
1158
+ price: p.price,
1159
+ interval: p.interval,
1160
+ description: p.description,
1161
+ limits: p.limits,
1162
+ features: Object.entries(p.features)
1163
+ .filter(([, v]) => v === true)
1164
+ .map(([k]) => k),
1165
+ }));
1166
+ res.json({ plans, usagePricing: USAGE_PRICING });
1167
+ });
1168
+
1169
+ /**
1170
+ * Get specific plan details
1171
+ */
1172
+ router.get('/plans/:planId', (req, res) => {
1173
+ const plan = getPlan(req.params.planId);
1174
+ if (!plan || plan.id === 'free' && req.params.planId !== 'free') {
1175
+ return res.status(404).json({ error: 'Plan not found' });
1176
+ }
1177
+ res.json(plan);
1178
+ });
1179
+
1180
+ // ═══════════════════════════════════════════════════════════════════════════
1181
+ // USAGE METERING
1182
+ // ═══════════════════════════════════════════════════════════════════════════
1183
+
1184
+ /**
1185
+ * Get usage for current agent
1186
+ */
1187
+ router.get('/usage', (req, res) => {
1188
+ const entityId = req.agentId || req.ip;
1189
+ const tier = req.agentTier || req.session?.tier || 'free';
1190
+ res.json(metering.getUsage(entityId, tier));
1191
+ });
1192
+
1193
+ /**
1194
+ * Get billing summary (overages)
1195
+ */
1196
+ router.get('/usage/billing', (req, res) => {
1197
+ const entityId = req.agentId || req.ip;
1198
+ res.json(metering.getBillingSummary(entityId));
1199
+ });
1200
+
1201
+ /**
1202
+ * Get metering stats (admin)
1203
+ */
1204
+ router.get('/usage/stats', (req, res) => {
1205
+ res.json(metering.getStats());
1206
+ });
1207
+
1208
+ // ═══════════════════════════════════════════════════════════════════════════
1209
+ // MARKETPLACE
1210
+ // ═══════════════════════════════════════════════════════════════════════════
1211
+
1212
+ /**
1213
+ * Search marketplace
1214
+ */
1215
+ router.get('/marketplace', (req, res) => {
1216
+ const listings = marketplace.search({
1217
+ type: req.query.type,
1218
+ category: req.query.category,
1219
+ query: req.query.q,
1220
+ tag: req.query.tag,
1221
+ free: req.query.free === 'true',
1222
+ paid: req.query.paid === 'true',
1223
+ minRating: req.query.minRating ? parseFloat(req.query.minRating) : undefined,
1224
+ sortBy: req.query.sortBy,
1225
+ }, parseInt(req.query.limit) || 50);
1226
+ res.json({ listings, total: listings.length });
1227
+ });
1228
+
1229
+ /**
1230
+ * Get listing
1231
+ */
1232
+ router.get('/marketplace/:listingId', (req, res) => {
1233
+ const listing = marketplace.getListing(req.params.listingId);
1234
+ if (!listing) return res.status(404).json({ error: 'Listing not found' });
1235
+ res.json(listing);
1236
+ });
1237
+
1238
+ /**
1239
+ * Get reviews
1240
+ */
1241
+ router.get('/marketplace/:listingId/reviews', (req, res) => {
1242
+ res.json({ reviews: marketplace.getReviews(req.params.listingId) });
1243
+ });
1244
+
1245
+ /**
1246
+ * Publish listing
1247
+ */
1248
+ router.post('/marketplace/publish', (req, res) => {
1249
+ try {
1250
+ const listing = marketplace.publish({
1251
+ ...req.body,
1252
+ sellerId: req.agentId || req.body.sellerId,
1253
+ });
1254
+ res.json(listing);
1255
+ } catch (err) {
1256
+ res.status(400).json({ error: err.message });
1257
+ }
1258
+ });
1259
+
1260
+ /**
1261
+ * Purchase/install listing
1262
+ */
1263
+ router.post('/marketplace/:listingId/purchase', (req, res) => {
1264
+ try {
1265
+ const buyerId = req.agentId || req.body.buyerId;
1266
+ if (!buyerId) return res.status(400).json({ error: 'buyerId required' });
1267
+ const purchase = marketplace.purchase(req.params.listingId, buyerId);
1268
+ res.json(purchase);
1269
+ } catch (err) {
1270
+ res.status(400).json({ error: err.message });
1271
+ }
1272
+ });
1273
+
1274
+ /**
1275
+ * Add review
1276
+ */
1277
+ router.post('/marketplace/:listingId/review', (req, res) => {
1278
+ try {
1279
+ const review = marketplace.addReview(req.params.listingId, {
1280
+ userId: req.agentId || req.body.userId,
1281
+ rating: req.body.rating,
1282
+ comment: req.body.comment,
1283
+ });
1284
+ res.json(review);
1285
+ } catch (err) {
1286
+ res.status(400).json({ error: err.message });
1287
+ }
1288
+ });
1289
+
1290
+ /**
1291
+ * Get my purchases
1292
+ */
1293
+ router.get('/marketplace/my/purchases', (req, res) => {
1294
+ const buyerId = req.agentId || req.query.buyerId;
1295
+ res.json({ purchases: marketplace.getPurchases(buyerId) });
1296
+ });
1297
+
1298
+ /**
1299
+ * Get seller earnings
1300
+ */
1301
+ router.get('/marketplace/my/earnings', (req, res) => {
1302
+ const sellerId = req.agentId || req.query.sellerId;
1303
+ res.json(marketplace.getEarnings(sellerId));
1304
+ });
1305
+
1306
+ /**
1307
+ * Admin: pending listings
1308
+ */
1309
+ router.get('/marketplace/admin/pending', (req, res) => {
1310
+ res.json({ listings: marketplace.getPendingListings() });
1311
+ });
1312
+
1313
+ /**
1314
+ * Admin: approve listing
1315
+ */
1316
+ router.post('/marketplace/admin/:listingId/approve', (req, res) => {
1317
+ try {
1318
+ const listing = marketplace.approve(req.params.listingId);
1319
+ res.json(listing);
1320
+ } catch (err) {
1321
+ res.status(400).json({ error: err.message });
1322
+ }
1323
+ });
1324
+
1325
+ /**
1326
+ * Admin: reject listing
1327
+ */
1328
+ router.post('/marketplace/admin/:listingId/reject', (req, res) => {
1329
+ try {
1330
+ const listing = marketplace.reject(req.params.listingId, req.body.reason);
1331
+ res.json(listing);
1332
+ } catch (err) {
1333
+ res.status(400).json({ error: err.message });
1334
+ }
1335
+ });
1336
+
1337
+ /**
1338
+ * Marketplace stats
1339
+ */
1340
+ router.get('/marketplace/stats', (req, res) => {
1341
+ res.json(marketplace.getStats());
1342
+ });
1343
+
1344
+ // ═══════════════════════════════════════════════════════════════════════════
1345
+ // HOSTED RUNTIME
1346
+ // ═══════════════════════════════════════════════════════════════════════════
1347
+
1348
+ /**
1349
+ * Launch hosted instance
1350
+ */
1351
+ router.post('/hosted/launch', (req, res) => {
1352
+ try {
1353
+ const instance = hostedRuntime.launch({
1354
+ agentId: req.agentId || req.body.agentId,
1355
+ tier: req.agentTier || req.session?.tier || 'starter',
1356
+ region: req.body.region,
1357
+ cpu: req.body.cpu,
1358
+ memory: req.body.memory,
1359
+ timeout: req.body.timeout,
1360
+ });
1361
+ res.json(instance);
1362
+ } catch (err) {
1363
+ res.status(400).json({ error: err.message });
1364
+ }
1365
+ });
1366
+
1367
+ /**
1368
+ * Execute on hosted instance
1369
+ */
1370
+ router.post('/hosted/:instanceId/execute', async (req, res) => {
1371
+ try {
1372
+ const execution = await hostedRuntime.execute(req.params.instanceId, req.body);
1373
+ res.json(execution);
1374
+ } catch (err) {
1375
+ res.status(400).json({ error: err.message });
1376
+ }
1377
+ });
1378
+
1379
+ /**
1380
+ * Complete execution
1381
+ */
1382
+ router.post('/hosted/executions/:executionId/complete', (req, res) => {
1383
+ const execution = hostedRuntime.completeExecution(
1384
+ req.params.executionId,
1385
+ req.body.result,
1386
+ req.body.error ? new Error(req.body.error) : null
1387
+ );
1388
+ if (!execution) return res.status(404).json({ error: 'Execution not found' });
1389
+ res.json(execution);
1390
+ });
1391
+
1392
+ /**
1393
+ * Stop hosted instance
1394
+ */
1395
+ router.post('/hosted/:instanceId/stop', (req, res) => {
1396
+ const success = hostedRuntime.stop(req.params.instanceId);
1397
+ res.json({ success });
1398
+ });
1399
+
1400
+ /**
1401
+ * Get hosted instance
1402
+ */
1403
+ router.get('/hosted/:instanceId', (req, res) => {
1404
+ const instance = hostedRuntime.getInstance(req.params.instanceId);
1405
+ if (!instance) return res.status(404).json({ error: 'Instance not found' });
1406
+ res.json(instance);
1407
+ });
1408
+
1409
+ /**
1410
+ * List instances
1411
+ */
1412
+ router.get('/hosted', (req, res) => {
1413
+ const instances = hostedRuntime.listInstances({
1414
+ agentId: req.query.agentId,
1415
+ status: req.query.status,
1416
+ region: req.query.region,
1417
+ }, parseInt(req.query.limit) || 50);
1418
+ res.json({ instances, total: instances.length });
1419
+ });
1420
+
1421
+ /**
1422
+ * List executions for instance
1423
+ */
1424
+ router.get('/hosted/:instanceId/executions', (req, res) => {
1425
+ const executions = hostedRuntime.listExecutions(
1426
+ req.params.instanceId,
1427
+ parseInt(req.query.limit) || 50
1428
+ );
1429
+ res.json({ executions, total: executions.length });
1430
+ });
1431
+
1432
+ /**
1433
+ * Get compute usage
1434
+ */
1435
+ router.get('/hosted/usage/:agentId', (req, res) => {
1436
+ res.json(hostedRuntime.getComputeUsage(req.params.agentId));
1437
+ });
1438
+
1439
+ /**
1440
+ * Hosted runtime stats
1441
+ */
1442
+ router.get('/hosted/stats', (req, res) => {
1443
+ res.json(hostedRuntime.getStats());
1444
+ });
1445
+
725
1446
  module.exports = router;