@delexec/ops 0.1.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.
Files changed (55) hide show
  1. package/README.md +3 -0
  2. package/README.zh-CN.md +6 -0
  3. package/node_modules/@delexec/caller-controller/README.md +3 -0
  4. package/node_modules/@delexec/caller-controller/README.zh-CN.md +6 -0
  5. package/node_modules/@delexec/caller-controller/package.json +53 -0
  6. package/node_modules/@delexec/caller-controller/src/server.js +127 -0
  7. package/node_modules/@delexec/caller-controller-core/README.md +3 -0
  8. package/node_modules/@delexec/caller-controller-core/README.zh-CN.md +6 -0
  9. package/node_modules/@delexec/caller-controller-core/package.json +26 -0
  10. package/node_modules/@delexec/caller-controller-core/src/index.js +1612 -0
  11. package/node_modules/@delexec/caller-skill-adapter/package.json +12 -0
  12. package/node_modules/@delexec/caller-skill-adapter/src/server.js +1042 -0
  13. package/node_modules/@delexec/caller-skill-mcp-adapter/README.md +65 -0
  14. package/node_modules/@delexec/caller-skill-mcp-adapter/package.json +16 -0
  15. package/node_modules/@delexec/caller-skill-mcp-adapter/src/server.js +527 -0
  16. package/node_modules/@delexec/responder-controller/README.md +3 -0
  17. package/node_modules/@delexec/responder-controller/README.zh-CN.md +6 -0
  18. package/node_modules/@delexec/responder-controller/package.json +53 -0
  19. package/node_modules/@delexec/responder-controller/src/server.js +254 -0
  20. package/node_modules/@delexec/responder-runtime-core/README.md +3 -0
  21. package/node_modules/@delexec/responder-runtime-core/README.zh-CN.md +6 -0
  22. package/node_modules/@delexec/responder-runtime-core/package.json +26 -0
  23. package/node_modules/@delexec/responder-runtime-core/src/executors.js +326 -0
  24. package/node_modules/@delexec/responder-runtime-core/src/index.js +1202 -0
  25. package/node_modules/@delexec/runtime-utils/README.md +3 -0
  26. package/node_modules/@delexec/runtime-utils/README.zh-CN.md +6 -0
  27. package/node_modules/@delexec/runtime-utils/package.json +23 -0
  28. package/node_modules/@delexec/runtime-utils/src/index.js +338 -0
  29. package/node_modules/@delexec/sqlite-store/README.md +3 -0
  30. package/node_modules/@delexec/sqlite-store/README.zh-CN.md +6 -0
  31. package/node_modules/@delexec/sqlite-store/package.json +26 -0
  32. package/node_modules/@delexec/sqlite-store/src/index.js +68 -0
  33. package/node_modules/@delexec/transport-email/README.md +3 -0
  34. package/node_modules/@delexec/transport-email/README.zh-CN.md +6 -0
  35. package/node_modules/@delexec/transport-email/package.json +23 -0
  36. package/node_modules/@delexec/transport-email/src/index.js +185 -0
  37. package/node_modules/@delexec/transport-emailengine/README.md +3 -0
  38. package/node_modules/@delexec/transport-emailengine/README.zh-CN.md +6 -0
  39. package/node_modules/@delexec/transport-emailengine/package.json +26 -0
  40. package/node_modules/@delexec/transport-emailengine/src/index.js +210 -0
  41. package/node_modules/@delexec/transport-gmail/README.md +3 -0
  42. package/node_modules/@delexec/transport-gmail/README.zh-CN.md +6 -0
  43. package/node_modules/@delexec/transport-gmail/package.json +26 -0
  44. package/node_modules/@delexec/transport-gmail/src/index.js +295 -0
  45. package/node_modules/@delexec/transport-relay-http/README.md +3 -0
  46. package/node_modules/@delexec/transport-relay-http/README.zh-CN.md +6 -0
  47. package/node_modules/@delexec/transport-relay-http/package.json +23 -0
  48. package/node_modules/@delexec/transport-relay-http/src/index.js +124 -0
  49. package/package.json +64 -0
  50. package/src/cli.js +1571 -0
  51. package/src/config.js +1180 -0
  52. package/src/example-hotline-worker.js +65 -0
  53. package/src/example-hotline.js +196 -0
  54. package/src/logging.js +56 -0
  55. package/src/supervisor.js +3070 -0
