overmind-mcp 2.8.12 → 2.8.14
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/dist/bin/launch.js +78 -0
- package/dist/bin/overmind-bridge.d.ts +42 -0
- package/dist/bin/overmind-bridge.d.ts.map +1 -0
- package/dist/bin/overmind-bridge.js +503 -0
- package/dist/bin/overmind-bridge.js.map +1 -0
- package/dist/bridge/AgentRegistry.d.ts +123 -0
- package/dist/bridge/AgentRegistry.d.ts.map +1 -0
- package/dist/bridge/AgentRegistry.js +207 -0
- package/dist/bridge/AgentRegistry.js.map +1 -0
- package/dist/bridge/ArgParser.d.ts +45 -0
- package/dist/bridge/ArgParser.d.ts.map +1 -0
- package/dist/bridge/ArgParser.js +134 -0
- package/dist/bridge/ArgParser.js.map +1 -0
- package/dist/bridge/BridgeHttpClient.d.ts +61 -0
- package/dist/bridge/BridgeHttpClient.d.ts.map +1 -0
- package/dist/bridge/BridgeHttpClient.js +164 -0
- package/dist/bridge/BridgeHttpClient.js.map +1 -0
- package/dist/bridge/DirectiveParser.d.ts +82 -0
- package/dist/bridge/DirectiveParser.d.ts.map +1 -0
- package/dist/bridge/DirectiveParser.js +154 -0
- package/dist/bridge/DirectiveParser.js.map +1 -0
- package/dist/bridge/JsonSanitizer.d.ts +34 -0
- package/dist/bridge/JsonSanitizer.d.ts.map +1 -0
- package/dist/bridge/JsonSanitizer.js +90 -0
- package/dist/bridge/JsonSanitizer.js.map +1 -0
- package/dist/bridge/MessageLog.d.ts +142 -0
- package/dist/bridge/MessageLog.d.ts.map +1 -0
- package/dist/bridge/MessageLog.js +311 -0
- package/dist/bridge/MessageLog.js.map +1 -0
- package/dist/bridge/OverBridgeServer.d.ts +179 -0
- package/dist/bridge/OverBridgeServer.d.ts.map +1 -0
- package/dist/bridge/OverBridgeServer.js +982 -0
- package/dist/bridge/OverBridgeServer.js.map +1 -0
- package/dist/bridge/PromptSource.d.ts +66 -0
- package/dist/bridge/PromptSource.d.ts.map +1 -0
- package/dist/bridge/PromptSource.js +152 -0
- package/dist/bridge/PromptSource.js.map +1 -0
- package/dist/bridge/RequestContext.d.ts +19 -0
- package/dist/bridge/RequestContext.d.ts.map +1 -0
- package/dist/bridge/RequestContext.js +34 -0
- package/dist/bridge/RequestContext.js.map +1 -0
- package/dist/bridge/ScenarioLoader.d.ts +124 -0
- package/dist/bridge/ScenarioLoader.d.ts.map +1 -0
- package/dist/bridge/ScenarioLoader.js +333 -0
- package/dist/bridge/ScenarioLoader.js.map +1 -0
- package/dist/bridge/SessionStore.d.ts +109 -0
- package/dist/bridge/SessionStore.d.ts.map +1 -0
- package/dist/bridge/SessionStore.js +220 -0
- package/dist/bridge/SessionStore.js.map +1 -0
- package/dist/bridge/WebhookAdapter.d.ts +76 -0
- package/dist/bridge/WebhookAdapter.d.ts.map +1 -0
- package/dist/bridge/WebhookAdapter.js +186 -0
- package/dist/bridge/WebhookAdapter.js.map +1 -0
- package/dist/bridge/index.d.ts +23 -1
- package/dist/bridge/index.d.ts.map +1 -1
- package/dist/bridge/index.js +19 -1
- package/dist/bridge/index.js.map +1 -1
- package/dist/bridge/utils.d.ts +9 -0
- package/dist/bridge/utils.d.ts.map +1 -1
- package/dist/bridge/utils.js +17 -0
- package/dist/bridge/utils.js.map +1 -1
- package/dist/services/AgentManager.d.ts.map +1 -1
- package/dist/services/AgentManager.js +16 -3
- package/dist/services/AgentManager.js.map +1 -1
- package/dist/services/NousHermesRunner.d.ts.map +1 -1
- package/dist/services/NousHermesRunner.js +47 -8
- package/dist/services/NousHermesRunner.js.map +1 -1
- package/dist/tools/create_agent.d.ts.map +1 -1
- package/dist/tools/create_agent.js +11 -0
- package/dist/tools/create_agent.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ╔══════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
* ║ OVERMIND BRIDGE — OverBridgeServer (HTTP JSON-RPC 2.0 Entry Point)║
|
|
4
|
+
* ║ ║
|
|
5
|
+
* ║ Serveur HTTP qui expose l'API Overmind Bridge via JSON-RPC 2.0. ║
|
|
6
|
+
* ║ Permet à n'importe quel client (curl, Python, fetch...) de : ║
|
|
7
|
+
* ║ - Parler à un agent ║
|
|
8
|
+
* ║ - Faire parler des agents entre eux (A2A) ║
|
|
9
|
+
* ║ - Suivre l'état live des agents (busy/idle/online) ║
|
|
10
|
+
* ║ - Consulter l'historique persistant des messages ║
|
|
11
|
+
* ║ ║
|
|
12
|
+
* ║ ARCHITECTURE ║
|
|
13
|
+
* ║ ───────────── ║
|
|
14
|
+
* ║ Client HTTP → POST /rpc → JSON-RPC dispatcher ║
|
|
15
|
+
* ║ → OverBridgeService (deja wrappe MCP) ║
|
|
16
|
+
* ║ → AgentRegistry (etat live) ║
|
|
17
|
+
* ║ → MessageLog (persistence Postgres) ║
|
|
18
|
+
* ║ → Overmind MCP :3099 ║
|
|
19
|
+
* ╚══════════════════════════════════════════════════════════════════════╝
|
|
20
|
+
*/
|
|
21
|
+
import http from 'node:http';
|
|
22
|
+
import { URL } from 'node:url';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import { z } from 'zod';
|
|
26
|
+
import { AgentRegistry } from './AgentRegistry.js';
|
|
27
|
+
import { MessageLog } from './MessageLog.js';
|
|
28
|
+
import { SessionStore } from './SessionStore.js';
|
|
29
|
+
import { DirectiveParser } from './DirectiveParser.js';
|
|
30
|
+
import { WebhookAdapter } from './WebhookAdapter.js';
|
|
31
|
+
import { sanitizeAndParse, looksLikeWindowsPathIssue } from './JsonSanitizer.js';
|
|
32
|
+
import { getOrCreateRequestId } from './RequestContext.js';
|
|
33
|
+
import { createBridgeLogger, validateAgentName } from './utils.js';
|
|
34
|
+
const JSON_RPC_ERRORS = {
|
|
35
|
+
PARSE_ERROR: { code: -32700, message: 'Parse error' },
|
|
36
|
+
INVALID_REQUEST: { code: -32600, message: 'Invalid Request' },
|
|
37
|
+
METHOD_NOT_FOUND: { code: -32601, message: 'Method not found' },
|
|
38
|
+
INVALID_PARAMS: { code: -32602, message: 'Invalid params' },
|
|
39
|
+
INTERNAL_ERROR: { code: -32603, message: 'Internal error' },
|
|
40
|
+
AGENT_BUSY: { code: -32001, message: 'Agent is busy' },
|
|
41
|
+
AGENT_OFFLINE: { code: -32002, message: 'Agent is offline' },
|
|
42
|
+
};
|
|
43
|
+
// ─── Zod Schemas (validation des params par méthode) ─────────────────────
|
|
44
|
+
const RunAgentParams = z.object({
|
|
45
|
+
agentName: z.string().min(1),
|
|
46
|
+
runner: z.string().min(1),
|
|
47
|
+
prompt: z.string().min(1),
|
|
48
|
+
sessionId: z.string().optional(),
|
|
49
|
+
path: z.string().optional(),
|
|
50
|
+
model: z.string().optional(),
|
|
51
|
+
mode: z.string().optional(),
|
|
52
|
+
silent: z.boolean().optional(),
|
|
53
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
54
|
+
/** Clé externe pour SessionStore (phone, userId, etc.) — auto-résolution de sessionId */
|
|
55
|
+
externalKey: z.string().optional(),
|
|
56
|
+
/** Active le parsing de directives dans la réponse (default: true si enableDirectives) */
|
|
57
|
+
parseDirectives: z.boolean().optional(),
|
|
58
|
+
});
|
|
59
|
+
const A2AParams = z.object({
|
|
60
|
+
fromAgent: z.string().min(1),
|
|
61
|
+
toAgent: z.string().min(1),
|
|
62
|
+
runner: z.string().min(1),
|
|
63
|
+
prompt: z.string().min(1),
|
|
64
|
+
model: z.string().optional(),
|
|
65
|
+
path: z.string().optional(),
|
|
66
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
67
|
+
});
|
|
68
|
+
const AgentStatusParams = z.object({
|
|
69
|
+
agentName: z.string().min(1),
|
|
70
|
+
runner: z.string().optional(),
|
|
71
|
+
action: z.enum(['status', 'stream', 'kill', 'wait']).default('status'),
|
|
72
|
+
sinceTimestamp: z.number().optional(),
|
|
73
|
+
timeoutMs: z.number().optional(),
|
|
74
|
+
});
|
|
75
|
+
const ListAgentsParams = z.object({
|
|
76
|
+
status: z.enum(['online', 'offline', 'busy', 'idle']).optional(),
|
|
77
|
+
runner: z.string().optional(),
|
|
78
|
+
}).default({});
|
|
79
|
+
const MessageHistoryParams = z.object({
|
|
80
|
+
toAgent: z.string().optional(),
|
|
81
|
+
fromAgent: z.string().nullable().optional(),
|
|
82
|
+
status: z.enum(['pending', 'running', 'done', 'failed', 'timeout']).optional(),
|
|
83
|
+
limit: z.number().int().min(1).max(500).default(50),
|
|
84
|
+
offset: z.number().int().min(0).default(0),
|
|
85
|
+
sinceHours: z.number().positive().optional(),
|
|
86
|
+
});
|
|
87
|
+
const MessageGetParams = z.object({
|
|
88
|
+
id: z.string().uuid(),
|
|
89
|
+
});
|
|
90
|
+
const MessageReplayParams = z.object({
|
|
91
|
+
id: z.string().uuid(),
|
|
92
|
+
});
|
|
93
|
+
// ─── OverBridgeServer ─────────────────────────────────────────────────────
|
|
94
|
+
export class OverBridgeServer {
|
|
95
|
+
service;
|
|
96
|
+
registry;
|
|
97
|
+
log;
|
|
98
|
+
messageLog;
|
|
99
|
+
sessions;
|
|
100
|
+
directiveParser;
|
|
101
|
+
webhookAdapter;
|
|
102
|
+
config;
|
|
103
|
+
server;
|
|
104
|
+
startTime = 0;
|
|
105
|
+
constructor(service, config, logger) {
|
|
106
|
+
this.service = service;
|
|
107
|
+
this.config = config;
|
|
108
|
+
this.log = logger ?? createBridgeLogger('overbridge-server');
|
|
109
|
+
this.registry = new AgentRegistry(this.log);
|
|
110
|
+
this.directiveParser = new DirectiveParser({ logger: this.log });
|
|
111
|
+
this.webhookAdapter = new WebhookAdapter({ logger: this.log });
|
|
112
|
+
if (config.enableMessageLog) {
|
|
113
|
+
this.messageLog = new MessageLog(config.postgres, this.log);
|
|
114
|
+
}
|
|
115
|
+
if (config.enableSessionStore) {
|
|
116
|
+
const defaultPath = path.join(process.env.HOME ?? process.env.USERPROFILE ?? '.', '.overmind', 'bridge', 'sessions.json');
|
|
117
|
+
this.sessions = new SessionStore({
|
|
118
|
+
persistPath: config.sessionStorePath ?? defaultPath,
|
|
119
|
+
ttlMs: config.sessionTtlMs,
|
|
120
|
+
}, this.log);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// ─── Lifecycle ───────────────────────────────────────────────────────────
|
|
124
|
+
/**
|
|
125
|
+
* Démarre le serveur HTTP et initialise les dépendances (MessageLog, SessionStore, OverBridgeService).
|
|
126
|
+
*/
|
|
127
|
+
async start() {
|
|
128
|
+
this.startTime = Date.now();
|
|
129
|
+
// 1) Init MessageLog (Postgres)
|
|
130
|
+
if (this.messageLog) {
|
|
131
|
+
await this.messageLog.init();
|
|
132
|
+
}
|
|
133
|
+
// 2) Init SessionStore
|
|
134
|
+
if (this.sessions) {
|
|
135
|
+
await this.sessions.init();
|
|
136
|
+
}
|
|
137
|
+
// 3) Connect OverBridgeService (MCP healthcheck)
|
|
138
|
+
try {
|
|
139
|
+
const status = await this.service.connect(this.config.healthCheckIntervalMs);
|
|
140
|
+
this.log.info(`🔌 OverBridgeService connected: ${status.status}`);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
this.log.warn(`⚠️ OverBridgeService connect failed: ${err.message}`);
|
|
144
|
+
// On continue quand même, le serveur répondra avec erreurs si MCP down
|
|
145
|
+
}
|
|
146
|
+
// 4) Démarre le serveur HTTP
|
|
147
|
+
this.server = http.createServer((req, res) => this.handleRequest(req, res));
|
|
148
|
+
await new Promise((resolve) => {
|
|
149
|
+
this.server.listen(this.config.port, this.config.host, () => resolve());
|
|
150
|
+
});
|
|
151
|
+
const addr = this.server.address();
|
|
152
|
+
const port = typeof addr === 'object' && addr ? addr.port : this.config.port;
|
|
153
|
+
const url = `http://${this.config.host}:${port}`;
|
|
154
|
+
this.log.info(`🚀 OverBridgeServer listening on ${url}`);
|
|
155
|
+
this.log.info(` POST ${url}/rpc (JSON-RPC 2.0)`);
|
|
156
|
+
this.log.info(` GET ${url}/health`);
|
|
157
|
+
if (this.config.enableWebhooks) {
|
|
158
|
+
this.log.info(` POST ${url}/webhook/:provider (voipms, twilio, discord, generic)`);
|
|
159
|
+
}
|
|
160
|
+
if (this.sessions) {
|
|
161
|
+
this.log.info(` SessionStore: enabled (TTL ${(this.config.sessionTtlMs ?? 14_400_000) / 1000}s)`);
|
|
162
|
+
}
|
|
163
|
+
if (this.config.enableDirectives) {
|
|
164
|
+
this.log.info(` DirectiveParser: enabled (SESSION_ID, CONTEXT_UPDATE, BRIDGE_NEXT)`);
|
|
165
|
+
}
|
|
166
|
+
return { port, host: this.config.host, url };
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Ferme proprement le serveur HTTP et les dépendances.
|
|
170
|
+
*/
|
|
171
|
+
async stop() {
|
|
172
|
+
if (this.server) {
|
|
173
|
+
await new Promise((resolve) => this.server.close(() => resolve()));
|
|
174
|
+
this.server = undefined;
|
|
175
|
+
this.log.info('🛑 HTTP server closed');
|
|
176
|
+
}
|
|
177
|
+
this.service.disconnect();
|
|
178
|
+
if (this.messageLog) {
|
|
179
|
+
await this.messageLog.close();
|
|
180
|
+
}
|
|
181
|
+
if (this.sessions) {
|
|
182
|
+
await this.sessions.close();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Expose le registry (pour tests / inspection).
|
|
187
|
+
*/
|
|
188
|
+
get agentRegistry() {
|
|
189
|
+
return this.registry;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Expose le MessageLog (pour tests / inspection).
|
|
193
|
+
*/
|
|
194
|
+
get messages() {
|
|
195
|
+
return this.messageLog;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Expose le SessionStore (pour tests / inspection).
|
|
199
|
+
*/
|
|
200
|
+
get sessionStore() {
|
|
201
|
+
return this.sessions;
|
|
202
|
+
}
|
|
203
|
+
// ─── HTTP Request Handler ───────────────────────────────────────────────
|
|
204
|
+
async handleRequest(req, res) {
|
|
205
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
206
|
+
const reqId = getOrCreateRequestId(req.headers);
|
|
207
|
+
// Set reqId header in response
|
|
208
|
+
res.setHeader('X-Request-Id', reqId);
|
|
209
|
+
// CORS preflight
|
|
210
|
+
if (req.method === 'OPTIONS') {
|
|
211
|
+
this.writeCors(res);
|
|
212
|
+
res.writeHead(204);
|
|
213
|
+
res.end();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
this.writeCors(res);
|
|
217
|
+
try {
|
|
218
|
+
// Health check simple
|
|
219
|
+
if (req.method === 'GET' && url.pathname === '/health') {
|
|
220
|
+
await this.handleHealth(res);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// JSON-RPC endpoint
|
|
224
|
+
if (req.method === 'POST' && url.pathname === '/rpc') {
|
|
225
|
+
await this.handleRpc(req, res, reqId);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// Webhook endpoint (si activé) — /webhook/:provider
|
|
229
|
+
if (this.config.enableWebhooks && req.method === 'POST' && url.pathname.startsWith('/webhook/')) {
|
|
230
|
+
await this.handleWebhook(req, res, url, reqId);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// File serve — /f/:filename (statique, comme bt-sms)
|
|
234
|
+
if (req.method === 'GET' && url.pathname.startsWith('/f/')) {
|
|
235
|
+
await this.handleStaticFile(req, res, url);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
// 404
|
|
239
|
+
this.writeJson(res, 404, { error: 'Not found', path: url.pathname, reqId });
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
this.log.error(`💥 Unhandled error: ${err.message} (reqId=${reqId})`);
|
|
243
|
+
this.writeJson(res, 500, { error: 'Internal server error', reqId });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
writeCors(res) {
|
|
247
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
248
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
|
249
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
250
|
+
}
|
|
251
|
+
writeJson(res, status, body) {
|
|
252
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
253
|
+
res.end(JSON.stringify(body));
|
|
254
|
+
}
|
|
255
|
+
// ─── /webhook/:provider Endpoint ─────────────────────────────────────────
|
|
256
|
+
async handleWebhook(req, res, url, reqId) {
|
|
257
|
+
// /webhook/:provider — extrait le provider du path
|
|
258
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
259
|
+
const provider = (parts[1] ?? 'voipms');
|
|
260
|
+
const raw = await this.readBody(req);
|
|
261
|
+
let payload;
|
|
262
|
+
try {
|
|
263
|
+
payload = JSON.parse(raw);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// Tente sanitizer (VoIP.ms envoie parfois du form-encoded mal formé)
|
|
267
|
+
try {
|
|
268
|
+
payload = sanitizeAndParse(raw);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
this.writeJson(res, 400, { error: 'Invalid JSON body', reqId });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Adapte via WebhookAdapter
|
|
276
|
+
const adapter = new WebhookAdapter({ provider, logger: this.log });
|
|
277
|
+
let normalized;
|
|
278
|
+
try {
|
|
279
|
+
normalized = adapter.adapt(payload);
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
this.writeJson(res, 400, { error: err.message, reqId });
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// Auto-dispatch vers agent.run via internal call
|
|
286
|
+
this.log.info(`[reqId=${reqId}] 📨 Webhook ${provider} from ${normalized.externalKey}`);
|
|
287
|
+
// Cherche un agent par défaut (config-driven ou fallback premier agent)
|
|
288
|
+
// Pour l'instant on retourne le normalized et on log ; les utilisateurs
|
|
289
|
+
// peuvent soit :
|
|
290
|
+
// 1. Définir une config par-provider pour auto-dispatch
|
|
291
|
+
// 2. Appeler manuellement /rpc agent.run après réception
|
|
292
|
+
this.writeJson(res, 200, {
|
|
293
|
+
received: true,
|
|
294
|
+
reqId,
|
|
295
|
+
provider,
|
|
296
|
+
externalKey: normalized.externalKey,
|
|
297
|
+
promptPreview: normalized.prompt.slice(0, 200),
|
|
298
|
+
mediaCount: normalized.mediaUrls.length,
|
|
299
|
+
metadata: normalized.metadata,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
// ─── /f/:filename Static File Serve ──────────────────────────────────────
|
|
303
|
+
async handleStaticFile(_req, res, url) {
|
|
304
|
+
const filename = url.pathname.slice(3); // strip /f/
|
|
305
|
+
if (!filename || filename.includes('..')) {
|
|
306
|
+
this.writeJson(res, 400, { error: 'Invalid filename' });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
// Le dossier est configuré via BRIDGE_STATIC_DIR env ou défaut './public'
|
|
310
|
+
const staticDir = process.env.BRIDGE_STATIC_DIR ?? './public';
|
|
311
|
+
const filepath = path.join(staticDir, filename);
|
|
312
|
+
if (!fs.existsSync(filepath)) {
|
|
313
|
+
this.writeJson(res, 404, { error: 'Not found' });
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const ext = path.extname(filepath).toLowerCase();
|
|
317
|
+
const mimeMap = {
|
|
318
|
+
'.html': 'text/html', '.pdf': 'application/pdf', '.png': 'image/png',
|
|
319
|
+
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.json': 'application/json',
|
|
320
|
+
'.txt': 'text/plain', '.mp3': 'audio/mpeg', '.mp4': 'video/mp4',
|
|
321
|
+
'.gif': 'image/gif', '.wav': 'audio/wav', '.xml': 'text/xml',
|
|
322
|
+
};
|
|
323
|
+
res.setHeader('Content-Type', mimeMap[ext] ?? 'application/octet-stream');
|
|
324
|
+
fs.createReadStream(filepath).pipe(res);
|
|
325
|
+
}
|
|
326
|
+
// ─── /health Endpoint ───────────────────────────────────────────────────
|
|
327
|
+
async handleHealth(res) {
|
|
328
|
+
const mcpHealth = await this.service.proxyAccess.healthCheck();
|
|
329
|
+
const regStats = this.registry.stats();
|
|
330
|
+
let msgStats = null;
|
|
331
|
+
if (this.messageLog) {
|
|
332
|
+
try {
|
|
333
|
+
msgStats = await this.messageLog.stats();
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
msgStats = null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const sessionStats = this.sessions ? this.sessions.stats() : null;
|
|
340
|
+
this.writeJson(res, 200, {
|
|
341
|
+
status: mcpHealth.status,
|
|
342
|
+
uptime: Date.now() - this.startTime,
|
|
343
|
+
mcp: mcpHealth,
|
|
344
|
+
agents: regStats,
|
|
345
|
+
messages: msgStats,
|
|
346
|
+
sessions: sessionStats,
|
|
347
|
+
features: {
|
|
348
|
+
sessionStore: !!this.sessions,
|
|
349
|
+
directives: !!this.config.enableDirectives,
|
|
350
|
+
webhooks: !!this.config.enableWebhooks,
|
|
351
|
+
sanitizeJson: !!this.config.sanitizeJson,
|
|
352
|
+
messageLog: !!this.messageLog,
|
|
353
|
+
},
|
|
354
|
+
version: '1.1.0',
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
// ─── /rpc Endpoint (JSON-RPC 2.0 Dispatcher) ────────────────────────────
|
|
358
|
+
async handleRpc(req, res, reqId) {
|
|
359
|
+
// Auth check
|
|
360
|
+
if (this.config.authToken) {
|
|
361
|
+
const auth = req.headers.authorization;
|
|
362
|
+
if (auth !== `Bearer ${this.config.authToken}`) {
|
|
363
|
+
this.writeJson(res, 401, {
|
|
364
|
+
jsonrpc: '2.0',
|
|
365
|
+
id: null,
|
|
366
|
+
error: { code: -32000, message: 'Unauthorized', data: { reqId } },
|
|
367
|
+
});
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// Body parsing
|
|
372
|
+
const raw = await this.readBody(req);
|
|
373
|
+
let parsed;
|
|
374
|
+
try {
|
|
375
|
+
// Try direct parse first
|
|
376
|
+
parsed = JSON.parse(raw);
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
// Si sanitizer activé et body ressemble à un Windows path, on tente la repair
|
|
380
|
+
if (this.config.sanitizeJson && looksLikeWindowsPathIssue(raw)) {
|
|
381
|
+
try {
|
|
382
|
+
parsed = sanitizeAndParse(raw);
|
|
383
|
+
this.log.warn(`[reqId=${reqId}] ⚠️ JSON body sanitized (Windows path issue)`);
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
this.respondError(res, null, JSON_RPC_ERRORS.PARSE_ERROR);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
this.respondError(res, null, JSON_RPC_ERRORS.PARSE_ERROR);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// Inject reqId into all calls (pour corrélation)
|
|
396
|
+
const injectReqId = (reqs) => {
|
|
397
|
+
const inject = (r) => {
|
|
398
|
+
if (!r.params)
|
|
399
|
+
r.params = {};
|
|
400
|
+
r.params.__reqId = reqId;
|
|
401
|
+
};
|
|
402
|
+
if (Array.isArray(reqs))
|
|
403
|
+
reqs.forEach(inject);
|
|
404
|
+
else
|
|
405
|
+
inject(reqs);
|
|
406
|
+
};
|
|
407
|
+
injectReqId(parsed);
|
|
408
|
+
// Batch ou single
|
|
409
|
+
if (Array.isArray(parsed)) {
|
|
410
|
+
if (parsed.length === 0) {
|
|
411
|
+
this.respondError(res, null, JSON_RPC_ERRORS.INVALID_REQUEST);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const responses = await Promise.all(parsed.map((r) => this.dispatchRpc(r).catch((e) => this.buildErrorResponse(r.id ?? null, e))));
|
|
415
|
+
this.writeJson(res, 200, responses);
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
const response = await this.dispatchRpc(parsed);
|
|
419
|
+
this.writeJson(res, 200, response);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Dispatch une requête JSON-RPC vers la bonne méthode.
|
|
424
|
+
*/
|
|
425
|
+
async dispatchRpc(req) {
|
|
426
|
+
// Validation de base JSON-RPC 2.0
|
|
427
|
+
if (req.jsonrpc !== '2.0' || !req.method) {
|
|
428
|
+
return { jsonrpc: '2.0', id: req.id ?? null, error: JSON_RPC_ERRORS.INVALID_REQUEST };
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
switch (req.method) {
|
|
432
|
+
case 'agent.run':
|
|
433
|
+
return this.methodAgentRun(req);
|
|
434
|
+
case 'agent.a2a':
|
|
435
|
+
return this.methodAgentA2A(req);
|
|
436
|
+
case 'agent.status':
|
|
437
|
+
return this.methodAgentStatus(req);
|
|
438
|
+
case 'agent.list':
|
|
439
|
+
return this.methodAgentList(req);
|
|
440
|
+
case 'agent.kill':
|
|
441
|
+
return this.methodAgentKill(req);
|
|
442
|
+
case 'message.history':
|
|
443
|
+
return this.methodMessageHistory(req);
|
|
444
|
+
case 'message.get':
|
|
445
|
+
return this.methodMessageGet(req);
|
|
446
|
+
case 'message.replay':
|
|
447
|
+
return this.methodMessageReplay(req);
|
|
448
|
+
case 'message.stats':
|
|
449
|
+
return this.methodMessageStats(req);
|
|
450
|
+
case 'session.get':
|
|
451
|
+
return this.methodSessionGet(req);
|
|
452
|
+
case 'session.list':
|
|
453
|
+
return this.methodSessionList(req);
|
|
454
|
+
case 'session.delete':
|
|
455
|
+
return this.methodSessionDelete(req);
|
|
456
|
+
case 'session.stats':
|
|
457
|
+
return this.methodSessionStats(req);
|
|
458
|
+
case 'webhook.sms':
|
|
459
|
+
return this.methodWebhookSms(req);
|
|
460
|
+
case 'health.ping':
|
|
461
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result: { pong: true, ts: Date.now() } };
|
|
462
|
+
default:
|
|
463
|
+
return {
|
|
464
|
+
jsonrpc: '2.0',
|
|
465
|
+
id: req.id ?? null,
|
|
466
|
+
error: { ...JSON_RPC_ERRORS.METHOD_NOT_FOUND, data: { method: req.method } },
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
catch (err) {
|
|
471
|
+
this.log.error(`💥 ${req.method} failed: ${err.message}`);
|
|
472
|
+
return this.buildErrorResponse(req.id ?? null, err);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// ─── RPC Methods ─────────────────────────────────────────────────────────
|
|
476
|
+
/**
|
|
477
|
+
* agent.run — Lance un agent (from client externe OU from autre agent).
|
|
478
|
+
* Avec support SessionStore (externalKey) et DirectiveParser.
|
|
479
|
+
*/
|
|
480
|
+
async methodAgentRun(req) {
|
|
481
|
+
const params = this.validateParams(RunAgentParams, req.params, req.id);
|
|
482
|
+
if (!params.ok)
|
|
483
|
+
return params.response;
|
|
484
|
+
const { agentName, runner, prompt, sessionId, path, model, mode, silent, metadata, externalKey, parseDirectives } = params.data;
|
|
485
|
+
const validatedAgentName = validateAgentName(agentName);
|
|
486
|
+
const reqId = req.params?.__reqId;
|
|
487
|
+
// Register
|
|
488
|
+
this.registry.register(validatedAgentName, runner);
|
|
489
|
+
// SessionStore resolution (si externalKey fourni)
|
|
490
|
+
let effectiveSessionId = sessionId;
|
|
491
|
+
if (this.sessions && externalKey) {
|
|
492
|
+
const stored = this.sessions.get(externalKey, validatedAgentName);
|
|
493
|
+
if (stored) {
|
|
494
|
+
effectiveSessionId = stored.sessionId;
|
|
495
|
+
this.log.info(`[reqId=${reqId}] 🔗 Session restored for ${externalKey} → ${effectiveSessionId}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// Persist (pending)
|
|
499
|
+
let messageId;
|
|
500
|
+
if (this.messageLog) {
|
|
501
|
+
messageId = await this.messageLog.create({
|
|
502
|
+
fromAgent: metadata?.fromAgent ?? null,
|
|
503
|
+
toAgent: validatedAgentName,
|
|
504
|
+
runner: runner,
|
|
505
|
+
prompt,
|
|
506
|
+
sessionId: effectiveSessionId,
|
|
507
|
+
metadata: { ...(metadata ?? {}), ...(reqId ? { reqId } : {}), ...(externalKey ? { externalKey } : {}) },
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
// Run avec mutex (1 run par agent à la fois)
|
|
511
|
+
const result = await this.registry.withLock(validatedAgentName, async () => {
|
|
512
|
+
this.registry.markBusy(validatedAgentName, effectiveSessionId);
|
|
513
|
+
if (messageId && this.messageLog) {
|
|
514
|
+
await this.messageLog.markRunning(messageId, effectiveSessionId);
|
|
515
|
+
}
|
|
516
|
+
try {
|
|
517
|
+
const agentResult = await this.service.runAgent({
|
|
518
|
+
runner: runner,
|
|
519
|
+
prompt,
|
|
520
|
+
agentName: validatedAgentName,
|
|
521
|
+
sessionId: effectiveSessionId,
|
|
522
|
+
path,
|
|
523
|
+
model,
|
|
524
|
+
mode,
|
|
525
|
+
silent,
|
|
526
|
+
});
|
|
527
|
+
const rawResponseText = agentResult.content.map((c) => c.text).join('\n');
|
|
528
|
+
// ─── Directive parsing ─────────────────────────────────────────
|
|
529
|
+
let cleanText = rawResponseText;
|
|
530
|
+
let directives;
|
|
531
|
+
const shouldParseDirectives = parseDirectives ?? this.config.enableDirectives;
|
|
532
|
+
if (shouldParseDirectives) {
|
|
533
|
+
directives = this.directiveParser.parse(rawResponseText);
|
|
534
|
+
cleanText = directives.cleanText;
|
|
535
|
+
for (const action of directives.actions) {
|
|
536
|
+
if (action.kind === 'session' && this.sessions && externalKey) {
|
|
537
|
+
this.sessions.updateSessionId(externalKey, validatedAgentName, action.sessionId, runner);
|
|
538
|
+
this.log.info(`[reqId=${reqId}] 🔗 SESSION_ID directive → ${action.sessionId}`);
|
|
539
|
+
}
|
|
540
|
+
else if (action.kind === 'context' && this.sessions && externalKey) {
|
|
541
|
+
this.sessions.updateContext(externalKey, validatedAgentName, action.patch);
|
|
542
|
+
this.log.info(`[reqId=${reqId}] 📝 CONTEXT_UPDATE directive → ${JSON.stringify(action.patch)}`);
|
|
543
|
+
}
|
|
544
|
+
else if (action.kind === 'hint') {
|
|
545
|
+
this.log.info(`[reqId=${reqId}] 💡 BRIDGE_HINT: ${action.text}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// Save session
|
|
550
|
+
if (this.sessions && externalKey && agentResult.sessionId) {
|
|
551
|
+
this.sessions.set({
|
|
552
|
+
externalKey,
|
|
553
|
+
agentName: validatedAgentName,
|
|
554
|
+
runner: runner,
|
|
555
|
+
sessionId: agentResult.sessionId,
|
|
556
|
+
context: directives?.actions.find((a) => a.kind === 'context')
|
|
557
|
+
? directives.actions.find((a) => a.kind === 'context').patch
|
|
558
|
+
: undefined,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
if (messageId && this.messageLog) {
|
|
562
|
+
await this.messageLog.markDone(messageId, cleanText, agentResult.sessionId);
|
|
563
|
+
}
|
|
564
|
+
this.registry.markIdle(validatedAgentName, !agentResult.isError);
|
|
565
|
+
if (!agentResult.isError)
|
|
566
|
+
this.registry.markOnline(validatedAgentName);
|
|
567
|
+
// Return content nettoyé des directives (si parsing activé)
|
|
568
|
+
const resultContent = shouldParseDirectives
|
|
569
|
+
? [{ type: 'text', text: cleanText }]
|
|
570
|
+
: agentResult.content;
|
|
571
|
+
return {
|
|
572
|
+
messageId,
|
|
573
|
+
sessionId: agentResult.sessionId,
|
|
574
|
+
content: resultContent,
|
|
575
|
+
isError: agentResult.isError,
|
|
576
|
+
directives: shouldParseDirectives
|
|
577
|
+
? directives?.actions.map((a) => a.kind)
|
|
578
|
+
: undefined,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
catch (err) {
|
|
582
|
+
const errorMsg = err.message;
|
|
583
|
+
if (messageId && this.messageLog) {
|
|
584
|
+
if (errorMsg.includes('TIMEOUT') || errorMsg.includes('timeout')) {
|
|
585
|
+
await this.messageLog.markTimeout(messageId);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
await this.messageLog.markFailed(messageId, errorMsg);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
this.registry.markIdle(validatedAgentName, false);
|
|
592
|
+
throw err;
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result };
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* agent.a2a — Agent A parle à Agent B (le hub orchestre).
|
|
599
|
+
* B reçoit un prompt enrichi avec le contexte de A.
|
|
600
|
+
*/
|
|
601
|
+
async methodAgentA2A(req) {
|
|
602
|
+
const params = this.validateParams(A2AParams, req.params, req.id);
|
|
603
|
+
if (!params.ok)
|
|
604
|
+
return params.response;
|
|
605
|
+
const { fromAgent, toAgent, runner, prompt, model, path, metadata } = params.data;
|
|
606
|
+
const validatedFrom = validateAgentName(fromAgent);
|
|
607
|
+
const validatedTo = validateAgentName(toAgent);
|
|
608
|
+
// Register les deux si pas vus
|
|
609
|
+
this.registry.register(validatedFrom, runner);
|
|
610
|
+
this.registry.register(validatedTo, runner);
|
|
611
|
+
this.registry.incrementA2aSent(validatedFrom);
|
|
612
|
+
this.registry.incrementA2aReceived(validatedTo);
|
|
613
|
+
// Enrichit le prompt avec contexte A→B
|
|
614
|
+
const enrichedPrompt = [
|
|
615
|
+
`[A2A — Agent-to-Agent Message]`,
|
|
616
|
+
`FROM: ${validatedFrom}`,
|
|
617
|
+
`TO: ${validatedTo}`,
|
|
618
|
+
`TIMESTAMP: ${new Date().toISOString()}`,
|
|
619
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
620
|
+
prompt,
|
|
621
|
+
].join('\n');
|
|
622
|
+
// Persist + run (délègue à agent.run)
|
|
623
|
+
let messageId;
|
|
624
|
+
if (this.messageLog) {
|
|
625
|
+
messageId = await this.messageLog.create({
|
|
626
|
+
fromAgent: validatedFrom,
|
|
627
|
+
toAgent: validatedTo,
|
|
628
|
+
runner: runner,
|
|
629
|
+
prompt: enrichedPrompt,
|
|
630
|
+
metadata: { ...(metadata ?? {}), a2a: true, from: validatedFrom, to: validatedTo },
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
const result = await this.registry.withLock(validatedTo, async () => {
|
|
634
|
+
this.registry.markBusy(validatedTo);
|
|
635
|
+
if (messageId && this.messageLog) {
|
|
636
|
+
await this.messageLog.markRunning(messageId);
|
|
637
|
+
}
|
|
638
|
+
try {
|
|
639
|
+
const agentResult = await this.service.runAgent({
|
|
640
|
+
runner: runner,
|
|
641
|
+
prompt: enrichedPrompt,
|
|
642
|
+
agentName: validatedTo,
|
|
643
|
+
path,
|
|
644
|
+
model,
|
|
645
|
+
});
|
|
646
|
+
const responseText = agentResult.content.map((c) => c.text).join('\n');
|
|
647
|
+
if (messageId && this.messageLog) {
|
|
648
|
+
await this.messageLog.markDone(messageId, responseText, agentResult.sessionId);
|
|
649
|
+
}
|
|
650
|
+
this.registry.markIdle(validatedTo, !agentResult.isError);
|
|
651
|
+
if (!agentResult.isError)
|
|
652
|
+
this.registry.markOnline(validatedTo);
|
|
653
|
+
return {
|
|
654
|
+
messageId,
|
|
655
|
+
from: validatedFrom,
|
|
656
|
+
to: validatedTo,
|
|
657
|
+
sessionId: agentResult.sessionId,
|
|
658
|
+
content: agentResult.content,
|
|
659
|
+
isError: agentResult.isError,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
catch (err) {
|
|
663
|
+
const errorMsg = err.message;
|
|
664
|
+
if (messageId && this.messageLog) {
|
|
665
|
+
if (errorMsg.includes('TIMEOUT') || errorMsg.includes('timeout')) {
|
|
666
|
+
await this.messageLog.markTimeout(messageId);
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
await this.messageLog.markFailed(messageId, errorMsg);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
this.registry.markIdle(validatedTo, false);
|
|
673
|
+
throw err;
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result };
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* agent.status — Status live d'un agent (busy/idle/online) via registry.
|
|
680
|
+
* Possibilité de proxy vers Overmind MCP agent_control aussi.
|
|
681
|
+
*/
|
|
682
|
+
async methodAgentStatus(req) {
|
|
683
|
+
const params = this.validateParams(AgentStatusParams, req.params, req.id);
|
|
684
|
+
if (!params.ok)
|
|
685
|
+
return params.response;
|
|
686
|
+
const { agentName, runner, action, sinceTimestamp, timeoutMs } = params.data;
|
|
687
|
+
const validatedName = validateAgentName(agentName);
|
|
688
|
+
// Status local du registry (instantané)
|
|
689
|
+
const localState = this.registry.get(validatedName);
|
|
690
|
+
// Si action demandée (status/stream/kill/wait), on proxy vers MCP
|
|
691
|
+
if (action) {
|
|
692
|
+
const result = await this.service.agentStatus({
|
|
693
|
+
agentName: validatedName,
|
|
694
|
+
action,
|
|
695
|
+
runner: runner,
|
|
696
|
+
sinceTimestamp,
|
|
697
|
+
timeoutMs,
|
|
698
|
+
});
|
|
699
|
+
// Sync registry avec retour MCP
|
|
700
|
+
if (action === 'status') {
|
|
701
|
+
const mcpState = this.parseAgentControlStatus(result);
|
|
702
|
+
if (mcpState === 'running') {
|
|
703
|
+
this.registry.markBusy(validatedName, result.sessionId);
|
|
704
|
+
}
|
|
705
|
+
else if (mcpState === 'done') {
|
|
706
|
+
this.registry.markIdle(validatedName, true);
|
|
707
|
+
this.registry.markOnline(validatedName);
|
|
708
|
+
}
|
|
709
|
+
else if (mcpState === 'failed') {
|
|
710
|
+
this.registry.markIdle(validatedName, false);
|
|
711
|
+
}
|
|
712
|
+
else if (mcpState === 'orphaned') {
|
|
713
|
+
this.registry.markOffline(validatedName);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
else if (action === 'kill') {
|
|
717
|
+
this.registry.markOffline(validatedName);
|
|
718
|
+
}
|
|
719
|
+
return {
|
|
720
|
+
jsonrpc: '2.0',
|
|
721
|
+
id: req.id ?? null,
|
|
722
|
+
result: { local: localState, mcp: result },
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result: { local: localState } };
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* agent.list — Liste tous les agents et leur état.
|
|
729
|
+
*/
|
|
730
|
+
async methodAgentList(req) {
|
|
731
|
+
const params = this.validateParams(ListAgentsParams, req.params, req.id);
|
|
732
|
+
if (!params.ok)
|
|
733
|
+
return params.response;
|
|
734
|
+
const agents = this.registry.list({
|
|
735
|
+
status: params.data.status,
|
|
736
|
+
runner: params.data.runner,
|
|
737
|
+
});
|
|
738
|
+
const stats = this.registry.stats();
|
|
739
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result: { agents, stats } };
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* agent.kill — Kill un agent en cours.
|
|
743
|
+
*/
|
|
744
|
+
async methodAgentKill(req) {
|
|
745
|
+
const params = this.validateParams(z.object({ agentName: z.string().min(1), runner: z.string().optional() }), req.params, req.id);
|
|
746
|
+
if (!params.ok)
|
|
747
|
+
return params.response;
|
|
748
|
+
const result = await this.service.killAgent(validateAgentName(params.data.agentName), params.data.runner);
|
|
749
|
+
this.registry.markOffline(params.data.agentName);
|
|
750
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result };
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* message.history — Historique des messages persistés.
|
|
754
|
+
*/
|
|
755
|
+
async methodMessageHistory(req) {
|
|
756
|
+
if (!this.messageLog) {
|
|
757
|
+
return { jsonrpc: '2.0', id: req.id ?? null, error: { code: -32003, message: 'MessageLog disabled' } };
|
|
758
|
+
}
|
|
759
|
+
const params = this.validateParams(MessageHistoryParams, req.params, req.id);
|
|
760
|
+
if (!params.ok)
|
|
761
|
+
return params.response;
|
|
762
|
+
const messages = await this.messageLog.list(params.data);
|
|
763
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result: { messages, count: messages.length } };
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* message.get — Récupère un message par ID.
|
|
767
|
+
*/
|
|
768
|
+
async methodMessageGet(req) {
|
|
769
|
+
if (!this.messageLog) {
|
|
770
|
+
return { jsonrpc: '2.0', id: req.id ?? null, error: { code: -32003, message: 'MessageLog disabled' } };
|
|
771
|
+
}
|
|
772
|
+
const params = this.validateParams(MessageGetParams, req.params, req.id);
|
|
773
|
+
if (!params.ok)
|
|
774
|
+
return params.response;
|
|
775
|
+
const message = await this.messageLog.getById(params.data.id);
|
|
776
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result: { message } };
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* message.replay — Rejoue un message (re-run l'agent avec le même prompt).
|
|
780
|
+
* Le nouveau run crée un NOUVEAU message, l'ancien reste en status 'pending' pour traçabilité.
|
|
781
|
+
*/
|
|
782
|
+
async methodMessageReplay(req) {
|
|
783
|
+
if (!this.messageLog) {
|
|
784
|
+
return { jsonrpc: '2.0', id: req.id ?? null, error: { code: -32003, message: 'MessageLog disabled' } };
|
|
785
|
+
}
|
|
786
|
+
const params = this.validateParams(MessageReplayParams, req.params, req.id);
|
|
787
|
+
if (!params.ok)
|
|
788
|
+
return params.response;
|
|
789
|
+
const original = await this.messageLog.getById(params.data.id);
|
|
790
|
+
if (!original) {
|
|
791
|
+
return { jsonrpc: '2.0', id: req.id ?? null, error: { code: -32004, message: 'Message not found' } };
|
|
792
|
+
}
|
|
793
|
+
// Relance via agent.run
|
|
794
|
+
const replayReq = {
|
|
795
|
+
jsonrpc: '2.0',
|
|
796
|
+
id: req.id,
|
|
797
|
+
method: 'agent.run',
|
|
798
|
+
params: {
|
|
799
|
+
agentName: original.toAgent,
|
|
800
|
+
runner: original.runner,
|
|
801
|
+
prompt: original.prompt,
|
|
802
|
+
sessionId: original.sessionId ?? undefined,
|
|
803
|
+
metadata: { ...(original.metadata ?? {}), replayOf: original.id },
|
|
804
|
+
},
|
|
805
|
+
};
|
|
806
|
+
return this.methodAgentRun(replayReq);
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* message.stats — Statistiques globales du log.
|
|
810
|
+
*/
|
|
811
|
+
async methodMessageStats(req) {
|
|
812
|
+
if (!this.messageLog) {
|
|
813
|
+
return { jsonrpc: '2.0', id: req.id ?? null, error: { code: -32003, message: 'MessageLog disabled' } };
|
|
814
|
+
}
|
|
815
|
+
const stats = await this.messageLog.stats();
|
|
816
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result: stats };
|
|
817
|
+
}
|
|
818
|
+
// ─── SessionStore RPC Methods ───────────────────────────────────────────
|
|
819
|
+
async methodSessionGet(req) {
|
|
820
|
+
if (!this.sessions) {
|
|
821
|
+
return { jsonrpc: '2.0', id: req.id ?? null, error: { code: -32003, message: 'SessionStore disabled' } };
|
|
822
|
+
}
|
|
823
|
+
const params = this.validateParams(z.object({ externalKey: z.string().min(1), agentName: z.string().min(1) }), req.params, req.id);
|
|
824
|
+
if (!params.ok)
|
|
825
|
+
return params.response;
|
|
826
|
+
const entry = this.sessions.get(params.data.externalKey, params.data.agentName);
|
|
827
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result: { session: entry } };
|
|
828
|
+
}
|
|
829
|
+
async methodSessionList(_req) {
|
|
830
|
+
if (!this.sessions) {
|
|
831
|
+
return { jsonrpc: '2.0', id: _req.id ?? null, error: { code: -32003, message: 'SessionStore disabled' } };
|
|
832
|
+
}
|
|
833
|
+
const list = this.sessions.list();
|
|
834
|
+
const stats = this.sessions.stats();
|
|
835
|
+
return { jsonrpc: '2.0', id: _req.id ?? null, result: { sessions: list, stats } };
|
|
836
|
+
}
|
|
837
|
+
async methodSessionDelete(req) {
|
|
838
|
+
if (!this.sessions) {
|
|
839
|
+
return { jsonrpc: '2.0', id: req.id ?? null, error: { code: -32003, message: 'SessionStore disabled' } };
|
|
840
|
+
}
|
|
841
|
+
const params = this.validateParams(z.object({ externalKey: z.string().min(1), agentName: z.string().min(1) }), req.params, req.id);
|
|
842
|
+
if (!params.ok)
|
|
843
|
+
return params.response;
|
|
844
|
+
const deleted = this.sessions.delete(params.data.externalKey, params.data.agentName);
|
|
845
|
+
return { jsonrpc: '2.0', id: req.id ?? null, result: { deleted } };
|
|
846
|
+
}
|
|
847
|
+
async methodSessionStats(_req) {
|
|
848
|
+
if (!this.sessions) {
|
|
849
|
+
return { jsonrpc: '2.0', id: _req.id ?? null, error: { code: -32003, message: 'SessionStore disabled' } };
|
|
850
|
+
}
|
|
851
|
+
return { jsonrpc: '2.0', id: _req.id ?? null, result: this.sessions.stats() };
|
|
852
|
+
}
|
|
853
|
+
// ─── Webhook RPC Method (programmatic, sans passer par HTTP) ────────────
|
|
854
|
+
async methodWebhookSms(req) {
|
|
855
|
+
const params = this.validateParams(z.object({
|
|
856
|
+
provider: z.enum(['voipms', 'twilio', 'discord', 'generic']).default('voipms'),
|
|
857
|
+
payload: z.record(z.string(), z.unknown()),
|
|
858
|
+
externalKey: z.string().optional(),
|
|
859
|
+
/** Si fourni, dispatch automatique vers agent.run après adaptation */
|
|
860
|
+
autoDispatch: z.object({
|
|
861
|
+
agentName: z.string().min(1),
|
|
862
|
+
runner: z.string().min(1),
|
|
863
|
+
model: z.string().optional(),
|
|
864
|
+
mode: z.string().optional(),
|
|
865
|
+
}).optional(),
|
|
866
|
+
}), req.params, req.id);
|
|
867
|
+
if (!params.ok)
|
|
868
|
+
return params.response;
|
|
869
|
+
const adapter = new WebhookAdapter({ provider: params.data.provider, logger: this.log });
|
|
870
|
+
const normalized = adapter.adapt(params.data.payload);
|
|
871
|
+
const externalKey = params.data.externalKey ?? normalized.externalKey;
|
|
872
|
+
if (!params.data.autoDispatch) {
|
|
873
|
+
return {
|
|
874
|
+
jsonrpc: '2.0',
|
|
875
|
+
id: req.id ?? null,
|
|
876
|
+
result: { externalKey, prompt: normalized.prompt, mediaUrls: normalized.mediaUrls, metadata: normalized.metadata },
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
// Auto-dispatch vers agent.run
|
|
880
|
+
const innerReq = {
|
|
881
|
+
jsonrpc: '2.0',
|
|
882
|
+
id: req.id,
|
|
883
|
+
method: 'agent.run',
|
|
884
|
+
params: {
|
|
885
|
+
agentName: params.data.autoDispatch.agentName,
|
|
886
|
+
runner: params.data.autoDispatch.runner,
|
|
887
|
+
prompt: normalized.prompt,
|
|
888
|
+
model: params.data.autoDispatch.model,
|
|
889
|
+
mode: params.data.autoDispatch.mode,
|
|
890
|
+
externalKey,
|
|
891
|
+
metadata: { webhook: true, provider: params.data.provider, ...normalized.metadata },
|
|
892
|
+
},
|
|
893
|
+
};
|
|
894
|
+
return this.methodAgentRun(innerReq);
|
|
895
|
+
}
|
|
896
|
+
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
897
|
+
/**
|
|
898
|
+
* Valide les params d'une requête JSON-RPC via Zod.
|
|
899
|
+
* Retourne soit { data } (succès) soit une JsonRpcResponse d'erreur (à retourner tel quel).
|
|
900
|
+
*/
|
|
901
|
+
validateParams(schema, params, id) {
|
|
902
|
+
const result = schema.safeParse(params ?? {});
|
|
903
|
+
if (!result.success) {
|
|
904
|
+
return {
|
|
905
|
+
ok: false,
|
|
906
|
+
response: {
|
|
907
|
+
jsonrpc: '2.0',
|
|
908
|
+
id: id ?? null,
|
|
909
|
+
error: {
|
|
910
|
+
...JSON_RPC_ERRORS.INVALID_PARAMS,
|
|
911
|
+
data: result.error.issues,
|
|
912
|
+
},
|
|
913
|
+
},
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
return { ok: true, data: result.data };
|
|
917
|
+
}
|
|
918
|
+
respondError(res, id, error) {
|
|
919
|
+
this.writeJson(res, 200, { jsonrpc: '2.0', id, error });
|
|
920
|
+
}
|
|
921
|
+
buildErrorResponse(id, err) {
|
|
922
|
+
const error = err;
|
|
923
|
+
return {
|
|
924
|
+
jsonrpc: '2.0',
|
|
925
|
+
id,
|
|
926
|
+
error: {
|
|
927
|
+
code: error.code ?? JSON_RPC_ERRORS.INTERNAL_ERROR.code,
|
|
928
|
+
message: error.message ?? 'Internal error',
|
|
929
|
+
},
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
readBody(req, maxBytes) {
|
|
933
|
+
const limit = maxBytes ?? this.parseBodyLimit(this.config.jsonBodyLimit);
|
|
934
|
+
return new Promise((resolve, reject) => {
|
|
935
|
+
const chunks = [];
|
|
936
|
+
let total = 0;
|
|
937
|
+
req.on('data', (chunk) => {
|
|
938
|
+
total += chunk.length;
|
|
939
|
+
if (total > limit) {
|
|
940
|
+
req.destroy();
|
|
941
|
+
reject(Object.assign(new Error('Request body too large'), { code: 'EBODYTOOLARGE' }));
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
chunks.push(chunk);
|
|
945
|
+
});
|
|
946
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
947
|
+
req.on('error', reject);
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Parse "10mb" / "1gb" en bytes.
|
|
952
|
+
*/
|
|
953
|
+
parseBodyLimit(limit) {
|
|
954
|
+
if (!limit)
|
|
955
|
+
return 10 * 1024 * 1024; // 10mb default
|
|
956
|
+
const m = limit.match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/i);
|
|
957
|
+
if (!m)
|
|
958
|
+
return 10 * 1024 * 1024;
|
|
959
|
+
const num = Number(m[1]);
|
|
960
|
+
const unit = (m[2] ?? 'b').toLowerCase();
|
|
961
|
+
const mult = { b: 1, kb: 1024, mb: 1024 ** 2, gb: 1024 ** 3 };
|
|
962
|
+
return Math.floor(num * (mult[unit] ?? 1));
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Parse le retour de agent_control pour extraire le status de l'agent distant.
|
|
966
|
+
*/
|
|
967
|
+
parseAgentControlStatus(result) {
|
|
968
|
+
const text = result.content.map((c) => c.text).join('\n').toLowerCase();
|
|
969
|
+
if (text.includes('running') || text.includes('status: running'))
|
|
970
|
+
return 'running';
|
|
971
|
+
if (text.includes('done') || text.includes('completed'))
|
|
972
|
+
return 'done';
|
|
973
|
+
if (text.includes('failed') || text.includes('error'))
|
|
974
|
+
return 'failed';
|
|
975
|
+
if (text.includes('orphaned') || text.includes('offline'))
|
|
976
|
+
return 'orphaned';
|
|
977
|
+
return 'unknown';
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
// ─── Safe Stats wrapper inline (déclaré ici pour éviter d'augmenter MessageLog) ───
|
|
981
|
+
// (Voir handleHealth — try/catch sur this.messageLog.stats())
|
|
982
|
+
//# sourceMappingURL=OverBridgeServer.js.map
|