web-agent-bridge 2.4.0 → 2.6.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/README.ar.md +18 -0
- package/README.md +32 -0
- package/package.json +1 -1
- package/public/.well-known/agent-tools.json +180 -0
- package/sdk/index.d.ts +170 -0
- package/sdk/index.js +246 -1
- package/sdk/package.json +1 -1
- package/server/adapters/index.js +520 -0
- package/server/control-plane/index.js +301 -0
- package/server/data-plane/index.js +354 -0
- package/server/index.js +6 -0
- package/server/llm/index.js +404 -0
- package/server/migrations/004_agent_os.sql +158 -0
- package/server/observability/failure-analysis.js +337 -0
- package/server/observability/index.js +394 -0
- package/server/protocol/capabilities.js +223 -0
- package/server/protocol/index.js +243 -0
- package/server/protocol/schema.js +584 -0
- package/server/registry/certification.js +271 -0
- package/server/registry/index.js +326 -0
- package/server/routes/runtime.js +1136 -0
- package/server/runtime/event-bus.js +210 -0
- package/server/runtime/index.js +233 -0
- package/server/runtime/replay.js +264 -0
- package/server/runtime/sandbox.js +266 -0
- package/server/runtime/scheduler.js +395 -0
- package/server/runtime/session-engine.js +293 -0
- package/server/runtime/state-manager.js +188 -0
- package/server/security/index.js +368 -0
|
@@ -0,0 +1,1136 @@
|
|
|
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 { sessionEngine } = require('../runtime/session-engine');
|
|
32
|
+
|
|
33
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
34
|
+
// AUTH MIDDLEWARE
|
|
35
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Authenticate requests via API key or session token.
|
|
39
|
+
* Public endpoints (protocol info, agent registration, health) bypass auth.
|
|
40
|
+
*/
|
|
41
|
+
const PUBLIC_PATHS = [
|
|
42
|
+
'/protocol',
|
|
43
|
+
'/agents/register',
|
|
44
|
+
'/agents/authenticate',
|
|
45
|
+
'/observability/health',
|
|
46
|
+
'/llm/models',
|
|
47
|
+
'/llm/status',
|
|
48
|
+
'/registry/commands',
|
|
49
|
+
'/registry/sites',
|
|
50
|
+
'/registry/templates',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function authMiddleware(req, res, next) {
|
|
54
|
+
// Allow public GET endpoints
|
|
55
|
+
const matchesPublic = PUBLIC_PATHS.some(p =>
|
|
56
|
+
req.path === p || (req.method === 'GET' && req.path.startsWith(p))
|
|
57
|
+
);
|
|
58
|
+
if (matchesPublic) return next();
|
|
59
|
+
|
|
60
|
+
// Check session token
|
|
61
|
+
const authHeader = req.headers['authorization'];
|
|
62
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
63
|
+
const token = authHeader.slice(7);
|
|
64
|
+
const session = identity.validateSession(token);
|
|
65
|
+
if (session) {
|
|
66
|
+
req.agentId = session.agentId;
|
|
67
|
+
req.session = session;
|
|
68
|
+
return next();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check API key
|
|
73
|
+
const apiKey = req.headers['x-wab-key'];
|
|
74
|
+
if (apiKey) {
|
|
75
|
+
const ip = req.ip || req.connection?.remoteAddress;
|
|
76
|
+
const session = identity.authenticate(apiKey, ip);
|
|
77
|
+
if (session) {
|
|
78
|
+
req.agentId = session.agentId;
|
|
79
|
+
req.session = session;
|
|
80
|
+
return next();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check agent ID header (for internal/trusted calls)
|
|
85
|
+
const agentHeader = req.headers['x-wab-agent'];
|
|
86
|
+
if (agentHeader) {
|
|
87
|
+
const agent = identity.getAgent(agentHeader);
|
|
88
|
+
if (agent && agent.status === 'active') {
|
|
89
|
+
req.agentId = agentHeader;
|
|
90
|
+
return next();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// No auth on non-mutation GET requests (read-only)
|
|
95
|
+
if (req.method === 'GET') return next();
|
|
96
|
+
|
|
97
|
+
metrics.increment('auth.rejected');
|
|
98
|
+
return res.status(401).json({ error: 'Authentication required. Provide X-WAB-Key or Authorization: Bearer <token>' });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
router.use(authMiddleware);
|
|
102
|
+
|
|
103
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
104
|
+
// PROTOCOL ENDPOINTS
|
|
105
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Protocol info & capabilities
|
|
109
|
+
*/
|
|
110
|
+
router.get('/protocol', (req, res) => {
|
|
111
|
+
res.json({
|
|
112
|
+
protocol: protocol.PROTOCOL_NAME,
|
|
113
|
+
version: protocol.PROTOCOL_VERSION,
|
|
114
|
+
commands: protocol.schema.listCommands().map(c => ({
|
|
115
|
+
name: c.name,
|
|
116
|
+
version: c.version,
|
|
117
|
+
category: c.category,
|
|
118
|
+
description: c.description,
|
|
119
|
+
capabilities: c.capabilities,
|
|
120
|
+
})),
|
|
121
|
+
capabilities: Object.keys(protocol.schema.Capabilities),
|
|
122
|
+
permissionLevels: protocol.schema.PermissionLevels,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Process a protocol message
|
|
128
|
+
*/
|
|
129
|
+
router.post('/protocol/message', async (req, res) => {
|
|
130
|
+
const endTimer = metrics.startTimer('api.protocol.message.duration');
|
|
131
|
+
try {
|
|
132
|
+
const msg = req.body;
|
|
133
|
+
if (!msg || !msg.command) {
|
|
134
|
+
return res.status(400).json({ error: 'Invalid protocol message' });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Create proper protocol request if not already
|
|
138
|
+
const request = msg.protocol === 'wabp' ? msg : protocol.createRequest(msg.command, msg.payload || msg.params || {}, {
|
|
139
|
+
agentId: msg.agentId,
|
|
140
|
+
traceId: msg.traceId,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const response = await protocolHandler.process(request);
|
|
144
|
+
endTimer();
|
|
145
|
+
metrics.increment('api.protocol.messages', 1, { command: msg.command });
|
|
146
|
+
res.json(response);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
endTimer();
|
|
149
|
+
res.status(500).json({ error: err.message });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
154
|
+
// AGENT IDENTITY & AUTH
|
|
155
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Register a new agent
|
|
159
|
+
*/
|
|
160
|
+
router.post('/agents/register', (req, res) => {
|
|
161
|
+
try {
|
|
162
|
+
const { name, type, capabilities, publicKey, metadata } = req.body;
|
|
163
|
+
if (!name || !type) return res.status(400).json({ error: 'name and type required' });
|
|
164
|
+
|
|
165
|
+
const result = identity.register(name, type, { capabilities, publicKey, metadata });
|
|
166
|
+
metrics.increment('agents.registered');
|
|
167
|
+
logger.info('Agent registered', { agentId: result.agentId, name, type });
|
|
168
|
+
|
|
169
|
+
res.json({
|
|
170
|
+
agentId: result.agentId,
|
|
171
|
+
apiKey: result.apiKey, // Only returned once!
|
|
172
|
+
message: 'Store your API key securely. It cannot be recovered.',
|
|
173
|
+
});
|
|
174
|
+
} catch (err) {
|
|
175
|
+
res.status(500).json({ error: err.message });
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Authenticate agent
|
|
181
|
+
*/
|
|
182
|
+
router.post('/agents/authenticate', (req, res) => {
|
|
183
|
+
const { apiKey } = req.body;
|
|
184
|
+
if (!apiKey) return res.status(400).json({ error: 'apiKey required' });
|
|
185
|
+
|
|
186
|
+
const ip = req.ip || req.connection?.remoteAddress;
|
|
187
|
+
const session = identity.authenticate(apiKey, ip);
|
|
188
|
+
if (!session) {
|
|
189
|
+
metrics.increment('agents.auth.failed');
|
|
190
|
+
return res.status(401).json({ error: 'Invalid API key or agent revoked' });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
metrics.increment('agents.auth.success');
|
|
194
|
+
res.json(session);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get agent info
|
|
199
|
+
*/
|
|
200
|
+
router.get('/agents/:agentId', (req, res) => {
|
|
201
|
+
const agent = identity.getAgent(req.params.agentId);
|
|
202
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
203
|
+
res.json(agent);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* List agents
|
|
208
|
+
*/
|
|
209
|
+
router.get('/agents', (req, res) => {
|
|
210
|
+
const agents = identity.listAgents({ type: req.query.type, status: req.query.status || 'active' });
|
|
211
|
+
res.json({ agents, total: agents.length });
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Negotiate capabilities
|
|
216
|
+
*/
|
|
217
|
+
router.post('/agents/:agentId/capabilities', (req, res) => {
|
|
218
|
+
const { capabilities, siteId, constraints } = req.body;
|
|
219
|
+
if (!capabilities || !Array.isArray(capabilities)) {
|
|
220
|
+
return res.status(400).json({ error: 'capabilities array required' });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const result = protocol.negotiator.negotiate(req.params.agentId, capabilities, siteId, constraints || {});
|
|
224
|
+
res.json(result);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Revoke agent
|
|
229
|
+
*/
|
|
230
|
+
router.delete('/agents/:agentId', (req, res) => {
|
|
231
|
+
identity.revoke(req.params.agentId);
|
|
232
|
+
protocol.negotiator.revokeAgent(req.params.agentId);
|
|
233
|
+
logger.info('Agent revoked', { agentId: req.params.agentId });
|
|
234
|
+
res.json({ success: true });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
238
|
+
// TASK MANAGEMENT (RUNTIME)
|
|
239
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Submit a task
|
|
243
|
+
*/
|
|
244
|
+
router.post('/tasks', (req, res) => {
|
|
245
|
+
try {
|
|
246
|
+
const result = runtime.submitTask(req.body);
|
|
247
|
+
metrics.increment('tasks.submitted', 1, { type: req.body.type });
|
|
248
|
+
res.json(result);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
res.status(400).json({ error: err.message });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get task status
|
|
256
|
+
*/
|
|
257
|
+
router.get('/tasks/:taskId', (req, res) => {
|
|
258
|
+
const task = runtime.scheduler.getTask(req.params.taskId);
|
|
259
|
+
if (!task) return res.status(404).json({ error: 'Task not found' });
|
|
260
|
+
res.json(task);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* List tasks
|
|
265
|
+
*/
|
|
266
|
+
router.get('/tasks', (req, res) => {
|
|
267
|
+
const tasks = runtime.scheduler.listTasks(req.query.state, parseInt(req.query.limit) || 50);
|
|
268
|
+
res.json({ tasks, total: tasks.length });
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Cancel a task
|
|
273
|
+
*/
|
|
274
|
+
router.delete('/tasks/:taskId', (req, res) => {
|
|
275
|
+
const success = runtime.scheduler.cancel(req.params.taskId);
|
|
276
|
+
res.json({ success });
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Pause a task
|
|
281
|
+
*/
|
|
282
|
+
router.post('/tasks/:taskId/pause', (req, res) => {
|
|
283
|
+
const success = runtime.scheduler.pause(req.params.taskId);
|
|
284
|
+
res.json({ success });
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Resume a task
|
|
289
|
+
*/
|
|
290
|
+
router.post('/tasks/:taskId/resume', (req, res) => {
|
|
291
|
+
const success = runtime.scheduler.resume(req.params.taskId);
|
|
292
|
+
res.json({ success });
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
296
|
+
// EXECUTION (DATA PLANE)
|
|
297
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Execute a semantic action
|
|
301
|
+
*/
|
|
302
|
+
router.post('/execute', async (req, res) => {
|
|
303
|
+
try {
|
|
304
|
+
const result = await executor.execute(req.body);
|
|
305
|
+
res.json(result);
|
|
306
|
+
} catch (err) {
|
|
307
|
+
res.status(500).json({ error: err.message });
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Execute semantic action (domain.action style)
|
|
313
|
+
*/
|
|
314
|
+
router.post('/execute/semantic', async (req, res) => {
|
|
315
|
+
try {
|
|
316
|
+
const { domain, action, params, siteId, agentId, siteDomain } = req.body;
|
|
317
|
+
if (!domain || !action) return res.status(400).json({ error: 'domain and action required' });
|
|
318
|
+
|
|
319
|
+
const result = await executor.execute({
|
|
320
|
+
type: 'semantic',
|
|
321
|
+
domain,
|
|
322
|
+
action,
|
|
323
|
+
params: params || {},
|
|
324
|
+
siteId,
|
|
325
|
+
agentId,
|
|
326
|
+
siteDomain,
|
|
327
|
+
});
|
|
328
|
+
res.json(result);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
res.status(500).json({ error: err.message });
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Execute a pipeline
|
|
336
|
+
*/
|
|
337
|
+
router.post('/execute/pipeline', async (req, res) => {
|
|
338
|
+
try {
|
|
339
|
+
const result = await executor.execute({ ...req.body, type: 'pipeline' });
|
|
340
|
+
res.json(result);
|
|
341
|
+
} catch (err) {
|
|
342
|
+
res.status(500).json({ error: err.message });
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Resolve a semantic action (without executing)
|
|
348
|
+
*/
|
|
349
|
+
router.get('/execute/resolve', (req, res) => {
|
|
350
|
+
const { domain, action, siteDomain } = req.query;
|
|
351
|
+
if (!domain || !action) return res.status(400).json({ error: 'domain and action required' });
|
|
352
|
+
const impl = executor.resolver.resolve(siteDomain || '*', `${domain}.${action}`);
|
|
353
|
+
if (!impl) return res.status(404).json({ error: 'No implementation found' });
|
|
354
|
+
res.json(impl);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
358
|
+
// CONTROL PLANE
|
|
359
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Deploy an agent
|
|
363
|
+
*/
|
|
364
|
+
router.post('/deployments', (req, res) => {
|
|
365
|
+
try {
|
|
366
|
+
const { agentId, config } = req.body;
|
|
367
|
+
if (!agentId) return res.status(400).json({ error: 'agentId required' });
|
|
368
|
+
const deployment = agentManager.deploy(agentId, config || {});
|
|
369
|
+
res.json(deployment);
|
|
370
|
+
} catch (err) {
|
|
371
|
+
res.status(400).json({ error: err.message });
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* List deployments
|
|
377
|
+
*/
|
|
378
|
+
router.get('/deployments', (req, res) => {
|
|
379
|
+
const deployments = agentManager.listDeployments({
|
|
380
|
+
status: req.query.status,
|
|
381
|
+
agentId: req.query.agentId,
|
|
382
|
+
});
|
|
383
|
+
res.json({ deployments, total: deployments.length });
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Create a policy
|
|
388
|
+
*/
|
|
389
|
+
router.post('/policies', (req, res) => {
|
|
390
|
+
try {
|
|
391
|
+
const policy = policyEngine.createPolicy(req.body);
|
|
392
|
+
res.json(policy);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
res.status(400).json({ error: err.message });
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Bind policy to entity
|
|
400
|
+
*/
|
|
401
|
+
router.post('/policies/:policyId/bind', (req, res) => {
|
|
402
|
+
const { entityId } = req.body;
|
|
403
|
+
if (!entityId) return res.status(400).json({ error: 'entityId required' });
|
|
404
|
+
policyEngine.bind(entityId, req.params.policyId);
|
|
405
|
+
res.json({ success: true });
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Evaluate policies
|
|
410
|
+
*/
|
|
411
|
+
router.post('/policies/evaluate', (req, res) => {
|
|
412
|
+
const { entityId, action, context } = req.body;
|
|
413
|
+
if (!entityId || !action) return res.status(400).json({ error: 'entityId and action required' });
|
|
414
|
+
const result = policyEngine.evaluate(entityId, action, context || {});
|
|
415
|
+
res.json(result);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* List policies
|
|
420
|
+
*/
|
|
421
|
+
router.get('/policies', (req, res) => {
|
|
422
|
+
const policies = policyEngine.listPolicies(req.query.entityId);
|
|
423
|
+
res.json({ policies, total: policies.length });
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
427
|
+
// SITE ISOLATION
|
|
428
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Configure site isolation
|
|
432
|
+
*/
|
|
433
|
+
router.post('/isolation/:siteId', (req, res) => {
|
|
434
|
+
isolation.configure(req.params.siteId, req.body);
|
|
435
|
+
res.json({ success: true });
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Get site isolation config
|
|
440
|
+
*/
|
|
441
|
+
router.get('/isolation/:siteId', (req, res) => {
|
|
442
|
+
const config = isolation.getConfig(req.params.siteId);
|
|
443
|
+
if (!config) return res.status(404).json({ error: 'No isolation config' });
|
|
444
|
+
res.json(config);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
448
|
+
// OBSERVABILITY
|
|
449
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Get metrics snapshot
|
|
453
|
+
*/
|
|
454
|
+
router.get('/observability/metrics', (req, res) => {
|
|
455
|
+
res.json(metrics.snapshot());
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Get specific metric
|
|
460
|
+
*/
|
|
461
|
+
router.get('/observability/metrics/:name', (req, res) => {
|
|
462
|
+
const h = metrics.getHistogram(req.params.name);
|
|
463
|
+
if (h) return res.json({ type: 'histogram', name: req.params.name, ...h });
|
|
464
|
+
|
|
465
|
+
const c = metrics.getCounter(req.params.name);
|
|
466
|
+
if (c) return res.json({ type: 'counter', name: req.params.name, value: c });
|
|
467
|
+
|
|
468
|
+
const g = metrics.getGauge(req.params.name);
|
|
469
|
+
if (g) return res.json({ type: 'gauge', name: req.params.name, value: g });
|
|
470
|
+
|
|
471
|
+
res.status(404).json({ error: 'Metric not found' });
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* List traces
|
|
476
|
+
*/
|
|
477
|
+
router.get('/observability/traces', (req, res) => {
|
|
478
|
+
const traces = tracer.listTraces(
|
|
479
|
+
parseInt(req.query.limit) || 50,
|
|
480
|
+
{ status: req.query.status, name: req.query.name, since: parseInt(req.query.since) || undefined }
|
|
481
|
+
);
|
|
482
|
+
res.json({ traces, total: traces.length });
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Get trace details
|
|
487
|
+
*/
|
|
488
|
+
router.get('/observability/traces/:traceId', (req, res) => {
|
|
489
|
+
const trace = tracer.getTrace(req.params.traceId);
|
|
490
|
+
if (!trace) return res.status(404).json({ error: 'Trace not found' });
|
|
491
|
+
res.json(trace);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Query logs
|
|
496
|
+
*/
|
|
497
|
+
router.get('/observability/logs', (req, res) => {
|
|
498
|
+
const logs = logger.query({
|
|
499
|
+
level: req.query.level,
|
|
500
|
+
traceId: req.query.traceId,
|
|
501
|
+
agentId: req.query.agentId,
|
|
502
|
+
since: parseInt(req.query.since) || undefined,
|
|
503
|
+
message: req.query.message,
|
|
504
|
+
}, parseInt(req.query.limit) || 100);
|
|
505
|
+
res.json({ logs, total: logs.length });
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Runtime health
|
|
510
|
+
*/
|
|
511
|
+
router.get('/observability/health', (req, res) => {
|
|
512
|
+
const health = runtime.getHealth();
|
|
513
|
+
health.identity = identity.getStats();
|
|
514
|
+
health.registry = {
|
|
515
|
+
commands: commandRegistry.getStats(),
|
|
516
|
+
sites: siteRegistry.getStats(),
|
|
517
|
+
templates: templateRegistry.getStats(),
|
|
518
|
+
};
|
|
519
|
+
health.executor = executor.getStats();
|
|
520
|
+
health.llm = llm.getStatus();
|
|
521
|
+
health.adapters = adapterManager.getStats();
|
|
522
|
+
health.replay = replayEngine.getStats();
|
|
523
|
+
health.sessions = sessionEngine.getStats();
|
|
524
|
+
health.failures = failureAnalyzer.getStats();
|
|
525
|
+
health.certification = certificationEngine.getStats();
|
|
526
|
+
res.json(health);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
530
|
+
// REGISTRY
|
|
531
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Register a command
|
|
535
|
+
*/
|
|
536
|
+
router.post('/registry/commands', (req, res) => {
|
|
537
|
+
try {
|
|
538
|
+
const { siteId, ...command } = req.body;
|
|
539
|
+
if (!siteId) return res.status(400).json({ error: 'siteId required' });
|
|
540
|
+
const entry = commandRegistry.register(siteId, command);
|
|
541
|
+
res.json(entry);
|
|
542
|
+
} catch (err) {
|
|
543
|
+
res.status(400).json({ error: err.message });
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Search commands
|
|
549
|
+
*/
|
|
550
|
+
router.get('/registry/commands', (req, res) => {
|
|
551
|
+
const results = commandRegistry.search({
|
|
552
|
+
siteId: req.query.siteId,
|
|
553
|
+
category: req.query.category,
|
|
554
|
+
name: req.query.name,
|
|
555
|
+
tag: req.query.tag,
|
|
556
|
+
capability: req.query.capability,
|
|
557
|
+
limit: parseInt(req.query.limit) || 50,
|
|
558
|
+
});
|
|
559
|
+
res.json({ commands: results, total: results.length });
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Register a site
|
|
564
|
+
*/
|
|
565
|
+
router.post('/registry/sites', (req, res) => {
|
|
566
|
+
const { domain, ...info } = req.body;
|
|
567
|
+
if (!domain) return res.status(400).json({ error: 'domain required' });
|
|
568
|
+
const entry = siteRegistry.register(domain, info);
|
|
569
|
+
res.json(entry);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Search sites
|
|
574
|
+
*/
|
|
575
|
+
router.get('/registry/sites', (req, res) => {
|
|
576
|
+
const results = siteRegistry.search({
|
|
577
|
+
tier: req.query.tier,
|
|
578
|
+
capability: req.query.capability,
|
|
579
|
+
name: req.query.name,
|
|
580
|
+
verified: req.query.verified === 'true' ? true : undefined,
|
|
581
|
+
limit: parseInt(req.query.limit) || 50,
|
|
582
|
+
});
|
|
583
|
+
res.json({ sites: results, total: results.length });
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Get site info
|
|
588
|
+
*/
|
|
589
|
+
router.get('/registry/sites/:domain', (req, res) => {
|
|
590
|
+
const site = siteRegistry.getSite(req.params.domain);
|
|
591
|
+
if (!site) return res.status(404).json({ error: 'Site not found' });
|
|
592
|
+
res.json(site);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Register a template
|
|
597
|
+
*/
|
|
598
|
+
router.post('/registry/templates', (req, res) => {
|
|
599
|
+
try {
|
|
600
|
+
const entry = templateRegistry.register(req.body);
|
|
601
|
+
res.json(entry);
|
|
602
|
+
} catch (err) {
|
|
603
|
+
res.status(400).json({ error: err.message });
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Search templates
|
|
609
|
+
*/
|
|
610
|
+
router.get('/registry/templates', (req, res) => {
|
|
611
|
+
const results = templateRegistry.search({
|
|
612
|
+
category: req.query.category,
|
|
613
|
+
name: req.query.name,
|
|
614
|
+
tag: req.query.tag,
|
|
615
|
+
limit: parseInt(req.query.limit) || 50,
|
|
616
|
+
});
|
|
617
|
+
res.json({ templates: results, total: results.length });
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Get template
|
|
622
|
+
*/
|
|
623
|
+
router.get('/registry/templates/:templateId', (req, res) => {
|
|
624
|
+
const tmpl = templateRegistry.getTemplate(req.params.templateId);
|
|
625
|
+
if (!tmpl) return res.status(404).json({ error: 'Template not found' });
|
|
626
|
+
templateRegistry.trackDownload(req.params.templateId);
|
|
627
|
+
res.json(tmpl);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
631
|
+
// LLM
|
|
632
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* LLM completion
|
|
636
|
+
*/
|
|
637
|
+
router.post('/llm/complete', async (req, res) => {
|
|
638
|
+
try {
|
|
639
|
+
const result = await llm.complete(req.body.prompt, req.body.options || req.body);
|
|
640
|
+
metrics.increment('llm.api.requests');
|
|
641
|
+
res.json(result);
|
|
642
|
+
} catch (err) {
|
|
643
|
+
res.status(500).json({ error: err.message });
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* LLM models
|
|
649
|
+
*/
|
|
650
|
+
router.get('/llm/models', (req, res) => {
|
|
651
|
+
res.json({ models: llm.listModels() });
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* LLM status
|
|
656
|
+
*/
|
|
657
|
+
router.get('/llm/status', (req, res) => {
|
|
658
|
+
res.json(llm.getStatus());
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* LLM embeddings
|
|
663
|
+
*/
|
|
664
|
+
router.post('/llm/embed', async (req, res) => {
|
|
665
|
+
try {
|
|
666
|
+
const result = await llm.embed(req.body.text, req.body.options || {});
|
|
667
|
+
res.json(result);
|
|
668
|
+
} catch (err) {
|
|
669
|
+
res.status(500).json({ error: err.message });
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
674
|
+
// COMMAND SIGNING
|
|
675
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Sign a command
|
|
679
|
+
*/
|
|
680
|
+
router.post('/sign', (req, res) => {
|
|
681
|
+
const { payload, agentId } = req.body;
|
|
682
|
+
if (!payload || !agentId) return res.status(400).json({ error: 'payload and agentId required' });
|
|
683
|
+
const signature = signer.sign(payload, agentId);
|
|
684
|
+
res.json(signature);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Verify a signed command
|
|
689
|
+
*/
|
|
690
|
+
router.post('/verify', (req, res) => {
|
|
691
|
+
const { payload, agentId, nonce, timestamp, signature } = req.body;
|
|
692
|
+
const result = signer.verify(payload, agentId, nonce, timestamp, signature);
|
|
693
|
+
res.json(result);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
697
|
+
// EVENT STREAM (SSE)
|
|
698
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Server-Sent Events for real-time updates
|
|
702
|
+
*/
|
|
703
|
+
router.get('/events', (req, res) => {
|
|
704
|
+
res.writeHead(200, {
|
|
705
|
+
'Content-Type': 'text/event-stream',
|
|
706
|
+
'Cache-Control': 'no-cache',
|
|
707
|
+
'Connection': 'keep-alive',
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
const filter = req.query.filter; // e.g., 'task.*' or 'agent.*'
|
|
711
|
+
|
|
712
|
+
const subId = bus.on(filter || '*', (data, meta) => {
|
|
713
|
+
res.write(`event: ${meta.event || 'message'}\n`);
|
|
714
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
req.on('close', () => {
|
|
718
|
+
bus.off(subId);
|
|
719
|
+
res.end();
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
724
|
+
// Protocol Handler Setup
|
|
725
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
726
|
+
|
|
727
|
+
const protocolHandler = new protocol.ProtocolHandler();
|
|
728
|
+
|
|
729
|
+
// Wire protocol commands to runtime
|
|
730
|
+
protocolHandler.handle('wab.discover', async (payload) => {
|
|
731
|
+
const commands = commandRegistry.search({ siteId: payload.siteId, category: payload.category });
|
|
732
|
+
return {
|
|
733
|
+
actions: commands.map(c => ({
|
|
734
|
+
name: c.name,
|
|
735
|
+
category: c.category,
|
|
736
|
+
params: c.input,
|
|
737
|
+
capabilities: c.capabilities,
|
|
738
|
+
})),
|
|
739
|
+
meta: {
|
|
740
|
+
protocol: protocol.PROTOCOL_VERSION,
|
|
741
|
+
timestamp: Date.now(),
|
|
742
|
+
},
|
|
743
|
+
};
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
protocolHandler.handle('wab.execute', async (payload, ctx) => {
|
|
747
|
+
const result = await executor.execute({
|
|
748
|
+
type: 'semantic',
|
|
749
|
+
domain: payload.domain || 'general',
|
|
750
|
+
action: payload.action,
|
|
751
|
+
params: payload.params,
|
|
752
|
+
agentId: ctx.message.agentId,
|
|
753
|
+
});
|
|
754
|
+
return result;
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
protocolHandler.handle('wab.task.submit', async (payload) => {
|
|
758
|
+
return runtime.submitTask(payload);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
protocolHandler.handle('wab.task.status', async (payload) => {
|
|
762
|
+
return runtime.scheduler.getTask(payload.taskId);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
protocolHandler.handle('wab.agent.register', async (payload) => {
|
|
766
|
+
const result = identity.register(payload.name, payload.type, {
|
|
767
|
+
capabilities: payload.capabilities,
|
|
768
|
+
publicKey: payload.publicKey,
|
|
769
|
+
metadata: payload.metadata,
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// Negotiate requested capabilities
|
|
773
|
+
const negotiation = protocol.negotiator.negotiate(
|
|
774
|
+
result.agentId,
|
|
775
|
+
payload.capabilities,
|
|
776
|
+
payload.siteId || '*'
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
return {
|
|
780
|
+
agentId: result.agentId,
|
|
781
|
+
token: result.apiKey,
|
|
782
|
+
grantedCapabilities: negotiation.granted,
|
|
783
|
+
expiresAt: negotiation.grant?.constraints?.expiresAt || Date.now() + 3600_000,
|
|
784
|
+
};
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
protocolHandler.handle('wab.ai.infer', async (payload) => {
|
|
788
|
+
return llm.complete(payload.prompt, {
|
|
789
|
+
model: payload.model,
|
|
790
|
+
provider: payload.provider,
|
|
791
|
+
...payload.options,
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
protocolHandler.handle('wab.commerce.compare', async (payload) => {
|
|
796
|
+
return executor.execute({
|
|
797
|
+
type: 'parallel',
|
|
798
|
+
tasks: (payload.sources || []).map(url => ({
|
|
799
|
+
type: 'extraction',
|
|
800
|
+
params: { url, query: payload.query },
|
|
801
|
+
})),
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
806
|
+
// ADAPTERS
|
|
807
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* List adapters
|
|
811
|
+
*/
|
|
812
|
+
router.get('/adapters', (req, res) => {
|
|
813
|
+
res.json({ adapters: adapterManager.list() });
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Adapter stats
|
|
818
|
+
*/
|
|
819
|
+
router.get('/adapters/stats', (req, res) => {
|
|
820
|
+
res.json(adapterManager.getStats());
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* MCP: list tools
|
|
825
|
+
*/
|
|
826
|
+
router.get('/adapters/mcp/tools', (req, res) => {
|
|
827
|
+
const commands = protocol.schema.listCommands();
|
|
828
|
+
res.json(mcpAdapter.handleListTools(commands));
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* MCP: call tool
|
|
833
|
+
*/
|
|
834
|
+
router.post('/adapters/mcp/call', async (req, res) => {
|
|
835
|
+
try {
|
|
836
|
+
const result = await mcpAdapter.handleCallTool(req.body, async (wapReq) => {
|
|
837
|
+
const request = protocol.createRequest(wapReq.command, wapReq.payload);
|
|
838
|
+
return protocolHandler.process(request);
|
|
839
|
+
});
|
|
840
|
+
res.json(result);
|
|
841
|
+
} catch (err) {
|
|
842
|
+
res.status(500).json({ error: err.message });
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* REST adapter: register endpoint
|
|
848
|
+
*/
|
|
849
|
+
router.post('/adapters/rest/endpoints', (req, res) => {
|
|
850
|
+
try {
|
|
851
|
+
const endpoint = restAdapter.registerEndpoint(req.body.id, req.body);
|
|
852
|
+
res.json(endpoint);
|
|
853
|
+
} catch (err) {
|
|
854
|
+
res.status(400).json({ error: err.message });
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* REST adapter: list endpoints
|
|
860
|
+
*/
|
|
861
|
+
router.get('/adapters/rest/endpoints', (req, res) => {
|
|
862
|
+
res.json({ endpoints: restAdapter.listEndpoints() });
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* REST adapter: execute
|
|
867
|
+
*/
|
|
868
|
+
router.post('/adapters/rest/execute', async (req, res) => {
|
|
869
|
+
try {
|
|
870
|
+
const result = await restAdapter.execute(req.body.endpoint, req.body.params);
|
|
871
|
+
res.json(result);
|
|
872
|
+
} catch (err) {
|
|
873
|
+
res.status(500).json({ error: err.message });
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Browser adapter: list semantic mappings
|
|
879
|
+
*/
|
|
880
|
+
router.get('/adapters/browser/mappings', (req, res) => {
|
|
881
|
+
res.json({ mappings: browserAdapter.listMappings() });
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Browser adapter: resolve semantic action
|
|
886
|
+
*/
|
|
887
|
+
router.post('/adapters/browser/resolve', (req, res) => {
|
|
888
|
+
const { domain, action, params } = req.body;
|
|
889
|
+
const plan = browserAdapter.fromWAP({ domain, action, params });
|
|
890
|
+
if (!plan) return res.status(404).json({ error: 'No mapping for this semantic action' });
|
|
891
|
+
res.json(plan);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Browser adapter: register mapping
|
|
896
|
+
*/
|
|
897
|
+
router.post('/adapters/browser/mappings', (req, res) => {
|
|
898
|
+
const { domainAction, plan } = req.body;
|
|
899
|
+
if (!domainAction || !plan) return res.status(400).json({ error: 'domainAction and plan required' });
|
|
900
|
+
browserAdapter.registerMapping(domainAction, plan);
|
|
901
|
+
res.json({ success: true });
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
905
|
+
// REPLAY ENGINE
|
|
906
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* List recordings
|
|
910
|
+
*/
|
|
911
|
+
router.get('/replay/recordings', (req, res) => {
|
|
912
|
+
res.json({ recordings: replayEngine.listRecordings(parseInt(req.query.limit) || 50) });
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Get recording
|
|
917
|
+
*/
|
|
918
|
+
router.get('/replay/recordings/:taskId', (req, res) => {
|
|
919
|
+
const rec = replayEngine.getRecording(req.params.taskId);
|
|
920
|
+
if (!rec) return res.status(404).json({ error: 'Recording not found' });
|
|
921
|
+
res.json(rec);
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Replay a task
|
|
926
|
+
*/
|
|
927
|
+
router.post('/replay/:taskId', async (req, res) => {
|
|
928
|
+
try {
|
|
929
|
+
const result = await replayEngine.replay(req.params.taskId, {
|
|
930
|
+
verify: req.body.verify !== false,
|
|
931
|
+
continueOnMismatch: !!req.body.continueOnMismatch,
|
|
932
|
+
});
|
|
933
|
+
res.json(result);
|
|
934
|
+
} catch (err) {
|
|
935
|
+
res.status(400).json({ error: err.message });
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Diff two recordings
|
|
941
|
+
*/
|
|
942
|
+
router.get('/replay/diff/:taskId1/:taskId2', (req, res) => {
|
|
943
|
+
const diff = replayEngine.diff(req.params.taskId1, req.params.taskId2);
|
|
944
|
+
if (!diff) return res.status(404).json({ error: 'One or both recordings not found' });
|
|
945
|
+
res.json(diff);
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Replay stats
|
|
950
|
+
*/
|
|
951
|
+
router.get('/replay/stats', (req, res) => {
|
|
952
|
+
res.json(replayEngine.getStats());
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
956
|
+
// SESSION ENGINE
|
|
957
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Create browser session
|
|
961
|
+
*/
|
|
962
|
+
router.post('/sessions', (req, res) => {
|
|
963
|
+
const session = sessionEngine.create(req.body);
|
|
964
|
+
res.json(session);
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* List sessions
|
|
969
|
+
*/
|
|
970
|
+
router.get('/sessions', (req, res) => {
|
|
971
|
+
const sessions = sessionEngine.list({
|
|
972
|
+
agentId: req.query.agentId,
|
|
973
|
+
siteId: req.query.siteId,
|
|
974
|
+
state: req.query.state,
|
|
975
|
+
}, parseInt(req.query.limit) || 50);
|
|
976
|
+
res.json({ sessions, total: sessions.length });
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Get session
|
|
981
|
+
*/
|
|
982
|
+
router.get('/sessions/:sessionId', (req, res) => {
|
|
983
|
+
const session = sessionEngine.get(req.params.sessionId);
|
|
984
|
+
if (!session) return res.status(404).json({ error: 'Session not found or expired' });
|
|
985
|
+
res.json(session);
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* Export session
|
|
990
|
+
*/
|
|
991
|
+
router.get('/sessions/:sessionId/export', (req, res) => {
|
|
992
|
+
const data = sessionEngine.export(req.params.sessionId);
|
|
993
|
+
if (!data) return res.status(404).json({ error: 'Session not found' });
|
|
994
|
+
res.json(data);
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Import session
|
|
999
|
+
*/
|
|
1000
|
+
router.post('/sessions/import', (req, res) => {
|
|
1001
|
+
const session = sessionEngine.import(req.body);
|
|
1002
|
+
res.json(session);
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Set cookies
|
|
1007
|
+
*/
|
|
1008
|
+
router.post('/sessions/:sessionId/cookies', (req, res) => {
|
|
1009
|
+
sessionEngine.setCookies(req.params.sessionId, req.body.cookies || []);
|
|
1010
|
+
res.json({ success: true });
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Get cookies
|
|
1015
|
+
*/
|
|
1016
|
+
router.get('/sessions/:sessionId/cookies', (req, res) => {
|
|
1017
|
+
const cookies = sessionEngine.getCookies(req.params.sessionId, req.query.domain);
|
|
1018
|
+
res.json({ cookies });
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Set storage
|
|
1023
|
+
*/
|
|
1024
|
+
router.post('/sessions/:sessionId/storage', (req, res) => {
|
|
1025
|
+
const { key, value, type } = req.body;
|
|
1026
|
+
sessionEngine.setStorage(req.params.sessionId, key, value, type);
|
|
1027
|
+
res.json({ success: true });
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Destroy session
|
|
1032
|
+
*/
|
|
1033
|
+
router.delete('/sessions/:sessionId', (req, res) => {
|
|
1034
|
+
sessionEngine.destroy(req.params.sessionId);
|
|
1035
|
+
res.json({ success: true });
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1039
|
+
// FAILURE ANALYSIS
|
|
1040
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Query failures
|
|
1044
|
+
*/
|
|
1045
|
+
router.get('/failures', (req, res) => {
|
|
1046
|
+
const failures = failureAnalyzer.query({
|
|
1047
|
+
classification: req.query.classification,
|
|
1048
|
+
severity: req.query.severity,
|
|
1049
|
+
agentId: req.query.agentId,
|
|
1050
|
+
taskId: req.query.taskId,
|
|
1051
|
+
retryable: req.query.retryable === 'true' ? true : req.query.retryable === 'false' ? false : undefined,
|
|
1052
|
+
since: parseInt(req.query.since) || undefined,
|
|
1053
|
+
}, parseInt(req.query.limit) || 50);
|
|
1054
|
+
res.json({ failures, total: failures.length });
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Get failure
|
|
1059
|
+
*/
|
|
1060
|
+
router.get('/failures/:failureId', (req, res) => {
|
|
1061
|
+
const failure = failureAnalyzer.getFailure(req.params.failureId);
|
|
1062
|
+
if (!failure) return res.status(404).json({ error: 'Failure not found' });
|
|
1063
|
+
res.json(failure);
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Get failure patterns
|
|
1068
|
+
*/
|
|
1069
|
+
router.get('/failures/analysis/patterns', (req, res) => {
|
|
1070
|
+
res.json({ patterns: failureAnalyzer.getPatterns() });
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Get failure summary
|
|
1075
|
+
*/
|
|
1076
|
+
router.get('/failures/analysis/summary', (req, res) => {
|
|
1077
|
+
res.json(failureAnalyzer.getSummary(parseInt(req.query.since) || 0));
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Classify a failure manually
|
|
1082
|
+
*/
|
|
1083
|
+
router.post('/failures/classify', (req, res) => {
|
|
1084
|
+
const { error, context } = req.body;
|
|
1085
|
+
if (!error) return res.status(400).json({ error: 'error object required' });
|
|
1086
|
+
const classification = failureAnalyzer.classify(error, context || {});
|
|
1087
|
+
res.json(classification);
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1091
|
+
// CERTIFICATION
|
|
1092
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Verify a site
|
|
1096
|
+
*/
|
|
1097
|
+
router.post('/certification/verify', async (req, res) => {
|
|
1098
|
+
try {
|
|
1099
|
+
const { domain, probeData } = req.body;
|
|
1100
|
+
if (!domain) return res.status(400).json({ error: 'domain required' });
|
|
1101
|
+
const result = await certificationEngine.verify(domain, probeData || {});
|
|
1102
|
+
res.json(result);
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
res.status(500).json({ error: err.message });
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Get certificate
|
|
1110
|
+
*/
|
|
1111
|
+
router.get('/certification/:domain', (req, res) => {
|
|
1112
|
+
const cert = certificationEngine.getCertificate(req.params.domain);
|
|
1113
|
+
if (!cert) return res.status(404).json({ error: 'No active certificate for this domain' });
|
|
1114
|
+
res.json(cert);
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* List certificates
|
|
1119
|
+
*/
|
|
1120
|
+
router.get('/certification', (req, res) => {
|
|
1121
|
+
const certs = certificationEngine.listCertificates({
|
|
1122
|
+
level: req.query.level,
|
|
1123
|
+
minScore: parseInt(req.query.minScore) || undefined,
|
|
1124
|
+
}, parseInt(req.query.limit) || 50);
|
|
1125
|
+
res.json({ certificates: certs, total: certs.length });
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Revoke certificate
|
|
1130
|
+
*/
|
|
1131
|
+
router.delete('/certification/:domain', (req, res) => {
|
|
1132
|
+
certificationEngine.revoke(req.params.domain);
|
|
1133
|
+
res.json({ success: true });
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
module.exports = router;
|