@@ -0,0 +1,3070 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import http from "node:http";
4
+ import { spawn, execFileSync } from "node:child_process";
5
+ import { createRequire } from "node:module";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ import { buildStructuredError } from "@delexec/contracts";
10
+ import {
11
+ buildHotlineOnboardingBody,
12
+ ensureHotlineRegistrationDraft,
13
+ buildTransportEnvUpdates,
14
+ buildTransportSecretUpdates,
15
+ ensureResponderIdentity,
16
+ ensureOpsState,
17
+ hasEncryptedSecretStore,
18
+ listLegacySecretKeys,
19
+ loadHotlineRegistrationDraft,
20
+ normalizeTransportConfig,
21
+ OPS_SECRET_KEYS,
22
+ readTransportSecretsFromEnv,
23
+ readResolvedOpsSecrets,
24
+ redactTransportConfig,
25
+ removeHotline,
26
+ saveOpsState,
27
+ scrubLegacySecrets,
28
+ setHotlineEnabled,
29
+ unlockOpsSecrets,
30
+ upsertHotline,
31
+ writeOpsSecrets
32
+ } from "./config.js";
33
+ import {
34
+ buildExampleRequestBody,
35
+ buildExampleHotlineDefinition,
36
+ isExampleHotlineDefinitionStale,
37
+ LOCAL_EXAMPLE_DISPLAY_NAME,
38
+ LOCAL_EXAMPLE_HOTLINE_ID
39
+ } from "./example-hotline.js";
40
+ import {
41
+ appendServiceLog,
42
+ appendSupervisorEvent,
43
+ getServiceLogFile,
44
+ getSupervisorEventsFile,
45
+ readServiceLogTail,
46
+ readSupervisorEventTail
47
+ } from "./logging.js";
48
+ import {
49
+ ensureOpsDirectories,
50
+ getOpsHomeDir,
51
+ initializeSecretStore,
52
+ rotateSecretStorePassphrase,
53
+ writeJsonFile
54
+ } from "@delexec/runtime-utils";
55
+
56
+ const require = createRequire(import.meta.url);
57
+ const __filename = fileURLToPath(import.meta.url);
58
+ const __dirname = path.dirname(__filename);
59
+ const PLATFORM_BOOTSTRAP_DEMO_HOTLINE_IDS = new Set([
60
+ "starlight.creative.studio.v1",
61
+ "atlas.knowledge.qa.v1",
62
+ "pixel.product.renderer.v1"
63
+ ]);
64
+ function getOpsSessionStateFile() {
65
+ return path.join(getOpsHomeDir(), "run", "session.json");
66
+ }
67
+
68
+ function nowIso() {
69
+ return new Date().toISOString();
70
+ }
71
+
72
+ function persistActiveSession(session) {
73
+ ensureOpsDirectories();
74
+ const sessionStateFile = getOpsSessionStateFile();
75
+ if (!session?.token) {
76
+ if (fs.existsSync(sessionStateFile)) {
77
+ fs.rmSync(sessionStateFile, { force: true });
78
+ }
79
+ return;
80
+ }
81
+ writeJsonFile(sessionStateFile, {
82
+ token: session.token,
83
+ expires_at: session.expires_at
84
+ });
85
+ }
86
+
87
+ function clearActiveSession() {
88
+ persistActiveSession(null);
89
+ }
90
+
91
+ function sendJson(res, statusCode, data) {
92
+ res.writeHead(statusCode, {
93
+ "content-type": "application/json; charset=utf-8",
94
+ "access-control-allow-origin": "*",
95
+ "access-control-allow-methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
96
+ "access-control-allow-headers": "Content-Type, Authorization, X-Ops-Session"
97
+ });
98
+ res.end(JSON.stringify(data));
99
+ }
100
+
101
+ function sendError(res, statusCode, code, message, { retryable, ...extra } = {}) {
102
+ sendJson(res, statusCode, buildStructuredError(code, message, { retryable, ...extra }));
103
+ }
104
+
105
+ function parseJsonBody(req) {
106
+ return new Promise((resolve, reject) => {
107
+ const chunks = [];
108
+ req.on("data", (chunk) => chunks.push(chunk));
109
+ req.on("end", () => {
110
+ if (chunks.length === 0) {
111
+ resolve({});
112
+ return;
113
+ }
114
+ try {
115
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
116
+ } catch {
117
+ reject(new Error("invalid_json"));
118
+ }
119
+ });
120
+ req.on("error", reject);
121
+ });
122
+ }
123
+
124
+ async function requestJson(baseUrl, pathname, { method = "GET", headers = {}, body } = {}) {
125
+ const response = await fetch(new URL(pathname, baseUrl), {
126
+ method,
127
+ headers: {
128
+ ...headers,
129
+ ...(body === undefined ? {} : { "content-type": "application/json; charset=utf-8" })
130
+ },
131
+ body: body === undefined ? undefined : JSON.stringify(body)
132
+ });
133
+ const text = await response.text();
134
+ return {
135
+ status: response.status,
136
+ body: text ? JSON.parse(text) : null
137
+ };
138
+ }
139
+
140
+ function processBaseUrl(port) {
141
+ return `http://127.0.0.1:${port}`;
142
+ }
143
+
144
+ function appendPath(baseUrl, pathname) {
145
+ return new URL(pathname, `${baseUrl}/`).toString();
146
+ }
147
+
148
+ function parseJsonArrayEnv(value) {
149
+ const normalized = normalizedString(value);
150
+ if (!normalized) {
151
+ return [];
152
+ }
153
+ try {
154
+ const parsed = JSON.parse(normalized);
155
+ return Array.isArray(parsed) ? parsed.map((item) => String(item)) : [normalized];
156
+ } catch {
157
+ return normalized.split(/\s+/).filter(Boolean);
158
+ }
159
+ }
160
+
161
+ function normalizedString(value) {
162
+ if (value === undefined || value === null) {
163
+ return null;
164
+ }
165
+ const trimmed = String(value).trim();
166
+ return trimmed ? trimmed : null;
167
+ }
168
+
169
+ const OPS_SESSION_HEADER = "x-ops-session";
170
+ const SESSION_TTL_MS = 8 * 60 * 60 * 1000;
171
+
172
+ function createSessionToken() {
173
+ return crypto.randomUUID().replace(/-/g, "");
174
+ }
175
+
176
+ function buildTransportSecretLookup(secrets) {
177
+ return {
178
+ [OPS_SECRET_KEYS.transport_emailengine_access_token]: secrets.transport.emailengine.access_token,
179
+ [OPS_SECRET_KEYS.transport_gmail_client_secret]: secrets.transport.gmail.client_secret,
180
+ [OPS_SECRET_KEYS.transport_gmail_refresh_token]: secrets.transport.gmail.refresh_token
181
+ };
182
+ }
183
+
184
+ function buildLegacyTransportSecretEnv(secretUpdates) {
185
+ return {
186
+ TRANSPORT_EMAILENGINE_ACCESS_TOKEN: secretUpdates[OPS_SECRET_KEYS.transport_emailengine_access_token] || undefined,
187
+ TRANSPORT_GMAIL_CLIENT_SECRET: secretUpdates[OPS_SECRET_KEYS.transport_gmail_client_secret] || undefined,
188
+ TRANSPORT_GMAIL_REFRESH_TOKEN: secretUpdates[OPS_SECRET_KEYS.transport_gmail_refresh_token] || undefined
189
+ };
190
+ }
191
+
192
+ function mergeEnvWithResolvedSecrets(env, secrets) {
193
+ return {
194
+ ...env,
195
+ CALLER_PLATFORM_API_KEY: secrets.caller_api_key || env.CALLER_PLATFORM_API_KEY || env.PLATFORM_API_KEY || "",
196
+ PLATFORM_API_KEY: secrets.caller_api_key || env.PLATFORM_API_KEY || env.CALLER_PLATFORM_API_KEY || "",
197
+ RESPONDER_PLATFORM_API_KEY: secrets.responder_platform_api_key || env.RESPONDER_PLATFORM_API_KEY || "",
198
+ PLATFORM_ADMIN_API_KEY: secrets.platform_admin_api_key || env.PLATFORM_ADMIN_API_KEY || "",
199
+ ...buildTransportSecretLookup(secrets)
200
+ };
201
+ }
202
+
203
+ function pruneExpiredSessions(runtime) {
204
+ const now = Date.now();
205
+ for (const [token, session] of runtime.auth.sessions.entries()) {
206
+ if (session.expiresAt <= now) {
207
+ runtime.auth.sessions.delete(token);
208
+ }
209
+ }
210
+ if (runtime.auth.sessions.size === 0) {
211
+ runtime.auth.unlockedSecrets = null;
212
+ runtime.auth.passphrase = null;
213
+ runtime.auth.unlockedAt = null;
214
+ clearActiveSession();
215
+ }
216
+ }
217
+
218
+ function createAuthenticatedSession(runtime, passphrase, secrets) {
219
+ pruneExpiredSessions(runtime);
220
+ const token = createSessionToken();
221
+ const expiresAt = Date.now() + SESSION_TTL_MS;
222
+ runtime.auth.passphrase = passphrase;
223
+ runtime.auth.unlockedSecrets = secrets;
224
+ runtime.auth.unlockedAt = nowIso();
225
+ runtime.auth.sessions.set(token, {
226
+ token,
227
+ createdAt: nowIso(),
228
+ expiresAt
229
+ });
230
+ const session = {
231
+ token,
232
+ expires_at: new Date(expiresAt).toISOString()
233
+ };
234
+ persistActiveSession(session);
235
+ return session;
236
+ }
237
+
238
+ function readSessionToken(req) {
239
+ const headerValue = req.headers[OPS_SESSION_HEADER];
240
+ if (Array.isArray(headerValue)) {
241
+ return headerValue[0] || null;
242
+ }
243
+ return normalizedString(headerValue);
244
+ }
245
+
246
+ function getCurrentSession(runtime, req) {
247
+ pruneExpiredSessions(runtime);
248
+ const token = readSessionToken(req);
249
+ if (!token) {
250
+ return null;
251
+ }
252
+ const session = runtime.auth.sessions.get(token);
253
+ if (!session) {
254
+ return null;
255
+ }
256
+ session.expiresAt = Date.now() + SESSION_TTL_MS;
257
+ const activeSession = {
258
+ token,
259
+ expires_at: new Date(session.expiresAt).toISOString()
260
+ };
261
+ persistActiveSession(activeSession);
262
+ return activeSession;
263
+ }
264
+
265
+ function isLocalResponderConfigRoute(method, pathname) {
266
+ if (method === "POST" && (pathname === "/responder/hotlines" || pathname === "/responder/hotlines/example")) {
267
+ return true;
268
+ }
269
+ if (method === "DELETE" && /^\/responder\/hotlines\/[^/]+$/.test(pathname)) {
270
+ return true;
271
+ }
272
+ if (method === "POST" && /^\/responder\/hotlines\/[^/]+\/(enable|disable)$/.test(pathname)) {
273
+ return true;
274
+ }
275
+ return false;
276
+ }
277
+
278
+ function isProtectedRoute(method, pathname) {
279
+ if (
280
+ pathname === "/healthz" ||
281
+ pathname === "/status" ||
282
+ pathname === "/setup" ||
283
+ pathname === "/mcp-adapter/spec" ||
284
+ pathname.startsWith("/auth/session")
285
+ ) {
286
+ return false;
287
+ }
288
+ if (isLocalResponderConfigRoute(method, pathname)) {
289
+ return false;
290
+ }
291
+ if (method === "GET" && pathname === "/") {
292
+ return false;
293
+ }
294
+ return true;
295
+ }
296
+
297
+ function buildResponderRuntimeStatus(state, runtime, hotlineId = null) {
298
+ const responderProcess = runtime.processes.get("responder") || null;
299
+ const configuredHotlineIds = (state.config.responder?.hotlines || []).map((item) => item.hotline_id).filter(Boolean);
300
+ return {
301
+ responder_running: Boolean(responderProcess && !responderProcess.exited),
302
+ responder_healthy: Boolean(responderProcess?.health?.status === 200),
303
+ configured_hotline_ids: configuredHotlineIds,
304
+ hotline_configured: hotlineId ? configuredHotlineIds.includes(hotlineId) : null
305
+ };
306
+ }
307
+
308
+ function platformFeaturesEnabled(state) {
309
+ return state.config?.platform?.enabled === true;
310
+ }
311
+
312
+ function serializeHotlineForUi(state, runtime, hotline) {
313
+ const draftFile = hotline?.metadata?.registration?.draft_file || null;
314
+ const localIntegrationFile = hotline?.metadata?.local?.integration_file || null;
315
+ const localHookFile = hotline?.metadata?.local?.hook_file || null;
316
+ const runtimeStatus = buildResponderRuntimeStatus(state, runtime, hotline?.hotline_id || null);
317
+ return {
318
+ ...hotline,
319
+ draft_ready: Boolean(draftFile),
320
+ draft_file: draftFile,
321
+ local_integration_file: localIntegrationFile,
322
+ local_hook_file: localHookFile,
323
+ runtime_loaded: Boolean(runtimeStatus.responder_running && hotline?.enabled !== false && runtimeStatus.hotline_configured),
324
+ local_status: hotline?.enabled === false ? "disabled" : draftFile ? "draft_ready" : "configured"
325
+ };
326
+ }
327
+
328
+ function getAuthState(runtime, state) {
329
+ pruneExpiredSessions(runtime);
330
+ const configured = hasEncryptedSecretStore();
331
+ const legacySecretKeys = listLegacySecretKeys(state);
332
+ const activeSession = runtime.auth.sessions.values().next().value || null;
333
+ return {
334
+ configured,
335
+ secret_file: state.secretsFile,
336
+ legacy_secret_keys: legacySecretKeys,
337
+ legacy_secret_source_present: legacySecretKeys.length > 0,
338
+ locked: configured && runtime.auth.sessions.size === 0,
339
+ authenticated: configured ? runtime.auth.sessions.size > 0 : true,
340
+ setup_required: !configured,
341
+ expires_at: activeSession ? new Date(activeSession.expiresAt).toISOString() : null
342
+ };
343
+ }
344
+
345
+ function getRecoverableSession(runtime) {
346
+ pruneExpiredSessions(runtime);
347
+ const activeSession = runtime.auth.sessions.values().next().value || null;
348
+ if (!activeSession) {
349
+ return null;
350
+ }
351
+ return {
352
+ token: activeSession.token,
353
+ expires_at: new Date(activeSession.expiresAt).toISOString()
354
+ };
355
+ }
356
+
357
+ function requireAuthenticatedSession(req, res, runtime, state) {
358
+ if (!hasEncryptedSecretStore()) {
359
+ return { ok: true, session: null };
360
+ }
361
+ const session = getCurrentSession(runtime, req);
362
+ if (!session) {
363
+ sendError(res, 401, "AUTH_SESSION_REQUIRED", "local supervisor session is locked or missing", {
364
+ retryable: false,
365
+ auth: getAuthState(runtime, state)
366
+ });
367
+ return { ok: false, session: null };
368
+ }
369
+ return { ok: true, session };
370
+ }
371
+
372
+ function normalizeTransportPayload(body = {}) {
373
+ return normalizeTransportConfig({ runtime: { transport: body } }, {});
374
+ }
375
+
376
+ function validateTransportConfig(transport) {
377
+ if (!["local", "relay_http", "email"].includes(transport.type)) {
378
+ return { status: 400, body: buildStructuredError("CONTRACT_INVALID_TRANSPORT_TYPE", "unsupported transport type") };
379
+ }
380
+ if (transport.type === "relay_http" && !normalizedString(transport.relay_http?.base_url)) {
381
+ return { status: 400, body: buildStructuredError("CONTRACT_INVALID_TRANSPORT_BODY", "relay_http.base_url is required") };
382
+ }
383
+ if (transport.type === "email") {
384
+ if (!["emailengine", "gmail"].includes(transport.email.provider)) {
385
+ return { status: 400, body: buildStructuredError("CONTRACT_INVALID_TRANSPORT_BODY", "unsupported email provider") };
386
+ }
387
+ if (!normalizedString(transport.email.sender)) {
388
+ return { status: 400, body: buildStructuredError("CONTRACT_INVALID_TRANSPORT_BODY", "email.sender is required") };
389
+ }
390
+ if (!normalizedString(transport.email.receiver)) {
391
+ return { status: 400, body: buildStructuredError("CONTRACT_INVALID_TRANSPORT_BODY", "email.receiver is required") };
392
+ }
393
+ if (transport.email.provider === "emailengine") {
394
+ if (!normalizedString(transport.email.emailengine?.base_url) || !normalizedString(transport.email.emailengine?.account)) {
395
+ return {
396
+ status: 400,
397
+ body: buildStructuredError("CONTRACT_INVALID_TRANSPORT_BODY", "email.emailengine.base_url and account are required")
398
+ };
399
+ }
400
+ }
401
+ if (transport.email.provider === "gmail" && (!normalizedString(transport.email.gmail?.client_id) || !normalizedString(transport.email.gmail?.user))) {
402
+ return {
403
+ status: 400,
404
+ body: buildStructuredError("CONTRACT_INVALID_TRANSPORT_BODY", "email.gmail.client_id and user are required")
405
+ };
406
+ }
407
+ }
408
+ return null;
409
+ }
410
+
411
+ function getRuntimeTransport(state) {
412
+ return normalizeTransportConfig(state.config, state.env);
413
+ }
414
+
415
+ function getResolvedSecrets(state, runtime) {
416
+ return readResolvedOpsSecrets(state, runtime.auth.unlockedSecrets);
417
+ }
418
+
419
+ function getTransportResponse(state, runtime) {
420
+ return redactTransportConfig(state.config.runtime?.transport || {}, mergeEnvWithResolvedSecrets(state.env, getResolvedSecrets(state, runtime)));
421
+ }
422
+
423
+ function buildPlatformHeaders(state, runtime) {
424
+ const secrets = getResolvedSecrets(state, runtime);
425
+ return secrets.caller_api_key ? { "X-Platform-Api-Key": secrets.caller_api_key } : {};
426
+ }
427
+
428
+ function findConfiguredExampleHotline(state) {
429
+ return (state.config.responder?.hotlines || []).find((item) => item.hotline_id === LOCAL_EXAMPLE_HOTLINE_ID) || null;
430
+ }
431
+
432
+ function buildExampleVisibilityError(example) {
433
+ if (!example) {
434
+ return {
435
+ status: 404,
436
+ body: buildStructuredError("EXAMPLE_HOTLINE_NOT_CONFIGURED", "official example hotline is not configured locally", {
437
+ stage: "add_example_hotline"
438
+ })
439
+ };
440
+ }
441
+ if (example.submitted_for_review !== true) {
442
+ return {
443
+ status: 409,
444
+ body: buildStructuredError("EXAMPLE_REVIEW_NOT_SUBMITTED", "official example hotline must be submitted for review first", {
445
+ stage: "submit_review"
446
+ })
447
+ };
448
+ }
449
+ return {
450
+ status: 409,
451
+ body: buildStructuredError("EXAMPLE_NOT_VISIBLE_IN_CATALOG", "official example hotline is not yet visible in catalog", {
452
+ stage: "approve_and_catalog",
453
+ review_status: example.review_status || "pending"
454
+ })
455
+ };
456
+ }
457
+
458
+ function ensurePreferenceState(state) {
459
+ state.config.preferences ||= {
460
+ task_types: {},
461
+ caller_policy: {
462
+ mode: "manual",
463
+ responderWhitelist: [],
464
+ hotlineWhitelist: [],
465
+ blocklist: []
466
+ }
467
+ };
468
+ state.config.preferences.task_types ||= {};
469
+ return state.config.preferences.task_types;
470
+ }
471
+
472
+ function ensureCallerPolicyState(state) {
473
+ state.config.preferences ||= {
474
+ task_types: {},
475
+ caller_policy: {
476
+ mode: "manual",
477
+ responderWhitelist: [],
478
+ hotlineWhitelist: [],
479
+ blocklist: []
480
+ }
481
+ };
482
+ state.config.preferences.caller_policy ||= {
483
+ mode: "manual",
484
+ responderWhitelist: [],
485
+ hotlineWhitelist: [],
486
+ blocklist: []
487
+ };
488
+ state.config.preferences.caller_policy.mode ||= "manual";
489
+ state.config.preferences.caller_policy.responderWhitelist ||= [];
490
+ state.config.preferences.caller_policy.hotlineWhitelist ||= [];
491
+ state.config.preferences.caller_policy.blocklist ||= [];
492
+ return state.config.preferences.caller_policy;
493
+ }
494
+
495
+ function normalizeTaskTypeKey(taskType) {
496
+ return normalizedString(taskType)?.toLowerCase() || null;
497
+ }
498
+
499
+ function getTaskTypePreference(state, taskType) {
500
+ const key = normalizeTaskTypeKey(taskType);
501
+ if (!key) {
502
+ return null;
503
+ }
504
+ return ensurePreferenceState(state)[key] || null;
505
+ }
506
+
507
+ function setTaskTypePreference(state, taskType, preference) {
508
+ const key = normalizeTaskTypeKey(taskType);
509
+ if (!key) {
510
+ return null;
511
+ }
512
+ const preferences = ensurePreferenceState(state);
513
+ if (!preference || !preference.hotline_id) {
514
+ delete preferences[key];
515
+ return null;
516
+ }
517
+ preferences[key] = {
518
+ task_type: key,
519
+ hotline_id: preference.hotline_id,
520
+ responder_id: preference.responder_id || null,
521
+ updated_at: nowIso()
522
+ };
523
+ return preferences[key];
524
+ }
525
+
526
+ function summarizeCandidate(item, { selected = false, taskType = null, preferred = false } = {}) {
527
+ const taskTypeMatched = taskType ? (item.task_types || []).includes(taskType) : false;
528
+ const reasons = [];
529
+ if (selected) {
530
+ reasons.push("agent_selected");
531
+ }
532
+ if (preferred) {
533
+ reasons.push("task_type_preference");
534
+ }
535
+ if (taskTypeMatched) {
536
+ reasons.push("task_type_match");
537
+ }
538
+ if (item.availability_status === "healthy") {
539
+ reasons.push("healthy");
540
+ }
541
+ if ((item.capabilities || []).length > 0) {
542
+ reasons.push("capability_signal");
543
+ }
544
+ return {
545
+ hotline_id: item.hotline_id,
546
+ responder_id: item.responder_id,
547
+ display_name: item.display_name || item.hotline_id,
548
+ responder_display_name: item.responder_display_name || item.responder_id,
549
+ task_types: item.task_types || [],
550
+ capabilities: item.capabilities || [],
551
+ tags: item.tags || [],
552
+ availability_status: item.availability_status || "unknown",
553
+ signer_public_key_pem: item.responder_public_key_pem || null,
554
+ template_summary: item.template_ref
555
+ ? {
556
+ template_ref: item.template_ref,
557
+ input_properties: Object.keys(item.input_schema?.properties || {}),
558
+ output_properties: Object.keys(item.output_schema?.properties || {})
559
+ }
560
+ : null,
561
+ difference_note: preferred
562
+ ? "Matches your remembered task-type preference."
563
+ : taskTypeMatched
564
+ ? "Matches the current task type."
565
+ : "Available as a fallback responder route.",
566
+ match_reasons: reasons
567
+ };
568
+ }
569
+
570
+ function scoreCandidate(item, { taskType = null, responderId = null, hotlineId = null, preferred = null } = {}) {
571
+ let score = 0;
572
+ if (hotlineId && item.hotline_id === hotlineId) {
573
+ score += 120;
574
+ }
575
+ if (responderId && item.responder_id === responderId) {
576
+ score += 80;
577
+ }
578
+ if (preferred && item.hotline_id === preferred.hotline_id) {
579
+ score += 60;
580
+ if (!preferred.responder_id || preferred.responder_id === item.responder_id) {
581
+ score += 20;
582
+ }
583
+ }
584
+ if (taskType && (item.task_types || []).includes(taskType)) {
585
+ score += 40;
586
+ }
587
+ if (item.availability_status === "healthy") {
588
+ score += 15;
589
+ }
590
+ if (item.review_status === "approved") {
591
+ score += 10;
592
+ }
593
+ score += Math.min((item.capabilities || []).length, 5);
594
+ return score;
595
+ }
596
+
597
+ async function fetchCatalogCandidates(state, runtime, filters = {}) {
598
+ if (!platformFeaturesEnabled(state)) {
599
+ return listLocalCatalogHotlines(state, runtime, filters);
600
+ }
601
+ const params = new URLSearchParams();
602
+ if (filters.hotline_id) {
603
+ params.set("hotline_id", filters.hotline_id);
604
+ }
605
+ if (filters.responder_id) {
606
+ params.set("responder_id", filters.responder_id);
607
+ }
608
+ if (filters.task_type) {
609
+ params.set("task_type", filters.task_type);
610
+ }
611
+ if (filters.capability) {
612
+ params.set("capability", filters.capability);
613
+ }
614
+
615
+ const response = await requestJson(
616
+ processBaseUrl(state.config.runtime.ports.caller),
617
+ `/controller/hotlines${params.toString() ? `?${params.toString()}` : ""}`,
618
+ {
619
+ headers: buildPlatformHeaders(state, runtime)
620
+ }
621
+ );
622
+ return response.body?.items || [];
623
+ }
624
+
625
+ function buildLocalCatalogHotline(state, runtime, hotline) {
626
+ const responderIdentity = ensureResponderIdentity(state);
627
+ const currentOfficialExample =
628
+ hotline.hotline_id === LOCAL_EXAMPLE_HOTLINE_ID ? buildExampleHotlineDefinition(hotline) : null;
629
+ const { draft } = currentOfficialExample ? { draft: currentOfficialExample } : loadHotlineRegistrationDraft(state, hotline);
630
+ const runtimeStatus = buildResponderRuntimeStatus(state, runtime, hotline.hotline_id);
631
+ const source = draft || {};
632
+ return {
633
+ responder_id: state.config.responder.responder_id || responderIdentity.responder_id,
634
+ hotline_id: hotline.hotline_id,
635
+ display_name: source.display_name || hotline.display_name || hotline.hotline_id,
636
+ description: source.description || null,
637
+ summary: source.summary || null,
638
+ status: hotline.enabled === false ? "disabled" : "enabled",
639
+ review_status: hotline.review_status || "local_only",
640
+ submission_version: null,
641
+ submitted_at: null,
642
+ reviewed_at: null,
643
+ reviewed_by: null,
644
+ review_reason: null,
645
+ availability_status:
646
+ runtimeStatus.responder_running && hotline.enabled !== false && runtimeStatus.hotline_configured ? "healthy" : "offline",
647
+ last_heartbeat_at: null,
648
+ template_ref: source.template_ref || `docs/templates/hotlines/${hotline.hotline_id}/`,
649
+ task_types: source.task_types || hotline.task_types || [],
650
+ capabilities: source.capabilities || hotline.capabilities || [],
651
+ tags: source.tags || hotline.tags || [],
652
+ recommended_for: Array.isArray(source.recommended_for) ? source.recommended_for : [],
653
+ not_recommended_for: Array.isArray(source.not_recommended_for) ? source.not_recommended_for : [],
654
+ limitations: Array.isArray(source.limitations) ? source.limitations : [],
655
+ input_summary: source.input_summary || null,
656
+ output_summary: source.output_summary || null,
657
+ input_schema: source.input_schema || null,
658
+ output_schema: source.output_schema || null,
659
+ input_attachments: source.input_attachments || null,
660
+ output_attachments: source.output_attachments || null,
661
+ input_examples: Array.isArray(source.input_examples) ? source.input_examples : null,
662
+ output_examples: Array.isArray(source.output_examples) ? source.output_examples : null,
663
+ responder_public_key_pem: responderIdentity.public_key_pem,
664
+ responder_public_keys_pem: responderIdentity.public_key_pem ? [responderIdentity.public_key_pem] : [],
665
+ catalog_visibility: "local",
666
+ source: "local"
667
+ };
668
+ }
669
+
670
+ function listLocalCatalogHotlines(state, runtime, filters = {}) {
671
+ return (state.config.responder?.hotlines || [])
672
+ .filter((item) => item.enabled !== false)
673
+ .map((item) => buildLocalCatalogHotline(state, runtime, item))
674
+ .filter((item) => {
675
+ if (filters.hotline_id && item.hotline_id !== filters.hotline_id) {
676
+ return false;
677
+ }
678
+ if (filters.responder_id && item.responder_id !== filters.responder_id) {
679
+ return false;
680
+ }
681
+ if (filters.task_type && !(item.task_types || []).includes(filters.task_type)) {
682
+ return false;
683
+ }
684
+ if (filters.capability && !(item.capabilities || []).includes(filters.capability)) {
685
+ return false;
686
+ }
687
+ return true;
688
+ });
689
+ }
690
+
691
+ function isCallableCatalogItem(item) {
692
+ if (!item) {
693
+ return false;
694
+ }
695
+ if (item.hotline_id === LOCAL_EXAMPLE_HOTLINE_ID) {
696
+ return true;
697
+ }
698
+ if (isPlatformBootstrapDemoCatalogItem(item)) {
699
+ return false;
700
+ }
701
+ if (item.source === "local" || item.review_status === "local_only" || (item.tags || []).includes("local")) {
702
+ return true;
703
+ }
704
+ return item.availability_status !== "offline";
705
+ }
706
+
707
+ function isPlatformBootstrapDemoCatalogItem(item) {
708
+ if (!item || item.source === "local" || !PLATFORM_BOOTSTRAP_DEMO_HOTLINE_IDS.has(item.hotline_id)) {
709
+ return false;
710
+ }
711
+ return (
712
+ item.review_reason === "bootstrap" ||
713
+ item.reviewed_by === "system" ||
714
+ String(item.template_ref || "").startsWith(`docs/templates/hotlines/${item.hotline_id}/`)
715
+ );
716
+ }
717
+
718
+ function mergeCatalogItems(platformItems = [], localItems = []) {
719
+ const seen = new Set();
720
+ const merged = [];
721
+ for (const item of [...localItems, ...platformItems]) {
722
+ const key = `${item.responder_id || ""}:${item.hotline_id || ""}`;
723
+ if (seen.has(key) || !isCallableCatalogItem(item)) {
724
+ continue;
725
+ }
726
+ seen.add(key);
727
+ merged.push(item);
728
+ }
729
+ return merged;
730
+ }
731
+
732
+
733
+ async function testRelayTransport(baseUrl) {
734
+ try {
735
+ const response = await fetch(new URL("/healthz", baseUrl));
736
+ return {
737
+ ok: response.ok,
738
+ kind: "relay_http",
739
+ status: response.status,
740
+ detail: response.ok ? "relay_health_ok" : "relay_health_failed"
741
+ };
742
+ } catch (error) {
743
+ return {
744
+ ok: false,
745
+ kind: "relay_http",
746
+ error: buildStructuredError("TRANSPORT_CONNECTION_FAILED", error instanceof Error ? error.message : "unknown_error")
747
+ };
748
+ }
749
+ }
750
+
751
+ async function testEmailEngineTransport(transport, secrets) {
752
+ if (!secrets.emailengine.access_token) {
753
+ return {
754
+ ok: false,
755
+ kind: "emailengine",
756
+ error: buildStructuredError("AUTH_CREDENTIALS_MISSING", "EmailEngine access token is not configured")
757
+ };
758
+ }
759
+
760
+ try {
761
+ const response = await fetch(
762
+ new URL(`/v1/account/${encodeURIComponent(transport.email.emailengine.account)}`, transport.email.emailengine.base_url),
763
+ {
764
+ headers: {
765
+ Authorization: `Bearer ${secrets.emailengine.access_token}`
766
+ }
767
+ }
768
+ );
769
+ if (!response.ok) {
770
+ return {
771
+ ok: false,
772
+ kind: "emailengine",
773
+ status: response.status,
774
+ error: buildStructuredError("AUTH_INVALID_CREDENTIALS", `EmailEngine returned ${response.status}`)
775
+ };
776
+ }
777
+ return {
778
+ ok: true,
779
+ kind: "emailengine",
780
+ status: response.status,
781
+ detail: "emailengine_auth_ok"
782
+ };
783
+ } catch (error) {
784
+ return {
785
+ ok: false,
786
+ kind: "emailengine",
787
+ error: buildStructuredError("TRANSPORT_CONNECTION_FAILED", error instanceof Error ? error.message : "unknown_error")
788
+ };
789
+ }
790
+ }
791
+
792
+ async function getGmailAccessToken(transport, secrets) {
793
+ if (!secrets.gmail.client_secret || !secrets.gmail.refresh_token) {
794
+ return {
795
+ ok: false,
796
+ error: buildStructuredError("AUTH_CREDENTIALS_MISSING", "Gmail client secret or refresh token is not configured")
797
+ };
798
+ }
799
+
800
+ try {
801
+ const response = await fetch("https://oauth2.googleapis.com/token", {
802
+ method: "POST",
803
+ headers: {
804
+ "content-type": "application/x-www-form-urlencoded"
805
+ },
806
+ body: new URLSearchParams({
807
+ client_id: transport.email.gmail.client_id,
808
+ client_secret: secrets.gmail.client_secret,
809
+ refresh_token: secrets.gmail.refresh_token,
810
+ grant_type: "refresh_token"
811
+ })
812
+ });
813
+ const body = await response.json().catch(() => null);
814
+ if (!response.ok || !body?.access_token) {
815
+ return {
816
+ ok: false,
817
+ status: response.status,
818
+ error: buildStructuredError("AUTH_INVALID_CREDENTIALS", body?.error_description || body?.error || "gmail_token_refresh_failed")
819
+ };
820
+ }
821
+ return {
822
+ ok: true,
823
+ accessToken: body.access_token
824
+ };
825
+ } catch (error) {
826
+ return {
827
+ ok: false,
828
+ error: buildStructuredError("TRANSPORT_CONNECTION_FAILED", error instanceof Error ? error.message : "unknown_error")
829
+ };
830
+ }
831
+ }
832
+
833
+ async function testGmailTransport(transport, secrets) {
834
+ const token = await getGmailAccessToken(transport, secrets);
835
+ if (!token.ok) {
836
+ return {
837
+ ok: false,
838
+ kind: "gmail",
839
+ ...(token.status ? { status: token.status } : {}),
840
+ error: token.error
841
+ };
842
+ }
843
+
844
+ try {
845
+ const response = await fetch(`https://gmail.googleapis.com/gmail/v1/users/${encodeURIComponent(transport.email.gmail.user)}/profile`, {
846
+ headers: {
847
+ Authorization: `Bearer ${token.accessToken}`
848
+ }
849
+ });
850
+ if (!response.ok) {
851
+ const body = await response.json().catch(() => null);
852
+ return {
853
+ ok: false,
854
+ kind: "gmail",
855
+ status: response.status,
856
+ error: buildStructuredError("AUTH_INVALID_CREDENTIALS", body?.error?.message || `gmail_profile_failed_${response.status}`)
857
+ };
858
+ }
859
+ return {
860
+ ok: true,
861
+ kind: "gmail",
862
+ status: response.status,
863
+ detail: "gmail_auth_ok"
864
+ };
865
+ } catch (error) {
866
+ return {
867
+ ok: false,
868
+ kind: "gmail",
869
+ error: buildStructuredError("TRANSPORT_CONNECTION_FAILED", error instanceof Error ? error.message : "unknown_error")
870
+ };
871
+ }
872
+ }
873
+
874
+ async function testTransportConnection(state, runtime) {
875
+ const transport = getRuntimeTransport(state);
876
+ const secrets = getResolvedSecrets(state, runtime).transport;
877
+ if (transport.type === "local") {
878
+ return {
879
+ ok: true,
880
+ kind: "local",
881
+ detail: "local_transport_uses_managed_relay"
882
+ };
883
+ }
884
+ if (transport.type === "relay_http") {
885
+ return testRelayTransport(transport.relay_http.base_url);
886
+ }
887
+ if (transport.email.provider === "emailengine") {
888
+ return testEmailEngineTransport(transport, secrets);
889
+ }
890
+ return testGmailTransport(transport, secrets);
891
+ }
892
+
893
+ function logSeverity(message) {
894
+ if (!message) {
895
+ return null;
896
+ }
897
+ if (/(error|exception|fatal|failed|failure)/i.test(message)) {
898
+ return "error";
899
+ }
900
+ if (/(warn|warning|retry|timeout|denied|reject)/i.test(message)) {
901
+ return "warning";
902
+ }
903
+ return null;
904
+ }
905
+
906
+ export function createOpsSupervisorServer() {
907
+ const state = ensureOpsState();
908
+ appendSupervisorEvent({
909
+ type: "supervisor_created",
910
+ platform_base_url: state.config.platform.base_url
911
+ });
912
+ const runtime = {
913
+ processes: new Map(),
914
+ starting: new Map(),
915
+ relayQueues: new Map(),
916
+ auth: {
917
+ sessions: new Map(),
918
+ unlockedSecrets: null,
919
+ passphrase: null,
920
+ unlockedAt: null
921
+ }
922
+ };
923
+
924
+ function refreshStateFromDisk() {
925
+ Object.assign(state, ensureOpsState());
926
+ return state;
927
+ }
928
+
929
+ function getRuntimeStatus(name) {
930
+ const processInfo = runtime.processes.get(name);
931
+ if (!processInfo) {
932
+ return {
933
+ name,
934
+ running: false,
935
+ launch_mode: null,
936
+ pid: null,
937
+ started_at: null,
938
+ exited_at: null,
939
+ exit_code: null,
940
+ last_error: null
941
+ };
942
+ }
943
+ return {
944
+ name,
945
+ running: !processInfo.exited,
946
+ launch_mode: processInfo.launchMode || null,
947
+ pid: processInfo.child?.pid || processInfo.pid || null,
948
+ started_at: processInfo.startedAt,
949
+ exited_at: processInfo.exitedAt,
950
+ exit_code: processInfo.exitCode,
951
+ last_error: processInfo.lastError
952
+ };
953
+ }
954
+
955
+ function usesManagedRelay() {
956
+ const runtimeTransport = getRuntimeTransport(state);
957
+ const managedRelayBaseUrl = processBaseUrl(state.config.runtime.ports.relay);
958
+ return (
959
+ runtimeTransport.type === "local" ||
960
+ (runtimeTransport.type === "relay_http" && normalizedString(runtimeTransport.relay_http.base_url) === managedRelayBaseUrl)
961
+ );
962
+ }
963
+
964
+ function resolveRelayPackageEntry() {
965
+ const candidatePackageJsons = [
966
+ path.resolve(__dirname, "../node_modules/@delexec/transport-relay/package.json"),
967
+ path.resolve(__dirname, "../../../../platform/apps/transport-relay/package.json")
968
+ ];
969
+
970
+ for (const packageJsonPath of candidatePackageJsons) {
971
+ if (!fs.existsSync(packageJsonPath)) {
972
+ continue;
973
+ }
974
+ const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
975
+ const packageRoot = path.dirname(packageJsonPath);
976
+ if (typeof manifest.bin === "string") {
977
+ return path.resolve(packageRoot, manifest.bin);
978
+ }
979
+ if (manifest.bin && typeof manifest.bin === "object") {
980
+ const relayBin = manifest.bin["delexec-relay"] || Object.values(manifest.bin)[0];
981
+ if (relayBin) {
982
+ return path.resolve(packageRoot, relayBin);
983
+ }
984
+ }
985
+ if (typeof manifest.main === "string") {
986
+ return path.resolve(packageRoot, manifest.main);
987
+ }
988
+ }
989
+
990
+ // Fall back to PATH lookup for delexec-relay / croc-relay binary.
991
+ // @delexec/transport-relay is a platform package and is no longer bundled
992
+ // with @delexec/ops. Operators who install it separately (globally or via
993
+ // the platform compose stack) will have the binary available in PATH.
994
+ for (const binName of ["delexec-relay", "croc-relay"]) {
995
+ try {
996
+ const resolved = execFileSync("which", [binName], { encoding: "utf8" }).trim();
997
+ if (resolved) {
998
+ return resolved;
999
+ }
1000
+ } catch (_) {
1001
+ // not in PATH, try next
1002
+ }
1003
+ }
1004
+
1005
+ return null;
1006
+ }
1007
+
1008
+ function relayLaunchSpec() {
1009
+ const configuredBin = normalizedString(process.env.OPS_RELAY_BIN);
1010
+ if (configuredBin) {
1011
+ return {
1012
+ command: configuredBin,
1013
+ args: parseJsonArrayEnv(process.env.OPS_RELAY_ARGS),
1014
+ mode: "configured_command"
1015
+ };
1016
+ }
1017
+
1018
+ const packageEntry = resolveRelayPackageEntry();
1019
+ if (packageEntry) {
1020
+ return {
1021
+ command: process.execPath,
1022
+ args: [packageEntry],
1023
+ mode: "package_entry"
1024
+ };
1025
+ }
1026
+
1027
+ throw new Error("relay_launch_command_not_found");
1028
+ }
1029
+
1030
+ function shouldUseEmbeddedRelay() {
1031
+ if (!usesManagedRelay()) {
1032
+ return false;
1033
+ }
1034
+ if (normalizedString(process.env.OPS_RELAY_BIN)) {
1035
+ return false;
1036
+ }
1037
+ return !resolveRelayPackageEntry();
1038
+ }
1039
+
1040
+ function relayQueueFor(receiver) {
1041
+ const key = String(receiver || "").trim();
1042
+ if (!runtime.relayQueues.has(key)) {
1043
+ runtime.relayQueues.set(key, []);
1044
+ }
1045
+ return runtime.relayQueues.get(key);
1046
+ }
1047
+
1048
+ async function startEmbeddedRelay() {
1049
+ const current = runtime.processes.get("relay");
1050
+ if (current && !current.exited) {
1051
+ return current;
1052
+ }
1053
+ runtime.relayQueues.clear();
1054
+
1055
+ const server = http.createServer(async (req, res) => {
1056
+ const method = req.method || "GET";
1057
+ const url = new URL(req.url || "/", "http://127.0.0.1");
1058
+ const pathname = url.pathname;
1059
+
1060
+ if (method === "GET" && pathname === "/healthz") {
1061
+ sendJson(res, 200, { ok: true, service: "embedded-local-relay" });
1062
+ return;
1063
+ }
1064
+
1065
+ if (method === "POST" && pathname === "/v1/messages/send") {
1066
+ const body = await parseJsonBody(req);
1067
+ if (!body?.receiver || !body?.envelope) {
1068
+ sendError(res, 400, "receiver_and_envelope_required", "receiver and envelope are required");
1069
+ return;
1070
+ }
1071
+ relayQueueFor(body.receiver).push(body.envelope);
1072
+ sendJson(res, 201, {
1073
+ ok: true,
1074
+ queued: true,
1075
+ receiver: body.receiver,
1076
+ message_id: body.envelope.message_id || null
1077
+ });
1078
+ return;
1079
+ }
1080
+
1081
+ if (method === "POST" && pathname === "/v1/messages/poll") {
1082
+ const body = await parseJsonBody(req);
1083
+ if (!body?.receiver) {
1084
+ sendError(res, 400, "receiver_required", "receiver is required");
1085
+ return;
1086
+ }
1087
+ sendJson(res, 200, {
1088
+ items: relayQueueFor(body.receiver).slice(0, Number(body.limit || 10))
1089
+ });
1090
+ return;
1091
+ }
1092
+
1093
+ if (method === "POST" && pathname === "/v1/messages/ack") {
1094
+ const body = await parseJsonBody(req);
1095
+ if (!body?.receiver || !body?.message_id) {
1096
+ sendError(res, 400, "receiver_and_message_id_required", "receiver and message_id are required");
1097
+ return;
1098
+ }
1099
+ const queue = relayQueueFor(body.receiver);
1100
+ const index = queue.findIndex((item) => item?.message_id === body.message_id);
1101
+ if (index >= 0) {
1102
+ queue.splice(index, 1);
1103
+ }
1104
+ sendJson(res, 200, { acked: index >= 0 });
1105
+ return;
1106
+ }
1107
+
1108
+ if (method === "GET" && pathname === "/v1/messages/peek") {
1109
+ const receiver = normalizedString(url.searchParams.get("receiver"));
1110
+ if (!receiver) {
1111
+ sendError(res, 400, "receiver_required", "receiver is required");
1112
+ return;
1113
+ }
1114
+ const threadId = normalizedString(url.searchParams.get("thread_id"));
1115
+ const items = relayQueueFor(receiver);
1116
+ sendJson(res, 200, {
1117
+ items: threadId ? items.filter((item) => item?.thread_id === threadId) : [...items]
1118
+ });
1119
+ return;
1120
+ }
1121
+
1122
+ const healthMatch = pathname.match(/^\/v1\/receivers\/([^/]+)\/health$/);
1123
+ if (method === "GET" && healthMatch) {
1124
+ const receiver = decodeURIComponent(healthMatch[1]);
1125
+ sendJson(res, 200, {
1126
+ ok: true,
1127
+ receiver,
1128
+ queue_depth: relayQueueFor(receiver).length
1129
+ });
1130
+ return;
1131
+ }
1132
+
1133
+ sendError(res, 404, "not_found", "no matching embedded relay route", { path: pathname });
1134
+ });
1135
+
1136
+ await new Promise((resolve, reject) => {
1137
+ server.once("error", reject);
1138
+ server.listen(state.config.runtime.ports.relay, "127.0.0.1", resolve);
1139
+ });
1140
+
1141
+ const processInfo = {
1142
+ name: "relay",
1143
+ child: null,
1144
+ pid: process.pid,
1145
+ logs: [],
1146
+ startedAt: nowIso(),
1147
+ launchMode: "embedded_local",
1148
+ exited: false,
1149
+ exitedAt: null,
1150
+ exitCode: null,
1151
+ lastError: null,
1152
+ close: async () => {
1153
+ if (processInfo.exited) {
1154
+ return;
1155
+ }
1156
+ await new Promise((resolve) => server.close(resolve));
1157
+ processInfo.exited = true;
1158
+ processInfo.exitedAt = nowIso();
1159
+ processInfo.exitCode = 0;
1160
+ }
1161
+ };
1162
+
1163
+ server.on("error", (error) => {
1164
+ processInfo.lastError = error instanceof Error ? error.message : "embedded_relay_error";
1165
+ appendSupervisorEvent({
1166
+ type: "service_error",
1167
+ service: "relay",
1168
+ message: processInfo.lastError
1169
+ });
1170
+ });
1171
+ server.on("close", () => {
1172
+ if (!processInfo.exited) {
1173
+ processInfo.exited = true;
1174
+ processInfo.exitedAt = nowIso();
1175
+ processInfo.exitCode = 0;
1176
+ }
1177
+ appendSupervisorEvent({
1178
+ type: "service_exit",
1179
+ service: "relay",
1180
+ exit_code: processInfo.exitCode
1181
+ });
1182
+ });
1183
+
1184
+ runtime.processes.set("relay", processInfo);
1185
+ appendSupervisorEvent({
1186
+ type: "service_started",
1187
+ service: "relay",
1188
+ pid: process.pid,
1189
+ launch_mode: "embedded_local"
1190
+ });
1191
+ return processInfo;
1192
+ }
1193
+
1194
+ async function stopProcessInfo(processInfo) {
1195
+ if (!processInfo || processInfo.exited) {
1196
+ return;
1197
+ }
1198
+ if (typeof processInfo.close === "function") {
1199
+ await processInfo.close();
1200
+ return;
1201
+ }
1202
+ if (processInfo.child) {
1203
+ processInfo.child.kill();
1204
+ const deadline = Date.now() + 3000;
1205
+ while (!processInfo.exited && Date.now() < deadline) {
1206
+ await new Promise((r) => setTimeout(r, 100));
1207
+ }
1208
+ }
1209
+ }
1210
+
1211
+ function serviceEnv(name) {
1212
+ const ports = state.config.runtime.ports;
1213
+ const runtimeTransport = getRuntimeTransport(state);
1214
+ const resolvedSecrets = getResolvedSecrets(state, runtime);
1215
+ const envWithSecrets = mergeEnvWithResolvedSecrets(state.env, resolvedSecrets);
1216
+ const relayBaseUrl =
1217
+ runtimeTransport.type === "relay_http"
1218
+ ? runtimeTransport.relay_http.base_url
1219
+ : processBaseUrl(ports.relay);
1220
+ const transportEnv = buildTransportEnvUpdates(
1221
+ runtimeTransport.type === "local"
1222
+ ? {
1223
+ ...runtimeTransport,
1224
+ relay_http: { base_url: relayBaseUrl }
1225
+ }
1226
+ : runtimeTransport,
1227
+ envWithSecrets
1228
+ );
1229
+ const base = {
1230
+ ...process.env,
1231
+ DELEXEC_HOME: process.env.DELEXEC_HOME || path.dirname(state.envFile),
1232
+ PLATFORM_API_BASE_URL: state.config.platform.base_url,
1233
+ CALLER_PLATFORM_API_KEY: resolvedSecrets.caller_api_key || "",
1234
+ PLATFORM_API_KEY: resolvedSecrets.caller_api_key || "",
1235
+ CALLER_CONTACT_EMAIL: state.config.caller.contact_email || "",
1236
+ CALLER_REGISTRATION_MODE: state.config.caller.registration_mode || "",
1237
+ RESPONDER_ID: state.config.responder.responder_id || "",
1238
+ RESPONDER_SIGNING_PUBLIC_KEY_PEM: state.env.RESPONDER_SIGNING_PUBLIC_KEY_PEM || "",
1239
+ RESPONDER_SIGNING_PRIVATE_KEY_PEM: state.env.RESPONDER_SIGNING_PRIVATE_KEY_PEM || "",
1240
+ HOTLINE_IDS: (state.config.responder.hotlines || []).map((item) => item.hotline_id).join(","),
1241
+ RESPONDER_PLATFORM_API_KEY: resolvedSecrets.responder_platform_api_key || "",
1242
+ TRANSPORT_BASE_URL: relayBaseUrl,
1243
+ TRANSPORT_TYPE: runtimeTransport.type,
1244
+ TRANSPORT_PROVIDER: transportEnv.TRANSPORT_PROVIDER || "",
1245
+ TRANSPORT_EMAIL_PROVIDER: transportEnv.TRANSPORT_EMAIL_PROVIDER || "",
1246
+ TRANSPORT_EMAIL_MODE: transportEnv.TRANSPORT_EMAIL_MODE || "",
1247
+ TRANSPORT_EMAIL_SENDER: transportEnv.TRANSPORT_EMAIL_SENDER || "",
1248
+ TRANSPORT_EMAIL_RECEIVER: transportEnv.TRANSPORT_EMAIL_RECEIVER || "",
1249
+ TRANSPORT_EMAIL_POLL_INTERVAL_MS: transportEnv.TRANSPORT_EMAIL_POLL_INTERVAL_MS || "",
1250
+ TRANSPORT_EMAILENGINE_BASE_URL: state.env.TRANSPORT_EMAILENGINE_BASE_URL || "",
1251
+ TRANSPORT_EMAILENGINE_ACCOUNT: state.env.TRANSPORT_EMAILENGINE_ACCOUNT || "",
1252
+ TRANSPORT_EMAILENGINE_ACCESS_TOKEN: resolvedSecrets.transport.emailengine.access_token || "",
1253
+ TRANSPORT_GMAIL_CLIENT_ID: state.env.TRANSPORT_GMAIL_CLIENT_ID || "",
1254
+ TRANSPORT_GMAIL_USER: state.env.TRANSPORT_GMAIL_USER || "",
1255
+ TRANSPORT_GMAIL_CLIENT_SECRET: resolvedSecrets.transport.gmail.client_secret || "",
1256
+ TRANSPORT_GMAIL_REFRESH_TOKEN: resolvedSecrets.transport.gmail.refresh_token || ""
1257
+ };
1258
+
1259
+ if (name === "relay") {
1260
+ return {
1261
+ ...base,
1262
+ PORT: String(ports.relay),
1263
+ SERVICE_NAME: "transport-relay"
1264
+ };
1265
+ }
1266
+ if (name === "caller") {
1267
+ return {
1268
+ ...base,
1269
+ PORT: String(ports.caller),
1270
+ SERVICE_NAME: "caller-controller",
1271
+ PLATFORM_ENABLED: String(platformFeaturesEnabled(state)),
1272
+ TRANSPORT_RECEIVER: "caller-controller"
1273
+ };
1274
+ }
1275
+ if (name === "skill-adapter") {
1276
+ return {
1277
+ ...base,
1278
+ PORT: String(ports.skill_adapter || 8091),
1279
+ SERVICE_NAME: "caller-skill-adapter",
1280
+ PLATFORM_ENABLED: String(platformFeaturesEnabled(state)),
1281
+ CALLER_CONTROLLER_BASE_URL: processBaseUrl(ports.caller)
1282
+ };
1283
+ }
1284
+ if (name === "mcp-adapter") {
1285
+ return {
1286
+ ...base,
1287
+ PORT: String(ports.mcp_adapter || 8092),
1288
+ SERVICE_NAME: "caller-skill-mcp-adapter",
1289
+ MCP_ADAPTER_TRANSPORT: "http",
1290
+ CALLER_SKILL_BASE_URL: processBaseUrl(ports.skill_adapter || 8091)
1291
+ };
1292
+ }
1293
+ return {
1294
+ ...base,
1295
+ PORT: String(ports.responder),
1296
+ SERVICE_NAME: "responder-controller",
1297
+ TRANSPORT_RECEIVER: state.config.responder.responder_id || "responder-controller"
1298
+ };
1299
+ }
1300
+
1301
+ function resolveWorkspaceServiceEntry(sourceRelativePath, packageName) {
1302
+ const sourceEntry = path.resolve(__dirname, sourceRelativePath);
1303
+ if (fs.existsSync(sourceEntry)) {
1304
+ return sourceEntry;
1305
+ }
1306
+ return require.resolve(packageName);
1307
+ }
1308
+
1309
+ function serviceEntry(name) {
1310
+ if (name === "caller") {
1311
+ return resolveWorkspaceServiceEntry("../../caller-controller/src/server.js", "@delexec/caller-controller");
1312
+ }
1313
+ if (name === "skill-adapter") {
1314
+ return resolveWorkspaceServiceEntry("../../caller-skill-adapter/src/server.js", "@delexec/caller-skill-adapter");
1315
+ }
1316
+ if (name === "mcp-adapter") {
1317
+ return resolveWorkspaceServiceEntry("../../caller-skill-mcp-adapter/src/server.js", "@delexec/caller-skill-mcp-adapter");
1318
+ }
1319
+ return resolveWorkspaceServiceEntry("../../responder-controller/src/server.js", "@delexec/responder-controller");
1320
+ }
1321
+
1322
+ function buildMcpAdapterSpec() {
1323
+ const callerSkillBaseUrl = processBaseUrl(state.config.runtime.ports.skill_adapter || 8091);
1324
+ const mcpEntry = resolveWorkspaceServiceEntry("../../caller-skill-mcp-adapter/src/server.js", "@delexec/caller-skill-mcp-adapter");
1325
+ const httpBaseUrl = processBaseUrl(state.config.runtime.ports.mcp_adapter || 8092);
1326
+ return {
1327
+ mode: "multi_transport",
1328
+ available: true,
1329
+ recommended_for: ["codex", "cursor", "claude-code"],
1330
+ preferred_transport: "streamable_http",
1331
+ stdio: {
1332
+ mode: "stdio",
1333
+ command: process.execPath,
1334
+ args: [mcpEntry],
1335
+ env: {
1336
+ CALLER_SKILL_BASE_URL: callerSkillBaseUrl
1337
+ }
1338
+ },
1339
+ streamable_http: {
1340
+ mode: "streamable_http",
1341
+ url: appendPath(httpBaseUrl, "/mcp"),
1342
+ health_url: appendPath(httpBaseUrl, "/healthz")
1343
+ },
1344
+ entry_file: mcpEntry,
1345
+ caller_skill_base_url: callerSkillBaseUrl,
1346
+ base_url: httpBaseUrl
1347
+ };
1348
+ }
1349
+
1350
+ function serviceLaunchSpec(name) {
1351
+ if (name === "relay") {
1352
+ return relayLaunchSpec();
1353
+ }
1354
+ return {
1355
+ command: process.execPath,
1356
+ args: [serviceEntry(name)],
1357
+ mode: "node_entry"
1358
+ };
1359
+ }
1360
+
1361
+ function captureLog(processInfo, stream, chunk) {
1362
+ const ts = new Date().toTimeString().slice(0, 8); // HH:mm:ss
1363
+ const lines = chunk.toString("utf8").split(/\r?\n/);
1364
+ for (const raw of lines) {
1365
+ const line = raw.trimEnd();
1366
+ if (!line) continue;
1367
+ const stamped = `${ts} ${line}`;
1368
+ processInfo.logs.push(stamped);
1369
+ if (processInfo.logs.length > 200) processInfo.logs.shift();
1370
+ appendServiceLog(processInfo.name, `${stamped}\n`);
1371
+ const mirrored = `[${processInfo.name}] ${stamped}\n`;
1372
+ if (stream === "stderr") {
1373
+ process.stderr.write(mirrored);
1374
+ } else {
1375
+ process.stdout.write(mirrored);
1376
+ }
1377
+ }
1378
+ }
1379
+
1380
+ async function ensureService(name) {
1381
+ const current = runtime.processes.get(name);
1382
+ if (current && !current.exited) {
1383
+ return current;
1384
+ }
1385
+ const inflight = runtime.starting.get(name);
1386
+ if (inflight) {
1387
+ return inflight;
1388
+ }
1389
+ const startPromise = (async () => {
1390
+ if (name === "relay" && shouldUseEmbeddedRelay()) {
1391
+ return startEmbeddedRelay();
1392
+ }
1393
+ const ports = state.config.runtime.ports;
1394
+ const portMap = {
1395
+ caller: ports.caller,
1396
+ responder: ports.responder,
1397
+ relay: ports.relay,
1398
+ "skill-adapter": ports.skill_adapter || 8091,
1399
+ "mcp-adapter": ports.mcp_adapter || 8092
1400
+ };
1401
+ // Kill any orphaned process holding the port before starting
1402
+ const targetPort = portMap[name];
1403
+ if (targetPort) {
1404
+ await new Promise((resolve) => {
1405
+ const killer = spawn(process.execPath, [
1406
+ "-e",
1407
+ `const { execSync } = require("child_process");
1408
+ try {
1409
+ const out = execSync("lsof -ti:${targetPort} 2>/dev/null", { encoding: "utf8" }).trim();
1410
+ if (out) { out.split("\\n").forEach(pid => { try { process.kill(Number(pid), "SIGKILL"); } catch(_) {} }); }
1411
+ } catch(_) {}
1412
+ `
1413
+ ]);
1414
+ killer.on("exit", resolve);
1415
+ setTimeout(resolve, 2000);
1416
+ });
1417
+ // Brief pause to let OS release the port
1418
+ await new Promise((r) => setTimeout(r, 300));
1419
+ }
1420
+ const launch = serviceLaunchSpec(name);
1421
+ const child = spawn(launch.command, launch.args, {
1422
+ env: serviceEnv(name),
1423
+ stdio: ["ignore", "pipe", "pipe"]
1424
+ });
1425
+ const processInfo = {
1426
+ name,
1427
+ child,
1428
+ logs: [],
1429
+ startedAt: nowIso(),
1430
+ launchMode: launch.mode,
1431
+ exited: false,
1432
+ exitedAt: null,
1433
+ exitCode: null,
1434
+ lastError: null
1435
+ };
1436
+ child.stdout.on("data", (chunk) => captureLog(processInfo, "stdout", chunk.toString("utf8")));
1437
+ child.stderr.on("data", (chunk) => captureLog(processInfo, "stderr", chunk.toString("utf8")));
1438
+ child.on("error", (error) => {
1439
+ processInfo.lastError = error instanceof Error ? error.message : "unknown_error";
1440
+ appendSupervisorEvent({
1441
+ type: "service_error",
1442
+ service: name,
1443
+ message: processInfo.lastError
1444
+ });
1445
+ });
1446
+ child.on("exit", (code) => {
1447
+ processInfo.exited = true;
1448
+ processInfo.exitedAt = nowIso();
1449
+ processInfo.exitCode = code;
1450
+ appendSupervisorEvent({
1451
+ type: "service_exit",
1452
+ service: name,
1453
+ exit_code: code
1454
+ });
1455
+ });
1456
+ runtime.processes.set(name, processInfo);
1457
+ appendSupervisorEvent({
1458
+ type: "service_started",
1459
+ service: name,
1460
+ pid: child.pid
1461
+ });
1462
+ return processInfo;
1463
+ })();
1464
+ runtime.starting.set(name, startPromise);
1465
+ try {
1466
+ return await startPromise;
1467
+ } finally {
1468
+ if (runtime.starting.get(name) === startPromise) {
1469
+ runtime.starting.delete(name);
1470
+ }
1471
+ }
1472
+ }
1473
+
1474
+ async function waitForRelay(maxWaitMs = 8000) {
1475
+ const relayUrl = `http://127.0.0.1:${state.config.runtime.ports?.relay || 8090}`;
1476
+ const start = Date.now();
1477
+ while (Date.now() - start < maxWaitMs) {
1478
+ try {
1479
+ const res = await fetch(`${relayUrl}/healthz`);
1480
+ if (res.ok) return;
1481
+ } catch {
1482
+ // not ready yet
1483
+ }
1484
+ await new Promise((r) => setTimeout(r, 100));
1485
+ }
1486
+ }
1487
+
1488
+ async function waitForServiceHealth(name, maxWaitMs = 8000) {
1489
+ const start = Date.now();
1490
+ while (Date.now() - start < maxWaitMs) {
1491
+ const health = await fetchHealth(name);
1492
+ if (health?.status === 200) {
1493
+ return health;
1494
+ }
1495
+ await new Promise((r) => setTimeout(r, 100));
1496
+ }
1497
+ return null;
1498
+ }
1499
+
1500
+ async function ensureBaseServices() {
1501
+ if (usesManagedRelay()) {
1502
+ await ensureService("relay");
1503
+ if (shouldUseEmbeddedRelay()) {
1504
+ await waitForServiceHealth("relay");
1505
+ } else {
1506
+ await waitForRelay();
1507
+ }
1508
+ }
1509
+ await ensureService("caller");
1510
+ await waitForServiceHealth("caller");
1511
+ await ensureService("skill-adapter");
1512
+ await waitForServiceHealth("skill-adapter");
1513
+ await ensureService("mcp-adapter");
1514
+ await waitForServiceHealth("mcp-adapter");
1515
+ if (state.config.responder.enabled) {
1516
+ await ensureService("responder");
1517
+ await waitForServiceHealth("responder");
1518
+ }
1519
+ }
1520
+
1521
+ async function prepareCallConfirmation(body = {}) {
1522
+ await ensureBaseServices();
1523
+ const taskType = normalizedString(body.task_type);
1524
+ const hotlineId = normalizedString(body.hotline_id);
1525
+ const responderId = normalizedString(body.responder_id);
1526
+ const capability = normalizedString(body.capability);
1527
+ const preference = getTaskTypePreference(state, taskType);
1528
+
1529
+ const items = await fetchCatalogCandidates(state, runtime, {
1530
+ task_type: taskType,
1531
+ hotline_id: hotlineId,
1532
+ responder_id: responderId,
1533
+ capability
1534
+ });
1535
+
1536
+ const candidates = items
1537
+ .map((item) => ({
1538
+ raw: item,
1539
+ score: scoreCandidate(item, {
1540
+ taskType,
1541
+ hotlineId,
1542
+ responderId,
1543
+ preferred: preference
1544
+ })
1545
+ }))
1546
+ .sort((left, right) => right.score - left.score)
1547
+ .slice(0, 5);
1548
+
1549
+ if (candidates.length === 0) {
1550
+ return {
1551
+ status: 404,
1552
+ body: buildStructuredError("HOTLINE_CANDIDATES_NOT_FOUND", "no visible hotline candidates matched the current request", {
1553
+ task_type: taskType,
1554
+ responder_id: responderId,
1555
+ hotline_id: hotlineId
1556
+ })
1557
+ };
1558
+ }
1559
+
1560
+ const selected = candidates[0].raw;
1561
+ const selectedSummary = summarizeCandidate(selected, {
1562
+ selected: true,
1563
+ taskType,
1564
+ preferred: Boolean(preference && selected.hotline_id === preference.hotline_id)
1565
+ });
1566
+
1567
+ return {
1568
+ status: 200,
1569
+ body: {
1570
+ task_type: taskType,
1571
+ always_ask: true,
1572
+ remembered_preference: preference,
1573
+ selection_reason: selectedSummary.match_reasons.join(" · "),
1574
+ selected_hotline: selectedSummary,
1575
+ candidate_hotlines: candidates.map(({ raw }) =>
1576
+ summarizeCandidate(raw, {
1577
+ selected: raw.hotline_id === selected.hotline_id && raw.responder_id === selected.responder_id,
1578
+ taskType,
1579
+ preferred: Boolean(preference && raw.hotline_id === preference.hotline_id)
1580
+ })
1581
+ )
1582
+ }
1583
+ };
1584
+ }
1585
+
1586
+ async function confirmPreparedCall(body = {}) {
1587
+ await ensureBaseServices();
1588
+ const chosenHotlineId = normalizedString(body.hotline_id);
1589
+ const chosenResponderId = normalizedString(body.responder_id);
1590
+ const taskType = normalizedString(body.task_type);
1591
+ if (!chosenHotlineId || !chosenResponderId || !taskType) {
1592
+ return {
1593
+ status: 400,
1594
+ body: buildStructuredError(
1595
+ "CONTRACT_INVALID_CONFIRM_BODY",
1596
+ "hotline_id, responder_id, and task_type are required for call confirmation"
1597
+ )
1598
+ };
1599
+ }
1600
+
1601
+ const candidates = await fetchCatalogCandidates(state, runtime, {
1602
+ hotline_id: chosenHotlineId,
1603
+ responder_id: chosenResponderId,
1604
+ task_type: taskType
1605
+ });
1606
+ const selected = candidates.find((item) => item.hotline_id === chosenHotlineId && item.responder_id === chosenResponderId);
1607
+ if (!selected) {
1608
+ return {
1609
+ status: 409,
1610
+ body: buildStructuredError("HOTLINE_NO_LONGER_VISIBLE", "chosen hotline is no longer visible for this caller", {
1611
+ hotline_id: chosenHotlineId,
1612
+ responder_id: chosenResponderId
1613
+ })
1614
+ };
1615
+ }
1616
+
1617
+ if (body.remember_for_task_type === true) {
1618
+ setTaskTypePreference(state, taskType, {
1619
+ hotline_id: chosenHotlineId,
1620
+ responder_id: chosenResponderId
1621
+ });
1622
+ state.env = saveOpsState(state);
1623
+ }
1624
+
1625
+ return requestJson(processBaseUrl(state.config.runtime.ports.caller), "/controller/remote-requests", {
1626
+ method: "POST",
1627
+ headers: buildPlatformHeaders(state, runtime),
1628
+ body: {
1629
+ responder_id: chosenResponderId,
1630
+ hotline_id: chosenHotlineId,
1631
+ task_type: taskType,
1632
+ input: body.input || { text: normalizedString(body.text) || "" },
1633
+ payload: body.payload || { text: normalizedString(body.text) || "" },
1634
+ output_schema: body.output_schema || {
1635
+ type: "object",
1636
+ properties: {
1637
+ summary: { type: "string" }
1638
+ }
1639
+ }
1640
+ }
1641
+ });
1642
+ }
1643
+
1644
+ async function reloadResponderIfRunning() {
1645
+ if (!state.config.responder.enabled) {
1646
+ return;
1647
+ }
1648
+ const processInfo = runtime.processes.get("responder");
1649
+ if (processInfo && !processInfo.exited) {
1650
+ await stopProcessInfo(processInfo);
1651
+ }
1652
+ await ensureService("responder");
1653
+ }
1654
+
1655
+ async function fetchHealth(name) {
1656
+ const portKey = name === "skill-adapter" ? "skill_adapter" : name === "mcp-adapter" ? "mcp_adapter" : name;
1657
+ const port = state.config.runtime.ports[portKey];
1658
+ if (name === "relay" && !usesManagedRelay()) {
1659
+ const runtimeTransport = getRuntimeTransport(state);
1660
+ if (runtimeTransport.type !== "relay_http") {
1661
+ return null;
1662
+ }
1663
+ try {
1664
+ return await requestJson(runtimeTransport.relay_http.base_url, "/healthz");
1665
+ } catch (error) {
1666
+ return { status: 503, body: { ok: false, error: error instanceof Error ? error.message : "unknown_error" } };
1667
+ }
1668
+ }
1669
+ try {
1670
+ return await requestJson(processBaseUrl(port), "/healthz");
1671
+ } catch (error) {
1672
+ return { status: 503, body: { ok: false, error: error instanceof Error ? error.message : "unknown_error" } };
1673
+ }
1674
+ }
1675
+
1676
+ async function fetchRecentRequestsSummary() {
1677
+ try {
1678
+ const response = await requestJson(processBaseUrl(state.config.runtime.ports.caller), "/controller/requests");
1679
+ const items = response.body?.items || [];
1680
+ const byStatus = items.reduce((summary, item) => {
1681
+ const key = item.status || "UNKNOWN";
1682
+ summary[key] = (summary[key] || 0) + 1;
1683
+ return summary;
1684
+ }, {});
1685
+ return {
1686
+ total: items.length,
1687
+ by_status: byStatus,
1688
+ latest: items.slice(0, 5).map((item) => ({
1689
+ request_id: item.request_id,
1690
+ status: item.status,
1691
+ updated_at: item.updated_at || item.created_at || null
1692
+ }))
1693
+ };
1694
+ } catch {
1695
+ return {
1696
+ total: 0,
1697
+ by_status: {},
1698
+ latest: []
1699
+ };
1700
+ }
1701
+ }
1702
+
1703
+ async function buildStatus() {
1704
+ await syncResponderReviewStatusesFromPlatform();
1705
+ const hotlines = state.config.responder.hotlines || [];
1706
+ const secrets = getResolvedSecrets(state, runtime);
1707
+ const runtimeTransport = getRuntimeTransport(state);
1708
+ const pendingReviewCount = platformFeaturesEnabled(state)
1709
+ ? hotlines.filter((item) => item.submitted_for_review !== true).length
1710
+ : 0;
1711
+ const reviewStatusCounts = hotlines.reduce((counts, item) => {
1712
+ const key = item.review_status || "local_only";
1713
+ counts[key] = (counts[key] || 0) + 1;
1714
+ return counts;
1715
+ }, {});
1716
+ state.config.caller.api_key_configured = Boolean(secrets.caller_api_key);
1717
+ state.config.caller.registration_mode ||= state.config.caller.api_key_configured ? "platform" : null;
1718
+ state.config.platform_console ||= {};
1719
+ state.config.platform_console.admin_api_key_configured = Boolean(secrets.platform_admin_api_key);
1720
+ return {
1721
+ ok: true,
1722
+ config: state.config,
1723
+ auth: getAuthState(runtime, state),
1724
+ debug: {
1725
+ logs_dir: path.join(path.dirname(state.envFile), "logs"),
1726
+ event_log: getSupervisorEventsFile(),
1727
+ service_logs: {
1728
+ relay: getServiceLogFile("relay"),
1729
+ caller: getServiceLogFile("caller"),
1730
+ skill_adapter: getServiceLogFile("skill-adapter"),
1731
+ mcp_adapter: getServiceLogFile("mcp-adapter"),
1732
+ responder: getServiceLogFile("responder")
1733
+ }
1734
+ },
1735
+ responder: {
1736
+ enabled: state.config.responder.enabled,
1737
+ responder_id: state.config.responder.responder_id,
1738
+ display_name: state.config.responder.display_name,
1739
+ hotline_count: hotlines.length,
1740
+ pending_review_count: pendingReviewCount,
1741
+ review_summary: reviewStatusCounts,
1742
+ platform_enabled: platformFeaturesEnabled(state)
1743
+ },
1744
+ caller: {
1745
+ enabled: state.config.caller.enabled !== false,
1746
+ registered: state.config.caller.registration_mode === "local_only" || state.config.caller.api_key_configured === true,
1747
+ registration_mode: state.config.caller.registration_mode || null,
1748
+ contact_email: state.config.caller.contact_email || null,
1749
+ api_key_configured: state.config.caller.api_key_configured === true,
1750
+ platform_enabled: platformFeaturesEnabled(state)
1751
+ },
1752
+ requests: await fetchRecentRequestsSummary(),
1753
+ runtime: {
1754
+ supervisor: {
1755
+ port: state.config.runtime.ports.supervisor
1756
+ },
1757
+ relay: {
1758
+ ...getRuntimeStatus("relay"),
1759
+ managed: usesManagedRelay(),
1760
+ transport_type: runtimeTransport.type,
1761
+ base_url: runtimeTransport.type === "relay_http" ? runtimeTransport.relay_http.base_url : processBaseUrl(state.config.runtime.ports.relay),
1762
+ health: await fetchHealth("relay")
1763
+ },
1764
+ caller: {
1765
+ ...getRuntimeStatus("caller"),
1766
+ health: await fetchHealth("caller")
1767
+ },
1768
+ skill_adapter: {
1769
+ ...getRuntimeStatus("skill-adapter"),
1770
+ health: await fetchHealth("skill-adapter")
1771
+ },
1772
+ mcp_adapter: {
1773
+ ...getRuntimeStatus("mcp-adapter"),
1774
+ health: await fetchHealth("mcp-adapter"),
1775
+ spec: buildMcpAdapterSpec()
1776
+ },
1777
+ responder: {
1778
+ ...getRuntimeStatus("responder"),
1779
+ health: state.config.responder.enabled ? await fetchHealth("responder") : null
1780
+ }
1781
+ }
1782
+ };
1783
+ }
1784
+
1785
+ async function buildExampleDiagnostics() {
1786
+ const status = await buildStatus();
1787
+ const uiPort = Number(process.env.DELEXEC_OPS_UI_PORT || 4173);
1788
+ const serviceCheck = (label, service) => {
1789
+ const running = service?.running === true;
1790
+ const healthy = service?.health?.status === 200 || service?.health?.body?.ok === true;
1791
+ return {
1792
+ name: label,
1793
+ status: healthy ? "ok" : running ? "warn" : "fail",
1794
+ detail: healthy
1795
+ ? `${label} health check is passing.`
1796
+ : running
1797
+ ? `${label} process is running but health has not reported ok yet.`
1798
+ : `${label} process is not running.`
1799
+ };
1800
+ };
1801
+
1802
+ const callerRegistered = status.caller?.registered === true;
1803
+ const responderEnabled = status.responder?.enabled === true;
1804
+ const localExample = (state.config.responder.hotlines || []).find((item) => item.hotline_id === LOCAL_EXAMPLE_HOTLINE_ID);
1805
+
1806
+ return {
1807
+ generated_at: nowIso(),
1808
+ checks: [
1809
+ {
1810
+ name: "caller_registration",
1811
+ status: callerRegistered ? "ok" : "fail",
1812
+ detail: callerRegistered
1813
+ ? `Caller is registered in ${status.caller.registration_mode || "configured"} mode.`
1814
+ : "Caller is not registered yet; run bootstrap or auth register before sending calls."
1815
+ },
1816
+ {
1817
+ name: "local_example_hotline",
1818
+ status: localExample?.enabled === false ? "fail" : localExample ? "ok" : "fail",
1819
+ detail: localExample
1820
+ ? `${LOCAL_EXAMPLE_DISPLAY_NAME} is configured locally.`
1821
+ : `${LOCAL_EXAMPLE_DISPLAY_NAME} is not configured locally; add the official example first.`
1822
+ },
1823
+ {
1824
+ name: "responder_enabled",
1825
+ status: responderEnabled ? "ok" : "fail",
1826
+ detail: responderEnabled ? "Local responder is enabled." : "Local responder is disabled."
1827
+ },
1828
+ serviceCheck("relay", status.runtime?.relay),
1829
+ serviceCheck("caller", status.runtime?.caller),
1830
+ serviceCheck("responder", status.runtime?.responder),
1831
+ serviceCheck("skill_adapter", status.runtime?.skill_adapter),
1832
+ serviceCheck("mcp_adapter", status.runtime?.mcp_adapter)
1833
+ ],
1834
+ links: [
1835
+ { label: "Ops Console", url: processBaseUrl(uiPort) },
1836
+ { label: "Runtime", url: appendPath(processBaseUrl(uiPort), "/general/runtime") },
1837
+ { label: "Calls", url: appendPath(processBaseUrl(uiPort), "/caller/calls") },
1838
+ { label: "Catalog", url: appendPath(processBaseUrl(uiPort), "/caller/catalog") },
1839
+ { label: "Supervisor Health", url: appendPath(processBaseUrl(state.config.runtime.ports.supervisor), "/healthz") }
1840
+ ],
1841
+ next_steps: [
1842
+ "Run delexec-ops status to inspect the local runtime.",
1843
+ "Open Calls after this request finishes to inspect the result package.",
1844
+ "Run delexec-ops debug-snapshot if any check is warn or fail."
1845
+ ]
1846
+ };
1847
+ }
1848
+
1849
+ function buildRuntimeAlerts(service, { maxItems = 20 } = {}) {
1850
+ const events = readSupervisorEventTail({ maxLines: 200 })
1851
+ .filter((event) => {
1852
+ if (service === "supervisor") {
1853
+ return true;
1854
+ }
1855
+ return event.service === service;
1856
+ })
1857
+ .flatMap((event) => {
1858
+ if (event.type === "service_error") {
1859
+ return [
1860
+ {
1861
+ at: event.at,
1862
+ service: event.service,
1863
+ severity: "error",
1864
+ source: "event",
1865
+ message: event.message || "service_error"
1866
+ }
1867
+ ];
1868
+ }
1869
+ if (event.type === "service_exit" && event.exit_code !== 0 && event.exit_code !== null) {
1870
+ return [
1871
+ {
1872
+ at: event.at,
1873
+ service: event.service,
1874
+ severity: "error",
1875
+ source: "event",
1876
+ message: `service exited with code ${event.exit_code}`
1877
+ }
1878
+ ];
1879
+ }
1880
+ return [];
1881
+ });
1882
+
1883
+ const logAlerts = (service === "supervisor" ? [] : readServiceLogTail(service, { maxLines: 200 }))
1884
+ .flatMap((line) => {
1885
+ const severity = logSeverity(line);
1886
+ if (!severity) {
1887
+ return [];
1888
+ }
1889
+ return [
1890
+ {
1891
+ at: null,
1892
+ service,
1893
+ severity,
1894
+ source: "log",
1895
+ message: line.trim()
1896
+ }
1897
+ ];
1898
+ });
1899
+
1900
+ return [...events, ...logAlerts].slice(-maxItems).reverse();
1901
+ }
1902
+
1903
+ async function registerCaller(contactEmail, { localOnly = false, forcePlatform = false } = {}) {
1904
+ if (!forcePlatform && (localOnly || !platformFeaturesEnabled(state))) {
1905
+ const nextEmail = normalizedString(contactEmail) || state.config.caller.contact_email || null;
1906
+ state.config.caller.contact_email = nextEmail;
1907
+ state.config.caller.registration_mode = "local_only";
1908
+ state.config.caller.api_key = null;
1909
+ state.config.caller.api_key_configured = false;
1910
+ state.env = saveOpsState(state);
1911
+ return {
1912
+ status: 201,
1913
+ body: {
1914
+ ok: true,
1915
+ registered: true,
1916
+ mode: "local_only",
1917
+ contact_email: nextEmail
1918
+ }
1919
+ };
1920
+ }
1921
+ const response = await requestJson(state.config.platform.base_url, "/v1/users/register", {
1922
+ method: "POST",
1923
+ body: {
1924
+ contact_email: contactEmail
1925
+ }
1926
+ });
1927
+ if (response.status !== 201) {
1928
+ return response;
1929
+ }
1930
+ state.config.caller.contact_email = response.body.contact_email || contactEmail;
1931
+ state.config.caller.registration_mode = "platform";
1932
+ state.config.caller.api_key_configured = true;
1933
+ state.config.platform.enabled = true;
1934
+ if (hasEncryptedSecretStore()) {
1935
+ writeOpsSecrets(runtime.auth.passphrase, {
1936
+ [OPS_SECRET_KEYS.caller_api_key]: response.body.api_key
1937
+ });
1938
+ runtime.auth.unlockedSecrets = unlockOpsSecrets(runtime.auth.passphrase);
1939
+ scrubLegacySecrets(state);
1940
+ } else {
1941
+ state.env = saveOpsState({
1942
+ ...state,
1943
+ env: {
1944
+ ...state.env,
1945
+ CALLER_PLATFORM_API_KEY: response.body.api_key,
1946
+ PLATFORM_API_KEY: response.body.api_key
1947
+ }
1948
+ });
1949
+ }
1950
+ state.env = saveOpsState(state);
1951
+ return response;
1952
+ }
1953
+
1954
+ function buildResponderRegisterHeaders() {
1955
+ const secrets = getResolvedSecrets(state, runtime);
1956
+ const apiKey = secrets.caller_api_key || secrets.responder_platform_api_key;
1957
+ if (!apiKey) {
1958
+ throw new Error("caller_platform_api_key_required");
1959
+ }
1960
+ return { Authorization: `Bearer ${apiKey}` };
1961
+ }
1962
+
1963
+ function buildPlatformReadHeaders() {
1964
+ const secrets = getResolvedSecrets(state, runtime);
1965
+ const apiKey = secrets.platform_admin_api_key || secrets.caller_api_key || secrets.responder_platform_api_key;
1966
+ return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
1967
+ }
1968
+
1969
+ async function syncResponderReviewStatusesFromPlatform() {
1970
+ if (!platformFeaturesEnabled(state)) {
1971
+ return false;
1972
+ }
1973
+ const hotlines = state.config.responder.hotlines || [];
1974
+ const submitted = hotlines.filter((item) => item.submitted_for_review === true);
1975
+ if (submitted.length === 0) {
1976
+ return false;
1977
+ }
1978
+ const headers = buildPlatformReadHeaders();
1979
+ let changed = false;
1980
+ for (const item of submitted) {
1981
+ let response;
1982
+ try {
1983
+ response = await requestJson(
1984
+ state.config.platform.base_url,
1985
+ `/v1/catalog/hotlines/${encodeURIComponent(item.hotline_id)}`,
1986
+ { headers }
1987
+ );
1988
+ } catch {
1989
+ continue;
1990
+ }
1991
+ if (response.status !== 200 || !response.body) {
1992
+ continue;
1993
+ }
1994
+ const nextReviewStatus = response.body.review_status || item.review_status || "pending";
1995
+ if (item.review_status !== nextReviewStatus) {
1996
+ item.review_status = nextReviewStatus;
1997
+ changed = true;
1998
+ }
1999
+ if (item.submitted_for_review !== true) {
2000
+ item.submitted_for_review = true;
2001
+ changed = true;
2002
+ }
2003
+ }
2004
+ if (changed) {
2005
+ state.env = saveOpsState(state);
2006
+ }
2007
+ return changed;
2008
+ }
2009
+
2010
+ async function verifyRegisteredHotline({ hotlineId, expectedTemplateRef }) {
2011
+ let detail;
2012
+ let bundle;
2013
+ try {
2014
+ detail = await requestJson(
2015
+ state.config.platform.base_url,
2016
+ `/v1/catalog/hotlines/${encodeURIComponent(hotlineId)}`,
2017
+ {
2018
+ headers: buildResponderRegisterHeaders()
2019
+ }
2020
+ );
2021
+ } catch (error) {
2022
+ return {
2023
+ ok: false,
2024
+ catalog_visible: false,
2025
+ template_ref_matches: false,
2026
+ template_bundle_available: false,
2027
+ catalog_status: null,
2028
+ template_bundle_status: null,
2029
+ error: error instanceof Error ? error.message : "catalog_verification_failed"
2030
+ };
2031
+ }
2032
+
2033
+ const actualTemplateRef = detail.body?.template_ref || null;
2034
+ const templateRefMatches = Boolean(detail.status === 200 && actualTemplateRef && actualTemplateRef === expectedTemplateRef);
2035
+ if (detail.status === 200 && actualTemplateRef) {
2036
+ try {
2037
+ bundle = await requestJson(
2038
+ state.config.platform.base_url,
2039
+ `/v1/catalog/hotlines/${encodeURIComponent(hotlineId)}/template-bundle?template_ref=${encodeURIComponent(actualTemplateRef)}`,
2040
+ {
2041
+ headers: buildResponderRegisterHeaders()
2042
+ }
2043
+ );
2044
+ } catch (error) {
2045
+ bundle = {
2046
+ status: null,
2047
+ body: {
2048
+ ok: false,
2049
+ error: error instanceof Error ? error.message : "template_bundle_verification_failed"
2050
+ }
2051
+ };
2052
+ }
2053
+ }
2054
+
2055
+ const templateBundleAvailable = Boolean(bundle?.status === 200);
2056
+ return {
2057
+ ok: Boolean(detail.status === 200 && templateRefMatches && templateBundleAvailable),
2058
+ catalog_visible: detail.status === 200,
2059
+ template_ref_matches: templateRefMatches,
2060
+ template_bundle_available: templateBundleAvailable,
2061
+ catalog_status: detail.status,
2062
+ template_bundle_status: bundle?.status ?? null,
2063
+ template_ref: actualTemplateRef || expectedTemplateRef || null
2064
+ };
2065
+ }
2066
+
2067
+ async function submitPendingResponderReviews({ hotlineId = null } = {}) {
2068
+ if (!platformFeaturesEnabled(state)) {
2069
+ return {
2070
+ status: 409,
2071
+ body: buildStructuredError(
2072
+ "PLATFORM_FEATURES_DISABLED",
2073
+ "platform publishing is disabled; enable platform features before submitting hotline reviews"
2074
+ )
2075
+ };
2076
+ }
2077
+ const responderIdentity = ensureResponderIdentity(state);
2078
+ const pending = (state.config.responder.hotlines || []).filter(
2079
+ (item) => item.submitted_for_review !== true && (!hotlineId || item.hotline_id === hotlineId)
2080
+ );
2081
+ const results = [];
2082
+ for (const item of pending) {
2083
+ let onboarding;
2084
+ try {
2085
+ onboarding = buildHotlineOnboardingBody(state, item, responderIdentity);
2086
+ } catch (error) {
2087
+ return {
2088
+ status: 400,
2089
+ body: buildStructuredError(
2090
+ error?.code || "HOTLINE_DRAFT_INVALID",
2091
+ error instanceof Error ? error.message : "hotline registration draft is invalid",
2092
+ { fields: Array.isArray(error?.fields) ? error.fields : [] }
2093
+ )
2094
+ };
2095
+ }
2096
+ const response = await requestJson(state.config.platform.base_url, "/v2/hotlines", {
2097
+ method: "POST",
2098
+ headers: buildResponderRegisterHeaders(),
2099
+ body: onboarding.body
2100
+ });
2101
+ if (response.status !== 201) {
2102
+ return response;
2103
+ }
2104
+ if (hasEncryptedSecretStore()) {
2105
+ writeOpsSecrets(runtime.auth.passphrase, {
2106
+ [OPS_SECRET_KEYS.responder_platform_api_key]: response.body.responder_api_key || response.body.api_key
2107
+ });
2108
+ runtime.auth.unlockedSecrets = unlockOpsSecrets(runtime.auth.passphrase);
2109
+ scrubLegacySecrets(state);
2110
+ } else {
2111
+ state.env = saveOpsState({
2112
+ ...state,
2113
+ env: {
2114
+ ...state.env,
2115
+ RESPONDER_PLATFORM_API_KEY: response.body.responder_api_key || response.body.api_key
2116
+ }
2117
+ });
2118
+ }
2119
+ item.submitted_for_review = true;
2120
+ item.review_status = response.body.hotline_review_status || response.body.review_status || "pending";
2121
+ const verification = await verifyRegisteredHotline({
2122
+ hotlineId: item.hotline_id,
2123
+ expectedTemplateRef: onboarding.body.template_ref
2124
+ });
2125
+ results.push({
2126
+ ...response.body,
2127
+ draft_file: onboarding.draft_file,
2128
+ used_draft: onboarding.used_draft,
2129
+ verification
2130
+ });
2131
+ }
2132
+ saveOpsState(state);
2133
+ return { status: 201, body: { ok: true, responder_id: responderIdentity.responder_id, submitted: results.length, results } };
2134
+ }
2135
+
2136
+ async function addOfficialExampleHotline() {
2137
+ const existing = findConfiguredExampleHotline(state);
2138
+ const definition = buildExampleHotlineDefinition(existing);
2139
+ if (isExampleHotlineDefinitionStale(existing)) {
2140
+ definition.submitted_for_review = false;
2141
+ definition.review_status = "local_only";
2142
+ }
2143
+ const registrationDraft = ensureHotlineRegistrationDraft(state, definition);
2144
+ upsertHotline(state, definition);
2145
+ state.env = saveOpsState(state);
2146
+ await reloadResponderIfRunning();
2147
+ appendSupervisorEvent({
2148
+ type: "hotline_upserted",
2149
+ hotline_id: definition.hotline_id,
2150
+ adapter_type: definition.adapter_type,
2151
+ example: true
2152
+ });
2153
+ return {
2154
+ ...definition,
2155
+ local_integration_file: registrationDraft.integration_file,
2156
+ local_hook_file: registrationDraft.hook_file,
2157
+ registration_draft_file: registrationDraft.draft_file,
2158
+ registration_draft: registrationDraft.draft
2159
+ };
2160
+ }
2161
+
2162
+ async function ensureOfficialExampleHotlineCurrent() {
2163
+ const existing = findConfiguredExampleHotline(state);
2164
+ if (!existing) {
2165
+ return null;
2166
+ }
2167
+ const current = buildExampleHotlineDefinition(existing);
2168
+ const stale = isExampleHotlineDefinitionStale(existing);
2169
+ if (!stale) {
2170
+ return existing;
2171
+ }
2172
+ current.submitted_for_review = false;
2173
+ current.review_status = "local_only";
2174
+ const registrationDraft = ensureHotlineRegistrationDraft(state, current);
2175
+ upsertHotline(state, current);
2176
+ state.env = saveOpsState(state);
2177
+ await reloadResponderIfRunning();
2178
+ appendSupervisorEvent({
2179
+ type: "hotline_upserted",
2180
+ hotline_id: current.hotline_id,
2181
+ adapter_type: current.adapter_type,
2182
+ example: true,
2183
+ upgraded: true,
2184
+ registration_draft_file: registrationDraft.draft_file
2185
+ });
2186
+ return current;
2187
+ }
2188
+
2189
+ async function dispatchExampleRequest(body = {}) {
2190
+ await ensureBaseServices();
2191
+ const callerRegistered =
2192
+ state.config.caller.registration_mode === "local_only" || Boolean(getResolvedSecrets(state, runtime).caller_api_key);
2193
+ if (!callerRegistered) {
2194
+ return {
2195
+ status: 409,
2196
+ body: buildStructuredError("CALLER_NOT_REGISTERED", "caller must be registered before running the local example", {
2197
+ stage: "register_caller"
2198
+ })
2199
+ };
2200
+ }
2201
+ if (state.config.responder.enabled !== true) {
2202
+ return {
2203
+ status: 409,
2204
+ body: buildStructuredError("RESPONDER_NOT_ENABLED", "responder must be enabled before running the local example", {
2205
+ stage: "enable_responder"
2206
+ })
2207
+ };
2208
+ }
2209
+
2210
+ const example = await ensureOfficialExampleHotlineCurrent();
2211
+ if (!example) {
2212
+ return buildExampleVisibilityError(example);
2213
+ }
2214
+ const responderIdentity = ensureResponderIdentity(state);
2215
+ let signerPublicKeyPem = responderIdentity.public_key_pem;
2216
+
2217
+ const diagnostics = await buildExampleDiagnostics();
2218
+ const requestBody = buildExampleRequestBody({
2219
+ text: body.text,
2220
+ responderId: state.config.responder.responder_id,
2221
+ hotlineId: LOCAL_EXAMPLE_HOTLINE_ID,
2222
+ signerPublicKeyPem,
2223
+ diagnostics
2224
+ });
2225
+ let response;
2226
+ const created = await requestJson(processBaseUrl(state.config.runtime.ports.caller), "/controller/requests", {
2227
+ method: "POST",
2228
+ body: requestBody
2229
+ });
2230
+ if (created.status !== 201 || !created.body?.request_id) {
2231
+ response = created;
2232
+ } else {
2233
+ await requestJson(
2234
+ processBaseUrl(state.config.runtime.ports.caller),
2235
+ `/controller/requests/${encodeURIComponent(created.body.request_id)}/contract-draft`,
2236
+ {
2237
+ method: "POST",
2238
+ body: {}
2239
+ }
2240
+ );
2241
+ const dispatched = await requestJson(
2242
+ processBaseUrl(state.config.runtime.ports.caller),
2243
+ `/controller/requests/${encodeURIComponent(created.body.request_id)}/dispatch`,
2244
+ {
2245
+ method: "POST",
2246
+ body: {
2247
+ thread_id: LOCAL_EXAMPLE_HOTLINE_ID,
2248
+ payload: requestBody.payload,
2249
+ task_input: requestBody.input
2250
+ }
2251
+ }
2252
+ );
2253
+ response = {
2254
+ status: dispatched.status === 202 ? 201 : dispatched.status,
2255
+ body: {
2256
+ request_id: created.body.request_id,
2257
+ request: dispatched.body?.request || created.body,
2258
+ accepted: dispatched.body?.accepted === true,
2259
+ delivery_meta: null,
2260
+ task_token: null
2261
+ }
2262
+ };
2263
+ }
2264
+ const draft = loadHotlineRegistrationDraft(state, example);
2265
+ return {
2266
+ status: response.status,
2267
+ body: {
2268
+ ...(response.body || {}),
2269
+ hotline_id: LOCAL_EXAMPLE_HOTLINE_ID,
2270
+ draft_file: draft.draft_file,
2271
+ draft_ready: Boolean(draft.draft)
2272
+ }
2273
+ };
2274
+ }
2275
+
2276
+ const server = http.createServer(async (req, res) => {
2277
+ const method = req.method || "GET";
2278
+ const url = new URL(req.url || "/", "http://localhost");
2279
+ const pathname = url.pathname;
2280
+
2281
+ try {
2282
+ if (method === "OPTIONS") {
2283
+ sendJson(res, 204, {});
2284
+ return;
2285
+ }
2286
+
2287
+ if (isProtectedRoute(method, pathname)) {
2288
+ const session = requireAuthenticatedSession(req, res, runtime, state);
2289
+ if (!session.ok) {
2290
+ return;
2291
+ }
2292
+ }
2293
+
2294
+ if (method === "GET" && pathname === "/healthz") {
2295
+ sendJson(res, 200, { ok: true, service: "ops-supervisor" });
2296
+ return;
2297
+ }
2298
+ if (method === "GET" && pathname === "/auth/session") {
2299
+ const recoverableSession = getRecoverableSession(runtime);
2300
+ if (recoverableSession) {
2301
+ persistActiveSession(recoverableSession);
2302
+ }
2303
+ sendJson(res, 200, {
2304
+ ok: true,
2305
+ session: getAuthState(runtime, state),
2306
+ recoverable_session: recoverableSession
2307
+ });
2308
+ return;
2309
+ }
2310
+ if (method === "POST" && pathname === "/auth/session/setup") {
2311
+ const body = await parseJsonBody(req);
2312
+ const passphrase = normalizedString(body.passphrase);
2313
+ if (!passphrase || passphrase.length < 8) {
2314
+ sendError(res, 400, "AUTH_INVALID_PASSPHRASE", "passphrase must be at least 8 characters");
2315
+ return;
2316
+ }
2317
+ if (hasEncryptedSecretStore()) {
2318
+ sendError(res, 409, "AUTH_SECRET_STORE_EXISTS", "encrypted secret store already exists");
2319
+ return;
2320
+ }
2321
+ const legacySecrets = Object.fromEntries(
2322
+ Object.entries(readResolvedOpsSecrets(state))
2323
+ .flatMap(([key, value]) => {
2324
+ if (key === "transport") {
2325
+ return [
2326
+ [OPS_SECRET_KEYS.transport_emailengine_access_token, value.emailengine.access_token],
2327
+ [OPS_SECRET_KEYS.transport_gmail_client_secret, value.gmail.client_secret],
2328
+ [OPS_SECRET_KEYS.transport_gmail_refresh_token, value.gmail.refresh_token]
2329
+ ];
2330
+ }
2331
+ return [[key, value]];
2332
+ })
2333
+ .filter(([, value]) => normalizedString(value))
2334
+ );
2335
+ initializeSecretStore(state.secretsFile, passphrase, legacySecrets);
2336
+ runtime.auth.unlockedSecrets = unlockOpsSecrets(passphrase);
2337
+ runtime.auth.passphrase = passphrase;
2338
+ runtime.auth.unlockedAt = nowIso();
2339
+ state.config.caller.api_key_configured = Boolean(runtime.auth.unlockedSecrets[OPS_SECRET_KEYS.caller_api_key]);
2340
+ scrubLegacySecrets(state);
2341
+ state.env = saveOpsState(state);
2342
+ const session = createAuthenticatedSession(runtime, passphrase, runtime.auth.unlockedSecrets);
2343
+ appendSupervisorEvent({ type: "auth_session_setup" });
2344
+ sendJson(res, 201, {
2345
+ ok: true,
2346
+ token: session.token,
2347
+ expires_at: session.expires_at,
2348
+ session: getAuthState(runtime, state)
2349
+ });
2350
+ return;
2351
+ }
2352
+ if (method === "POST" && pathname === "/auth/session/login") {
2353
+ const body = await parseJsonBody(req);
2354
+ const passphrase = normalizedString(body.passphrase);
2355
+ if (!passphrase) {
2356
+ sendError(res, 400, "AUTH_INVALID_PASSPHRASE", "passphrase is required");
2357
+ return;
2358
+ }
2359
+ if (!hasEncryptedSecretStore()) {
2360
+ sendError(res, 409, "AUTH_SECRET_STORE_MISSING", "encrypted secret store is not initialized yet");
2361
+ return;
2362
+ }
2363
+ try {
2364
+ const secrets = unlockOpsSecrets(passphrase);
2365
+ const session = createAuthenticatedSession(runtime, passphrase, secrets);
2366
+ appendSupervisorEvent({ type: "auth_session_login" });
2367
+ for (const svc of ["caller", "skill-adapter"]) {
2368
+ const existing = runtime.processes.get(svc);
2369
+ if (existing && !existing.exited) {
2370
+ existing.child.kill();
2371
+ const deadline = Date.now() + 3000;
2372
+ while (!existing.exited && Date.now() < deadline) {
2373
+ await new Promise((r) => setTimeout(r, 100));
2374
+ }
2375
+ }
2376
+ await ensureService(svc);
2377
+ }
2378
+ appendSupervisorEvent({ type: "services_restarted_after_login", services: ["caller", "skill-adapter"] });
2379
+ sendJson(res, 200, {
2380
+ ok: true,
2381
+ token: session.token,
2382
+ expires_at: session.expires_at,
2383
+ session: getAuthState(runtime, state)
2384
+ });
2385
+ } catch (error) {
2386
+ sendError(res, 401, "AUTH_INVALID_PASSPHRASE", error instanceof Error ? error.message : "secret_unlock_failed");
2387
+ }
2388
+ return;
2389
+ }
2390
+ if (method === "POST" && pathname === "/auth/session/logout") {
2391
+ const token = readSessionToken(req);
2392
+ if (token) {
2393
+ runtime.auth.sessions.delete(token);
2394
+ } else {
2395
+ runtime.auth.sessions.clear();
2396
+ }
2397
+ pruneExpiredSessions(runtime);
2398
+ if (runtime.auth.sessions.size === 0) {
2399
+ clearActiveSession();
2400
+ }
2401
+ appendSupervisorEvent({ type: "auth_session_logout" });
2402
+ sendJson(res, 200, {
2403
+ ok: true,
2404
+ session: getAuthState(runtime, state)
2405
+ });
2406
+ return;
2407
+ }
2408
+ if (method === "POST" && pathname === "/auth/session/change-passphrase") {
2409
+ if (!hasEncryptedSecretStore()) {
2410
+ sendError(res, 409, "AUTH_SECRET_STORE_MISSING", "encrypted secret store is not initialized yet");
2411
+ return;
2412
+ }
2413
+ const body = await parseJsonBody(req);
2414
+ const nextPassphrase = normalizedString(body.next_passphrase);
2415
+ if (!nextPassphrase || nextPassphrase.length < 8) {
2416
+ sendError(res, 400, "AUTH_INVALID_PASSPHRASE", "next_passphrase must be at least 8 characters");
2417
+ return;
2418
+ }
2419
+ const currentPassphrase = runtime.auth.passphrase || normalizedString(body.current_passphrase);
2420
+ if (!currentPassphrase) {
2421
+ sendError(res, 400, "AUTH_INVALID_PASSPHRASE", "current passphrase is required");
2422
+ return;
2423
+ }
2424
+ try {
2425
+ rotateSecretStorePassphrase(state.secretsFile, currentPassphrase, nextPassphrase);
2426
+ const secrets = unlockOpsSecrets(nextPassphrase);
2427
+ runtime.auth.passphrase = nextPassphrase;
2428
+ runtime.auth.unlockedSecrets = secrets;
2429
+ runtime.auth.unlockedAt = nowIso();
2430
+ appendSupervisorEvent({ type: "auth_passphrase_rotated" });
2431
+ sendJson(res, 200, {
2432
+ ok: true,
2433
+ session: getAuthState(runtime, state)
2434
+ });
2435
+ } catch (error) {
2436
+ sendError(res, 401, "AUTH_INVALID_PASSPHRASE", error instanceof Error ? error.message : "passphrase_rotation_failed");
2437
+ }
2438
+ return;
2439
+ }
2440
+ if (method === "GET" && pathname === "/status") {
2441
+ sendJson(res, 200, await buildStatus());
2442
+ return;
2443
+ }
2444
+ if (method === "GET" && pathname === "/platform/settings") {
2445
+ sendJson(res, 200, {
2446
+ enabled: platformFeaturesEnabled(state),
2447
+ base_url: state.config.platform?.base_url || null
2448
+ });
2449
+ return;
2450
+ }
2451
+ if (method === "PUT" && pathname === "/platform/settings") {
2452
+ const body = await parseJsonBody(req);
2453
+ state.config.platform ||= {};
2454
+ if (typeof body.enabled === "boolean") {
2455
+ state.config.platform.enabled = body.enabled;
2456
+ }
2457
+ if (normalizedString(body.base_url)) {
2458
+ state.config.platform.base_url = normalizedString(body.base_url);
2459
+ state.config.platform_console ||= {};
2460
+ state.config.platform_console.base_url = state.config.platform.base_url;
2461
+ }
2462
+ state.env = saveOpsState(state);
2463
+ for (const svc of ["caller", "skill-adapter"]) {
2464
+ const existing = runtime.processes.get(svc);
2465
+ if (existing && !existing.exited) {
2466
+ existing.child.kill();
2467
+ const deadline = Date.now() + 3000;
2468
+ while (!existing.exited && Date.now() < deadline) {
2469
+ await new Promise((r) => setTimeout(r, 100));
2470
+ }
2471
+ }
2472
+ await ensureService(svc);
2473
+ }
2474
+ appendSupervisorEvent({
2475
+ type: "platform_settings_updated",
2476
+ enabled: platformFeaturesEnabled(state),
2477
+ base_url: state.config.platform.base_url
2478
+ });
2479
+ sendJson(res, 200, {
2480
+ ok: true,
2481
+ enabled: platformFeaturesEnabled(state),
2482
+ base_url: state.config.platform.base_url
2483
+ });
2484
+ return;
2485
+ }
2486
+ if (method === "GET" && pathname === "/runtime/transport") {
2487
+ sendJson(res, 200, getTransportResponse(state, runtime));
2488
+ return;
2489
+ }
2490
+ if (method === "PUT" && pathname === "/runtime/transport") {
2491
+ const body = await parseJsonBody(req);
2492
+ const nextTransport = normalizeTransportPayload(body);
2493
+ const validation = validateTransportConfig(nextTransport);
2494
+ if (validation) {
2495
+ sendJson(res, validation.status, validation.body);
2496
+ return;
2497
+ }
2498
+ state.config.runtime ||= {};
2499
+ state.config.runtime.transport = nextTransport;
2500
+ const secretUpdates = buildTransportSecretUpdates(body);
2501
+ if (hasEncryptedSecretStore()) {
2502
+ if (Object.keys(secretUpdates).length > 0) {
2503
+ writeOpsSecrets(runtime.auth.passphrase, secretUpdates);
2504
+ runtime.auth.unlockedSecrets = unlockOpsSecrets(runtime.auth.passphrase);
2505
+ }
2506
+ scrubLegacySecrets(state);
2507
+ } else if (Object.keys(secretUpdates).length > 0) {
2508
+ state.env = {
2509
+ ...state.env,
2510
+ ...buildLegacyTransportSecretEnv(secretUpdates)
2511
+ };
2512
+ }
2513
+ // Clear after scrubLegacySecrets (which re-reads disk) so saveOpsState picks up config.type
2514
+ if (state.env) state.env = { ...state.env, TRANSPORT_TYPE: null };
2515
+ state.env = saveOpsState(state);
2516
+ appendSupervisorEvent({
2517
+ type: "transport_updated",
2518
+ transport_type: nextTransport.type,
2519
+ provider: nextTransport.type === "email" ? nextTransport.email.provider : null
2520
+ });
2521
+ sendJson(res, 200, getTransportResponse(state, runtime));
2522
+ return;
2523
+ }
2524
+ if (method === "POST" && pathname === "/runtime/transport/test") {
2525
+ const validation = validateTransportConfig(getRuntimeTransport(state));
2526
+ if (validation) {
2527
+ sendJson(res, validation.status, validation.body);
2528
+ return;
2529
+ }
2530
+ const result = await testTransportConnection(state, runtime);
2531
+ sendJson(res, result.ok ? 200 : result.status || 502, result);
2532
+ return;
2533
+ }
2534
+ if (method === "POST" && pathname === "/setup") {
2535
+ refreshStateFromDisk();
2536
+ ensureResponderIdentity(state);
2537
+ state.env = saveOpsState(state);
2538
+ for (const svc of ["caller", "skill-adapter", "mcp-adapter", "responder"]) {
2539
+ const existing = runtime.processes.get(svc);
2540
+ if (existing && !existing.exited) {
2541
+ await stopProcessInfo(existing);
2542
+ await ensureService(svc);
2543
+ }
2544
+ }
2545
+ appendSupervisorEvent({ type: "setup_completed" });
2546
+ sendJson(res, 200, { ok: true, config: state.config });
2547
+ return;
2548
+ }
2549
+ if (method === "POST" && pathname === "/auth/register-caller") {
2550
+ const body = await parseJsonBody(req);
2551
+ const registered = await registerCaller(body.contact_email, {
2552
+ localOnly: normalizedString(body.mode) === "local_only",
2553
+ forcePlatform: normalizedString(body.mode) === "platform"
2554
+ });
2555
+ appendSupervisorEvent({
2556
+ type: "caller_registered",
2557
+ ok: registered.status === 201,
2558
+ contact_email: body.contact_email || null
2559
+ });
2560
+ if (registered.status === 201) {
2561
+ for (const svc of ["caller", "skill-adapter"]) {
2562
+ const existing = runtime.processes.get(svc);
2563
+ if (existing && !existing.exited) {
2564
+ await stopProcessInfo(existing);
2565
+ }
2566
+ await ensureService(svc);
2567
+ }
2568
+ appendSupervisorEvent({ type: "services_restarted_after_registration", services: ["caller", "skill-adapter"] });
2569
+ }
2570
+ sendJson(res, registered.status, registered.body);
2571
+ return;
2572
+ }
2573
+ if (method === "GET" && pathname === "/catalog/hotlines") {
2574
+ const localItems = listLocalCatalogHotlines(state, runtime, {
2575
+ hotline_id: url.searchParams.get("hotline_id") || undefined,
2576
+ responder_id: url.searchParams.get("responder_id") || undefined,
2577
+ task_type: url.searchParams.get("task_type") || undefined,
2578
+ capability: url.searchParams.get("capability") || undefined
2579
+ });
2580
+ if (!platformFeaturesEnabled(state)) {
2581
+ sendJson(res, 200, { items: localItems.filter(isCallableCatalogItem) });
2582
+ return;
2583
+ }
2584
+ const response = await requestJson(
2585
+ processBaseUrl(state.config.runtime.ports.caller),
2586
+ `/controller/hotlines${url.search}`
2587
+ , {
2588
+ headers: buildPlatformHeaders(state, runtime)
2589
+ });
2590
+ if (response.status >= 200 && response.status < 300) {
2591
+ sendJson(res, response.status, {
2592
+ ...(response.body || {}),
2593
+ items: mergeCatalogItems(response.body?.items || [], localItems)
2594
+ });
2595
+ return;
2596
+ }
2597
+ const callableLocalItems = localItems.filter(isCallableCatalogItem);
2598
+ if (callableLocalItems.length > 0) {
2599
+ sendJson(res, 200, { items: callableLocalItems });
2600
+ return;
2601
+ }
2602
+ sendJson(res, response.status, response.body);
2603
+ return;
2604
+ }
2605
+ const catalogDetailMatch = pathname.match(/^\/catalog\/hotlines\/([^/]+)$/);
2606
+ if (method === "GET" && catalogDetailMatch) {
2607
+ const hotlineId = decodeURIComponent(catalogDetailMatch[1]);
2608
+ const localItem = listLocalCatalogHotlines(state, runtime, { hotline_id: hotlineId })[0] || null;
2609
+ if (localItem && isCallableCatalogItem(localItem)) {
2610
+ sendJson(res, 200, localItem);
2611
+ return;
2612
+ }
2613
+ if (!platformFeaturesEnabled(state)) {
2614
+ sendError(res, 404, "HOTLINE_NOT_FOUND", "hotline is not configured locally");
2615
+ return;
2616
+ }
2617
+ const response = await requestJson(
2618
+ state.config.platform.base_url,
2619
+ `/v1/catalog/hotlines/${encodeURIComponent(hotlineId)}`,
2620
+ {
2621
+ headers: buildPlatformReadHeaders()
2622
+ }
2623
+ );
2624
+ if (response.status >= 200 && response.status < 300 && isPlatformBootstrapDemoCatalogItem(response.body)) {
2625
+ sendError(res, 404, "HOTLINE_NOT_FOUND", "hotline is not available in the local caller catalog");
2626
+ return;
2627
+ }
2628
+ sendJson(res, response.status, response.body);
2629
+ return;
2630
+ }
2631
+
2632
+ // ------------------------------------------------------------------
2633
+ // /caller/approvals proxy → Skill Adapter
2634
+ // Allows the Ops Console to read and action pending approval records
2635
+ // ------------------------------------------------------------------
2636
+ if (pathname.startsWith("/caller/approvals")) {
2637
+ const skillAdapterBase = processBaseUrl(state.config.runtime.ports.skill_adapter);
2638
+ const upstreamPath = `/skills/remote-hotline/approvals${pathname.slice("/caller/approvals".length)}${url.search}`;
2639
+ const body = ["POST", "PUT", "PATCH"].includes(method) ? await parseJsonBody(req) : undefined;
2640
+ const response = await requestJson(skillAdapterBase, upstreamPath, {
2641
+ method,
2642
+ body
2643
+ });
2644
+ sendJson(res, response.status, response.body);
2645
+ return;
2646
+ }
2647
+
2648
+ if (pathname === "/caller/global-policy") {
2649
+ if (method === "GET") {
2650
+ sendJson(res, 200, ensureCallerPolicyState(state));
2651
+ return;
2652
+ }
2653
+ if (["PUT", "PATCH"].includes(method)) {
2654
+ const body = await parseJsonBody(req);
2655
+ const current = ensureCallerPolicyState(state);
2656
+ const next = {
2657
+ ...current,
2658
+ ...(method === "PATCH" ? body : {}),
2659
+ ...(method === "PUT" ? {
2660
+ mode: normalizedString(body.mode) || "manual",
2661
+ responderWhitelist: Array.isArray(body.responderWhitelist) ? body.responderWhitelist.map((item) => String(item)) : [],
2662
+ hotlineWhitelist: Array.isArray(body.hotlineWhitelist) ? body.hotlineWhitelist.map((item) => String(item)) : [],
2663
+ blocklist: Array.isArray(body.blocklist) ? body.blocklist.map((item) => String(item)) : [],
2664
+ } : {}),
2665
+ };
2666
+ next.mode = ["manual", "allow_listed", "allow_all"].includes(next.mode) ? next.mode : "manual";
2667
+ next.responderWhitelist = Array.isArray(next.responderWhitelist) ? next.responderWhitelist.filter(Boolean) : [];
2668
+ next.hotlineWhitelist = Array.isArray(next.hotlineWhitelist) ? next.hotlineWhitelist.filter(Boolean) : [];
2669
+ next.blocklist = Array.isArray(next.blocklist) ? next.blocklist.filter(Boolean) : [];
2670
+ state.config.preferences.caller_policy = next;
2671
+ state.env = saveOpsState(state);
2672
+ sendJson(res, 200, next);
2673
+ return;
2674
+ }
2675
+ sendError(res, 405, "method_not_allowed", "method not allowed");
2676
+ return;
2677
+ }
2678
+ if (method === "POST" && pathname === "/calls/prepare") {
2679
+ const body = await parseJsonBody(req);
2680
+ const response = await prepareCallConfirmation(body);
2681
+ sendJson(res, response.status, response.body);
2682
+ return;
2683
+ }
2684
+ if (method === "POST" && pathname === "/calls/confirm") {
2685
+ const body = await parseJsonBody(req);
2686
+ const response = await confirmPreparedCall(body);
2687
+ sendJson(res, response.status, response.body);
2688
+ return;
2689
+ }
2690
+ const preferenceMatch = pathname.match(/^\/preferences\/task-types\/([^/]+)\/hotline$/);
2691
+ if (preferenceMatch && method === "PUT") {
2692
+ const body = await parseJsonBody(req);
2693
+ const preference = setTaskTypePreference(state, decodeURIComponent(preferenceMatch[1]), {
2694
+ hotline_id: normalizedString(body.hotline_id),
2695
+ responder_id: normalizedString(body.responder_id)
2696
+ });
2697
+ state.env = saveOpsState(state);
2698
+ sendJson(res, 200, {
2699
+ ok: true,
2700
+ preference
2701
+ });
2702
+ return;
2703
+ }
2704
+ if (method === "GET" && pathname === "/preferences/task-types") {
2705
+ sendJson(res, 200, { items: Object.values(ensurePreferenceState(state)) });
2706
+ return;
2707
+ }
2708
+ if (method === "GET" && pathname === "/requests") {
2709
+ const response = await requestJson(processBaseUrl(state.config.runtime.ports.caller), "/controller/requests");
2710
+ sendJson(res, response.status, response.body);
2711
+ return;
2712
+ }
2713
+ const requestMatch = pathname.match(/^\/requests\/([^/]+)$/);
2714
+ if (method === "GET" && requestMatch) {
2715
+ const response = await requestJson(processBaseUrl(state.config.runtime.ports.caller), `/controller/requests/${requestMatch[1]}`);
2716
+ sendJson(res, response.status, response.body);
2717
+ return;
2718
+ }
2719
+ const requestResultMatch = pathname.match(/^\/requests\/([^/]+)\/result$/);
2720
+ if (method === "GET" && requestResultMatch) {
2721
+ const response = await requestJson(processBaseUrl(state.config.runtime.ports.caller), `/controller/requests/${requestResultMatch[1]}/result`);
2722
+ sendJson(res, response.status, response.body);
2723
+ return;
2724
+ }
2725
+ if (method === "POST" && pathname === "/requests") {
2726
+ const body = await parseJsonBody(req);
2727
+ const response = await requestJson(processBaseUrl(state.config.runtime.ports.caller), "/controller/remote-requests", {
2728
+ method: "POST",
2729
+ headers: buildPlatformHeaders(state, runtime),
2730
+ body
2731
+ });
2732
+ sendJson(res, response.status, response.body);
2733
+ return;
2734
+ }
2735
+ if (method === "POST" && pathname === "/requests/example") {
2736
+ const body = await parseJsonBody(req);
2737
+ const response = await dispatchExampleRequest(body);
2738
+ sendJson(res, response.status, response.body);
2739
+ return;
2740
+ }
2741
+ if (method === "GET" && pathname === "/responder") {
2742
+ await syncResponderReviewStatusesFromPlatform();
2743
+ sendJson(res, 200, {
2744
+ enabled: state.config.responder.enabled,
2745
+ responder_id: state.config.responder.responder_id,
2746
+ display_name: state.config.responder.display_name,
2747
+ platform_enabled: platformFeaturesEnabled(state),
2748
+ hotline_count: (state.config.responder.hotlines || []).length,
2749
+ hotlines: (state.config.responder.hotlines || []).map((item) => serializeHotlineForUi(state, runtime, item))
2750
+ });
2751
+ return;
2752
+ }
2753
+ if (method === "GET" && pathname === "/responder/hotlines") {
2754
+ await syncResponderReviewStatusesFromPlatform();
2755
+ sendJson(res, 200, {
2756
+ platform_enabled: platformFeaturesEnabled(state),
2757
+ items: (state.config.responder.hotlines || []).map((item) => serializeHotlineForUi(state, runtime, item))
2758
+ });
2759
+ return;
2760
+ }
2761
+ const hotlineDraftMatch = pathname.match(/^\/responder\/hotlines\/([^/]+)\/draft$/);
2762
+ if (method === "GET" && hotlineDraftMatch) {
2763
+ const hotlineId = decodeURIComponent(hotlineDraftMatch[1]);
2764
+ const hotline = (state.config.responder.hotlines || []).find((item) => item.hotline_id === hotlineId);
2765
+ if (!hotline) {
2766
+ sendError(res, 404, "hotline_not_found", "no hotline found with this id", { hotline_id: hotlineId });
2767
+ return;
2768
+ }
2769
+ await syncResponderReviewStatusesFromPlatform();
2770
+ const registrationDraft = loadHotlineRegistrationDraft(state, hotline);
2771
+ sendJson(res, 200, {
2772
+ ok: Boolean(registrationDraft.draft),
2773
+ hotline_id: hotline.hotline_id,
2774
+ platform_enabled: platformFeaturesEnabled(state),
2775
+ review_status: hotline.review_status || "local_only",
2776
+ submitted_for_review: hotline.submitted_for_review === true,
2777
+ draft_file: registrationDraft.draft_file,
2778
+ local_integration_file: hotline?.metadata?.local?.integration_file || null,
2779
+ local_hook_file: hotline?.metadata?.local?.hook_file || null,
2780
+ draft_ready: Boolean(registrationDraft.draft_file),
2781
+ runtime: buildResponderRuntimeStatus(state, runtime, hotline.hotline_id),
2782
+ draft: registrationDraft.draft
2783
+ });
2784
+ return;
2785
+ }
2786
+ if (method === "POST" && pathname === "/responder/hotlines/example") {
2787
+ const definition = await addOfficialExampleHotline();
2788
+ sendJson(res, 201, {
2789
+ ...definition,
2790
+ example: true,
2791
+ message: `${LOCAL_EXAMPLE_DISPLAY_NAME} is configured locally`
2792
+ });
2793
+ return;
2794
+ }
2795
+ if (method === "POST" && pathname === "/responder/hotlines") {
2796
+ const body = await parseJsonBody(req);
2797
+ const definition = {
2798
+ hotline_id: body.hotline_id,
2799
+ display_name: body.display_name || body.hotline_id,
2800
+ enabled: body.enabled !== false,
2801
+ task_types: body.task_types || [],
2802
+ capabilities: body.capabilities || [],
2803
+ tags: body.tags || [],
2804
+ adapter_type: body.adapter_type || "process",
2805
+ adapter: body.adapter || {},
2806
+ metadata: body.metadata || null,
2807
+ timeouts: body.timeouts || { soft_timeout_s: 60, hard_timeout_s: 180 },
2808
+ review_status: "local_only",
2809
+ submitted_for_review: false
2810
+ };
2811
+ const registrationDraft = ensureHotlineRegistrationDraft(state, definition);
2812
+ upsertHotline(state, definition);
2813
+ state.env = saveOpsState(state);
2814
+ await reloadResponderIfRunning();
2815
+ appendSupervisorEvent({
2816
+ type: "hotline_upserted",
2817
+ hotline_id: definition.hotline_id,
2818
+ adapter_type: definition.adapter_type
2819
+ });
2820
+ sendJson(res, 201, {
2821
+ ...definition,
2822
+ local_integration_file: registrationDraft.integration_file,
2823
+ local_hook_file: registrationDraft.hook_file,
2824
+ registration_draft_file: registrationDraft.draft_file,
2825
+ registration_draft: registrationDraft.draft,
2826
+ runtime: buildResponderRuntimeStatus(state, runtime, definition.hotline_id)
2827
+ });
2828
+ return;
2829
+ }
2830
+ const hotlineToggleMatch = pathname.match(/^\/responder\/hotlines\/([^/]+)\/(enable|disable)$/);
2831
+ if (method === "POST" && hotlineToggleMatch) {
2832
+ const hotlineId = decodeURIComponent(hotlineToggleMatch[1]);
2833
+ const enabled = hotlineToggleMatch[2] === "enable";
2834
+ const item = setHotlineEnabled(state, hotlineId, enabled);
2835
+ if (!item) {
2836
+ sendError(res, 404, "hotline_not_found", "no hotline found with this id", { hotline_id: hotlineId });
2837
+ return;
2838
+ }
2839
+ state.env = saveOpsState(state);
2840
+ await reloadResponderIfRunning();
2841
+ appendSupervisorEvent({
2842
+ type: "hotline_toggled",
2843
+ hotline_id: item.hotline_id,
2844
+ enabled: item.enabled !== false
2845
+ });
2846
+ sendJson(res, 200, {
2847
+ ok: true,
2848
+ hotline_id: item.hotline_id,
2849
+ enabled: item.enabled !== false,
2850
+ review_status: item.review_status || "local_only",
2851
+ submitted_for_review: item.submitted_for_review === true,
2852
+ runtime: buildResponderRuntimeStatus(state, runtime, item.hotline_id)
2853
+ });
2854
+ return;
2855
+ }
2856
+ const hotlineDeleteMatch = pathname.match(/^\/responder\/hotlines\/([^/]+)$/);
2857
+ if (method === "DELETE" && hotlineDeleteMatch) {
2858
+ const hotlineId = decodeURIComponent(hotlineDeleteMatch[1]);
2859
+ const removed = removeHotline(state, hotlineId);
2860
+ if (!removed) {
2861
+ sendError(res, 404, "hotline_not_found", "no hotline found with this id", { hotline_id: hotlineId });
2862
+ return;
2863
+ }
2864
+ state.env = saveOpsState(state);
2865
+ await reloadResponderIfRunning();
2866
+ appendSupervisorEvent({
2867
+ type: "hotline_removed",
2868
+ hotline_id: removed.hotline_id
2869
+ });
2870
+ sendJson(res, 200, {
2871
+ ok: true,
2872
+ removed: {
2873
+ hotline_id: removed.hotline_id,
2874
+ review_status: removed.review_status || "local_only"
2875
+ },
2876
+ runtime: buildResponderRuntimeStatus(state, runtime, removed.hotline_id)
2877
+ });
2878
+ return;
2879
+ }
2880
+ const hotlineSubmitDraftMatch = pathname.match(/^\/responder\/hotlines\/([^/]+)\/submit-review$/);
2881
+ if (method === "POST" && hotlineSubmitDraftMatch) {
2882
+ const hotlineId = decodeURIComponent(hotlineSubmitDraftMatch[1]);
2883
+ const hotline = (state.config.responder.hotlines || []).find((item) => item.hotline_id === hotlineId);
2884
+ if (!hotline) {
2885
+ sendError(res, 404, "hotline_not_found", "no hotline found with this id", { hotline_id: hotlineId });
2886
+ return;
2887
+ }
2888
+ ensureResponderIdentity(state);
2889
+ state.env = saveOpsState(state);
2890
+ const submitted = await submitPendingResponderReviews({ hotlineId });
2891
+ await reloadResponderIfRunning();
2892
+ appendSupervisorEvent({
2893
+ type: "responder_review_submitted",
2894
+ responder_id: state.config.responder.responder_id,
2895
+ hotline_id: hotlineId,
2896
+ submitted: submitted.body?.submitted || 0,
2897
+ ok: submitted.status === 201
2898
+ });
2899
+ sendJson(res, submitted.status, submitted.body);
2900
+ return;
2901
+ }
2902
+ if (method === "POST" && pathname === "/responder/enable") {
2903
+ const body = await parseJsonBody(req);
2904
+ ensureResponderIdentity(state, {
2905
+ responderId: body.responder_id || state.config.responder.responder_id || null,
2906
+ displayName: body.display_name || state.config.responder.display_name || null
2907
+ });
2908
+ state.config.responder.enabled = true;
2909
+ if (body.hotline_id) {
2910
+ const definition = {
2911
+ hotline_id: body.hotline_id,
2912
+ display_name: body.display_name || body.hotline_id,
2913
+ enabled: true,
2914
+ task_types: body.task_types || [],
2915
+ capabilities: body.capabilities || [],
2916
+ tags: body.tags || [],
2917
+ adapter_type: body.adapter_type || "process",
2918
+ adapter: body.adapter || { cmd: body.cmd || "" },
2919
+ timeouts: body.timeouts || { soft_timeout_s: 60, hard_timeout_s: 180 },
2920
+ review_status: "local_only",
2921
+ submitted_for_review: false
2922
+ };
2923
+ ensureHotlineRegistrationDraft(state, definition);
2924
+ upsertHotline(state, definition);
2925
+ }
2926
+ state.env = saveOpsState(state);
2927
+ await ensureService("responder");
2928
+ appendSupervisorEvent({
2929
+ type: "responder_enabled",
2930
+ responder_id: state.config.responder.responder_id
2931
+ });
2932
+ sendJson(res, 200, {
2933
+ ok: true,
2934
+ responder: state.config.responder,
2935
+ submitted: 0,
2936
+ review: null
2937
+ });
2938
+ return;
2939
+ }
2940
+ if (method === "POST" && pathname === "/responder/submit-review") {
2941
+ const body = await parseJsonBody(req);
2942
+ ensureResponderIdentity(state, {
2943
+ responderId: body.responder_id || state.config.responder.responder_id || null,
2944
+ displayName: body.display_name || state.config.responder.display_name || null
2945
+ });
2946
+ state.env = saveOpsState(state);
2947
+ const submitted = await submitPendingResponderReviews({
2948
+ hotlineId: normalizedString(body.hotline_id) || null
2949
+ });
2950
+ await reloadResponderIfRunning();
2951
+ appendSupervisorEvent({
2952
+ type: "responder_review_submitted",
2953
+ responder_id: state.config.responder.responder_id,
2954
+ hotline_id: normalizedString(body.hotline_id) || null,
2955
+ submitted: submitted.body?.submitted || 0,
2956
+ ok: submitted.status === 201
2957
+ });
2958
+ sendJson(res, submitted.status, submitted.body);
2959
+ return;
2960
+ }
2961
+ if (method === "GET" && pathname === "/runtime/logs") {
2962
+ const service = url.searchParams.get("service");
2963
+ if (!service) {
2964
+ sendError(res, 400, "service_required", "service query parameter is required");
2965
+ return;
2966
+ }
2967
+ const maxLines = Number(url.searchParams.get("max_lines") || 200);
2968
+ sendJson(res, 200, {
2969
+ service,
2970
+ file: getServiceLogFile(service),
2971
+ logs: readServiceLogTail(service, { maxLines })
2972
+ });
2973
+ return;
2974
+ }
2975
+ if (method === "DELETE" && pathname === "/runtime/logs") {
2976
+ const service = url.searchParams.get("service");
2977
+ if (!service) {
2978
+ sendError(res, 400, "service_required", "service query parameter is required");
2979
+ return;
2980
+ }
2981
+ const logFile = getServiceLogFile(service);
2982
+ if (fs.existsSync(logFile)) fs.writeFileSync(logFile, "", "utf8");
2983
+ sendJson(res, 200, { ok: true });
2984
+ return;
2985
+ }
2986
+ if (method === "GET" && pathname === "/runtime/alerts") {
2987
+ const service = url.searchParams.get("service");
2988
+ if (!service) {
2989
+ sendError(res, 400, "service_required", "service query parameter is required");
2990
+ return;
2991
+ }
2992
+ const maxItems = Number(url.searchParams.get("max_items") || 20);
2993
+ sendJson(res, 200, {
2994
+ service,
2995
+ alerts: buildRuntimeAlerts(service, { maxItems })
2996
+ });
2997
+ return;
2998
+ }
2999
+ if (method === "DELETE" && pathname === "/runtime/alerts") {
3000
+ const eventsFile = getSupervisorEventsFile();
3001
+ if (fs.existsSync(eventsFile)) fs.writeFileSync(eventsFile, "", "utf8");
3002
+ sendJson(res, 200, { ok: true });
3003
+ return;
3004
+ }
3005
+ if (method === "GET" && pathname === "/debug/snapshot") {
3006
+ const status = await buildStatus();
3007
+ sendJson(res, 200, {
3008
+ ok: true,
3009
+ generated_at: nowIso(),
3010
+ status,
3011
+ recent_events: readSupervisorEventTail({ maxLines: 50 }),
3012
+ log_tail: {
3013
+ relay: readServiceLogTail("relay", { maxLines: 50 }),
3014
+ caller: readServiceLogTail("caller", { maxLines: 50 }),
3015
+ responder: readServiceLogTail("responder", { maxLines: 50 })
3016
+ }
3017
+ });
3018
+ return;
3019
+ }
3020
+ if (method === "GET" && pathname === "/mcp-adapter/spec") {
3021
+ sendJson(res, 200, {
3022
+ ok: true,
3023
+ spec: buildMcpAdapterSpec()
3024
+ });
3025
+ return;
3026
+ }
3027
+
3028
+ const serviceRestartMatch = pathname.match(/^\/runtime\/services\/([^/]+)\/restart$/);
3029
+ if (method === "POST" && serviceRestartMatch) {
3030
+ const name = serviceRestartMatch[1];
3031
+ if (!["caller", "responder", "relay", "skill-adapter", "mcp-adapter"].includes(name)) {
3032
+ sendError(res, 400, "invalid_service", "service must be caller, responder, relay, skill-adapter, or mcp-adapter");
3033
+ return;
3034
+ }
3035
+ const existing = runtime.processes.get(name);
3036
+ if (existing && !existing.exited) {
3037
+ await stopProcessInfo(existing);
3038
+ }
3039
+ await ensureService(name);
3040
+ appendSupervisorEvent({ type: "service_restarted", service: name });
3041
+ sendJson(res, 200, { ok: true, service: name });
3042
+ return;
3043
+ }
3044
+
3045
+ sendError(res, 404, "not_found", "no matching route", { path: pathname });
3046
+ } catch (error) {
3047
+ if (error instanceof Error && error.message === "invalid_json") {
3048
+ sendError(res, 400, "invalid_json", "request body is not valid JSON");
3049
+ return;
3050
+ }
3051
+ sendError(res, 500, "ops_supervisor_internal_error", error instanceof Error ? error.message : "unknown_error", { retryable: true });
3052
+ }
3053
+ });
3054
+
3055
+ server.startManagedServices = async () => {
3056
+ ensureResponderIdentity(state);
3057
+ state.env = saveOpsState(state);
3058
+ await ensureBaseServices();
3059
+ appendSupervisorEvent({ type: "managed_services_started" });
3060
+ };
3061
+
3062
+ server.stopManagedServices = async () => {
3063
+ for (const processInfo of runtime.processes.values()) {
3064
+ await stopProcessInfo(processInfo);
3065
+ }
3066
+ appendSupervisorEvent({ type: "managed_services_stopped" });
3067
+ };
3068
+
3069
+ return server;
3070
+ }