web-agent-bridge 2.6.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.
- package/package.json +79 -79
- package/sdk/package.json +22 -14
- package/server/config/plans.js +367 -0
- package/server/middleware/featureGate.js +88 -0
- package/server/migrations/005_marketplace_metering.sql +126 -0
- package/server/routes/runtime.js +313 -3
- package/server/services/hosted-runtime.js +205 -0
- package/server/services/marketplace.js +270 -0
- package/server/services/metering.js +182 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
-- Migration 005: Marketplace & Usage Metering tables
|
|
2
|
+
|
|
3
|
+
-- Marketplace listings
|
|
4
|
+
CREATE TABLE IF NOT EXISTS marketplace_listings (
|
|
5
|
+
id TEXT PRIMARY KEY,
|
|
6
|
+
name TEXT NOT NULL,
|
|
7
|
+
description TEXT DEFAULT '',
|
|
8
|
+
type TEXT NOT NULL,
|
|
9
|
+
category TEXT DEFAULT 'automation',
|
|
10
|
+
seller_id TEXT NOT NULL,
|
|
11
|
+
seller_name TEXT DEFAULT 'Anonymous',
|
|
12
|
+
price REAL DEFAULT 0,
|
|
13
|
+
currency TEXT DEFAULT 'usd',
|
|
14
|
+
version TEXT DEFAULT '1.0.0',
|
|
15
|
+
tags TEXT DEFAULT '[]',
|
|
16
|
+
icon TEXT,
|
|
17
|
+
readme TEXT DEFAULT '',
|
|
18
|
+
install_command TEXT,
|
|
19
|
+
config_schema TEXT DEFAULT '{}',
|
|
20
|
+
entry_point TEXT,
|
|
21
|
+
installs INTEGER DEFAULT 0,
|
|
22
|
+
revenue REAL DEFAULT 0,
|
|
23
|
+
rating REAL DEFAULT 0,
|
|
24
|
+
review_count INTEGER DEFAULT 0,
|
|
25
|
+
status TEXT DEFAULT 'pending_review',
|
|
26
|
+
rejection_reason TEXT,
|
|
27
|
+
published_at INTEGER,
|
|
28
|
+
created_at INTEGER NOT NULL,
|
|
29
|
+
updated_at INTEGER NOT NULL
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_mkt_listings_status ON marketplace_listings(status);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_mkt_listings_category ON marketplace_listings(category);
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_mkt_listings_seller ON marketplace_listings(seller_id);
|
|
35
|
+
|
|
36
|
+
-- Marketplace purchases
|
|
37
|
+
CREATE TABLE IF NOT EXISTS marketplace_purchases (
|
|
38
|
+
id TEXT PRIMARY KEY,
|
|
39
|
+
listing_id TEXT NOT NULL,
|
|
40
|
+
listing_name TEXT,
|
|
41
|
+
buyer_id TEXT NOT NULL,
|
|
42
|
+
seller_id TEXT NOT NULL,
|
|
43
|
+
price REAL DEFAULT 0,
|
|
44
|
+
commission REAL DEFAULT 0,
|
|
45
|
+
seller_earning REAL DEFAULT 0,
|
|
46
|
+
currency TEXT DEFAULT 'usd',
|
|
47
|
+
status TEXT DEFAULT 'pending_payment',
|
|
48
|
+
created_at INTEGER NOT NULL,
|
|
49
|
+
completed_at INTEGER,
|
|
50
|
+
FOREIGN KEY (listing_id) REFERENCES marketplace_listings(id)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_mkt_purchases_buyer ON marketplace_purchases(buyer_id);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_mkt_purchases_seller ON marketplace_purchases(seller_id);
|
|
55
|
+
|
|
56
|
+
-- Marketplace reviews
|
|
57
|
+
CREATE TABLE IF NOT EXISTS marketplace_reviews (
|
|
58
|
+
id TEXT PRIMARY KEY,
|
|
59
|
+
listing_id TEXT NOT NULL,
|
|
60
|
+
user_id TEXT NOT NULL,
|
|
61
|
+
rating INTEGER NOT NULL,
|
|
62
|
+
comment TEXT DEFAULT '',
|
|
63
|
+
created_at INTEGER NOT NULL,
|
|
64
|
+
FOREIGN KEY (listing_id) REFERENCES marketplace_listings(id)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
-- Seller earnings
|
|
68
|
+
CREATE TABLE IF NOT EXISTS marketplace_earnings (
|
|
69
|
+
seller_id TEXT PRIMARY KEY,
|
|
70
|
+
total REAL DEFAULT 0,
|
|
71
|
+
pending REAL DEFAULT 0,
|
|
72
|
+
paid REAL DEFAULT 0,
|
|
73
|
+
last_payout INTEGER
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
-- Usage metering daily records
|
|
77
|
+
CREATE TABLE IF NOT EXISTS usage_metering (
|
|
78
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
79
|
+
entity_id TEXT NOT NULL,
|
|
80
|
+
metric TEXT NOT NULL,
|
|
81
|
+
date TEXT NOT NULL,
|
|
82
|
+
count INTEGER DEFAULT 0,
|
|
83
|
+
overage INTEGER DEFAULT 0,
|
|
84
|
+
overage_cost REAL DEFAULT 0,
|
|
85
|
+
UNIQUE(entity_id, metric, date)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_usage_entity ON usage_metering(entity_id);
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_usage_date ON usage_metering(date);
|
|
90
|
+
|
|
91
|
+
-- Hosted runtime instances
|
|
92
|
+
CREATE TABLE IF NOT EXISTS hosted_instances (
|
|
93
|
+
id TEXT PRIMARY KEY,
|
|
94
|
+
agent_id TEXT NOT NULL,
|
|
95
|
+
tier TEXT DEFAULT 'starter',
|
|
96
|
+
region TEXT DEFAULT 'auto',
|
|
97
|
+
cpu TEXT DEFAULT '0.5',
|
|
98
|
+
memory TEXT DEFAULT '512',
|
|
99
|
+
status TEXT DEFAULT 'starting',
|
|
100
|
+
execution_count INTEGER DEFAULT 0,
|
|
101
|
+
compute_minutes REAL DEFAULT 0,
|
|
102
|
+
errors INTEGER DEFAULT 0,
|
|
103
|
+
started_at INTEGER NOT NULL,
|
|
104
|
+
stopped_at INTEGER,
|
|
105
|
+
last_activity INTEGER
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
CREATE INDEX IF NOT EXISTS idx_hosted_agent ON hosted_instances(agent_id);
|
|
109
|
+
CREATE INDEX IF NOT EXISTS idx_hosted_status ON hosted_instances(status);
|
|
110
|
+
|
|
111
|
+
-- Hosted executions
|
|
112
|
+
CREATE TABLE IF NOT EXISTS hosted_executions (
|
|
113
|
+
id TEXT PRIMARY KEY,
|
|
114
|
+
instance_id TEXT NOT NULL,
|
|
115
|
+
agent_id TEXT NOT NULL,
|
|
116
|
+
task_type TEXT,
|
|
117
|
+
task_action TEXT,
|
|
118
|
+
status TEXT DEFAULT 'running',
|
|
119
|
+
started_at INTEGER NOT NULL,
|
|
120
|
+
completed_at INTEGER,
|
|
121
|
+
compute_ms INTEGER DEFAULT 0,
|
|
122
|
+
error TEXT,
|
|
123
|
+
FOREIGN KEY (instance_id) REFERENCES hosted_instances(id)
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
CREATE INDEX IF NOT EXISTS idx_hexe_instance ON hosted_executions(instance_id);
|
package/server/routes/runtime.js
CHANGED
|
@@ -28,6 +28,11 @@ const { commandRegistry, siteRegistry, templateRegistry } = require('../registry
|
|
|
28
28
|
const { certificationEngine } = require('../registry/certification');
|
|
29
29
|
const { adapterManager, mcpAdapter, restAdapter, browserAdapter } = require('../adapters');
|
|
30
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');
|
|
31
36
|
const { sessionEngine } = require('../runtime/session-engine');
|
|
32
37
|
|
|
33
38
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -48,6 +53,8 @@ const PUBLIC_PATHS = [
|
|
|
48
53
|
'/registry/commands',
|
|
49
54
|
'/registry/sites',
|
|
50
55
|
'/registry/templates',
|
|
56
|
+
'/plans',
|
|
57
|
+
'/marketplace',
|
|
51
58
|
];
|
|
52
59
|
|
|
53
60
|
function authMiddleware(req, res, next) {
|
|
@@ -99,6 +106,7 @@ function authMiddleware(req, res, next) {
|
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
router.use(authMiddleware);
|
|
109
|
+
router.use(featureGate);
|
|
102
110
|
|
|
103
111
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
104
112
|
// PROTOCOL ENDPOINTS
|
|
@@ -241,7 +249,7 @@ router.delete('/agents/:agentId', (req, res) => {
|
|
|
241
249
|
/**
|
|
242
250
|
* Submit a task
|
|
243
251
|
*/
|
|
244
|
-
router.post('/tasks', (req, res) => {
|
|
252
|
+
router.post('/tasks', usageLimit('tasksPerDay'), (req, res) => {
|
|
245
253
|
try {
|
|
246
254
|
const result = runtime.submitTask(req.body);
|
|
247
255
|
metrics.increment('tasks.submitted', 1, { type: req.body.type });
|
|
@@ -299,7 +307,7 @@ router.post('/tasks/:taskId/resume', (req, res) => {
|
|
|
299
307
|
/**
|
|
300
308
|
* Execute a semantic action
|
|
301
309
|
*/
|
|
302
|
-
router.post('/execute', async (req, res) => {
|
|
310
|
+
router.post('/execute', usageLimit('executionsPerDay'), async (req, res) => {
|
|
303
311
|
try {
|
|
304
312
|
const result = await executor.execute(req.body);
|
|
305
313
|
res.json(result);
|
|
@@ -523,6 +531,9 @@ router.get('/observability/health', (req, res) => {
|
|
|
523
531
|
health.sessions = sessionEngine.getStats();
|
|
524
532
|
health.failures = failureAnalyzer.getStats();
|
|
525
533
|
health.certification = certificationEngine.getStats();
|
|
534
|
+
health.marketplace = marketplace.getStats();
|
|
535
|
+
health.hostedRuntime = hostedRuntime.getStats();
|
|
536
|
+
health.metering = metering.getStats();
|
|
526
537
|
res.json(health);
|
|
527
538
|
});
|
|
528
539
|
|
|
@@ -634,7 +645,7 @@ router.get('/registry/templates/:templateId', (req, res) => {
|
|
|
634
645
|
/**
|
|
635
646
|
* LLM completion
|
|
636
647
|
*/
|
|
637
|
-
router.post('/llm/complete', async (req, res) => {
|
|
648
|
+
router.post('/llm/complete', usageLimit('executionsPerDay'), async (req, res) => {
|
|
638
649
|
try {
|
|
639
650
|
const result = await llm.complete(req.body.prompt, req.body.options || req.body);
|
|
640
651
|
metrics.increment('llm.api.requests');
|
|
@@ -1133,4 +1144,303 @@ router.delete('/certification/:domain', (req, res) => {
|
|
|
1133
1144
|
res.json({ success: true });
|
|
1134
1145
|
});
|
|
1135
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
|
+
|
|
1136
1446
|
module.exports = router;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hosted Runtime Service
|
|
5
|
+
*
|
|
6
|
+
* Cloud execution abstraction for running agents without local infrastructure.
|
|
7
|
+
* Pay-as-you-go model with auto-scaling, resource tracking, and multi-region support.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const { bus } = require('../runtime/event-bus');
|
|
12
|
+
const metering = require('./metering');
|
|
13
|
+
|
|
14
|
+
class HostedRuntime {
|
|
15
|
+
constructor() {
|
|
16
|
+
this._instances = new Map(); // instanceId → RuntimeInstance
|
|
17
|
+
this._executions = new Map(); // executionId → Execution
|
|
18
|
+
this._maxInstances = 1000;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Launch a hosted runtime instance for an agent
|
|
23
|
+
*/
|
|
24
|
+
launch(config) {
|
|
25
|
+
if (!config.agentId) throw new Error('agentId required');
|
|
26
|
+
|
|
27
|
+
const instanceId = `hrt_${crypto.randomBytes(8).toString('hex')}`;
|
|
28
|
+
const instance = {
|
|
29
|
+
id: instanceId,
|
|
30
|
+
agentId: config.agentId,
|
|
31
|
+
tier: config.tier || 'starter',
|
|
32
|
+
region: config.region || 'auto',
|
|
33
|
+
resources: {
|
|
34
|
+
cpu: config.cpu || '0.5', // vCPU
|
|
35
|
+
memory: config.memory || '512', // MB
|
|
36
|
+
timeout: config.timeout || 300000, // 5 min default
|
|
37
|
+
},
|
|
38
|
+
status: 'starting',
|
|
39
|
+
startedAt: Date.now(),
|
|
40
|
+
lastActivity: Date.now(),
|
|
41
|
+
executionCount: 0,
|
|
42
|
+
computeMinutes: 0,
|
|
43
|
+
errors: 0,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
this._instances.set(instanceId, instance);
|
|
47
|
+
|
|
48
|
+
// Simulate startup (in real deployment, this would provision container/lambda)
|
|
49
|
+
instance.status = 'running';
|
|
50
|
+
bus.emit('hosted.launched', { instanceId, agentId: config.agentId, region: instance.region });
|
|
51
|
+
|
|
52
|
+
return instance;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Execute a task on a hosted instance
|
|
57
|
+
*/
|
|
58
|
+
async execute(instanceId, task) {
|
|
59
|
+
const instance = this._instances.get(instanceId);
|
|
60
|
+
if (!instance) throw new Error('Instance not found');
|
|
61
|
+
if (instance.status !== 'running') throw new Error(`Instance not running (status: ${instance.status})`);
|
|
62
|
+
|
|
63
|
+
const executionId = `hexe_${crypto.randomBytes(8).toString('hex')}`;
|
|
64
|
+
const execution = {
|
|
65
|
+
id: executionId,
|
|
66
|
+
instanceId,
|
|
67
|
+
agentId: instance.agentId,
|
|
68
|
+
task: {
|
|
69
|
+
type: task.type,
|
|
70
|
+
action: task.action,
|
|
71
|
+
params: task.params || {},
|
|
72
|
+
},
|
|
73
|
+
status: 'running',
|
|
74
|
+
startedAt: Date.now(),
|
|
75
|
+
completedAt: null,
|
|
76
|
+
result: null,
|
|
77
|
+
error: null,
|
|
78
|
+
computeMs: 0,
|
|
79
|
+
resources: {
|
|
80
|
+
cpuUsage: 0,
|
|
81
|
+
memoryUsage: 0,
|
|
82
|
+
networkCalls: 0,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
this._executions.set(executionId, execution);
|
|
87
|
+
instance.executionCount++;
|
|
88
|
+
instance.lastActivity = Date.now();
|
|
89
|
+
|
|
90
|
+
// Record metering
|
|
91
|
+
metering.record(instance.agentId, 'executionsPerDay', instance.tier, 1);
|
|
92
|
+
|
|
93
|
+
bus.emit('hosted.execution.started', { executionId, instanceId, type: task.type });
|
|
94
|
+
|
|
95
|
+
// Return execution handle (actual execution is async in real deployment)
|
|
96
|
+
return execution;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Complete an execution (called by worker after task finishes)
|
|
101
|
+
*/
|
|
102
|
+
completeExecution(executionId, result, error = null) {
|
|
103
|
+
const execution = this._executions.get(executionId);
|
|
104
|
+
if (!execution) return null;
|
|
105
|
+
|
|
106
|
+
execution.status = error ? 'failed' : 'completed';
|
|
107
|
+
execution.completedAt = Date.now();
|
|
108
|
+
execution.result = result;
|
|
109
|
+
execution.error = error ? { message: error.message || String(error) } : null;
|
|
110
|
+
execution.computeMs = execution.completedAt - execution.startedAt;
|
|
111
|
+
|
|
112
|
+
// Update instance stats
|
|
113
|
+
const instance = this._instances.get(execution.instanceId);
|
|
114
|
+
if (instance) {
|
|
115
|
+
const minutes = execution.computeMs / 60000;
|
|
116
|
+
instance.computeMinutes += minutes;
|
|
117
|
+
metering.record(instance.agentId, 'computeMinutesPerDay', instance.tier, minutes);
|
|
118
|
+
if (error) instance.errors++;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
bus.emit('hosted.execution.completed', {
|
|
122
|
+
executionId, instanceId: execution.instanceId,
|
|
123
|
+
status: execution.status, computeMs: execution.computeMs,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return execution;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Stop a hosted instance
|
|
131
|
+
*/
|
|
132
|
+
stop(instanceId) {
|
|
133
|
+
const instance = this._instances.get(instanceId);
|
|
134
|
+
if (!instance) return false;
|
|
135
|
+
instance.status = 'stopped';
|
|
136
|
+
instance.stoppedAt = Date.now();
|
|
137
|
+
bus.emit('hosted.stopped', { instanceId, agentId: instance.agentId });
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get instance
|
|
143
|
+
*/
|
|
144
|
+
getInstance(instanceId) {
|
|
145
|
+
return this._instances.get(instanceId) || null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* List instances
|
|
150
|
+
*/
|
|
151
|
+
listInstances(filters = {}, limit = 50) {
|
|
152
|
+
let instances = Array.from(this._instances.values());
|
|
153
|
+
if (filters.agentId) instances = instances.filter(i => i.agentId === filters.agentId);
|
|
154
|
+
if (filters.status) instances = instances.filter(i => i.status === filters.status);
|
|
155
|
+
if (filters.region) instances = instances.filter(i => i.region === filters.region);
|
|
156
|
+
return instances.slice(0, limit);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get execution
|
|
161
|
+
*/
|
|
162
|
+
getExecution(executionId) {
|
|
163
|
+
return this._executions.get(executionId) || null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* List executions for an instance
|
|
168
|
+
*/
|
|
169
|
+
listExecutions(instanceId, limit = 50) {
|
|
170
|
+
return Array.from(this._executions.values())
|
|
171
|
+
.filter(e => e.instanceId === instanceId)
|
|
172
|
+
.sort((a, b) => b.startedAt - a.startedAt)
|
|
173
|
+
.slice(0, limit);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get compute usage for an agent
|
|
178
|
+
*/
|
|
179
|
+
getComputeUsage(agentId) {
|
|
180
|
+
const instances = Array.from(this._instances.values())
|
|
181
|
+
.filter(i => i.agentId === agentId);
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
activeInstances: instances.filter(i => i.status === 'running').length,
|
|
185
|
+
totalExecutions: instances.reduce((sum, i) => sum + i.executionCount, 0),
|
|
186
|
+
totalComputeMinutes: Math.round(instances.reduce((sum, i) => sum + i.computeMinutes, 0) * 100) / 100,
|
|
187
|
+
totalErrors: instances.reduce((sum, i) => sum + i.errors, 0),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
getStats() {
|
|
192
|
+
const instances = Array.from(this._instances.values());
|
|
193
|
+
return {
|
|
194
|
+
totalInstances: instances.length,
|
|
195
|
+
running: instances.filter(i => i.status === 'running').length,
|
|
196
|
+
stopped: instances.filter(i => i.status === 'stopped').length,
|
|
197
|
+
totalExecutions: this._executions.size,
|
|
198
|
+
totalComputeMinutes: Math.round(instances.reduce((sum, i) => sum + i.computeMinutes, 0) * 100) / 100,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const hostedRuntime = new HostedRuntime();
|
|
204
|
+
|
|
205
|
+
module.exports = { HostedRuntime, hostedRuntime };
|