u-foo 3.0.18 → 3.0.19

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.
@@ -22,6 +22,8 @@ const WAIT_FOR_MESSAGE_DEFAULT_TIMEOUT_SECONDS = 600;
22
22
  const WAIT_FOR_MESSAGE_MAX_TIMEOUT_SECONDS = 600;
23
23
  const WAIT_FOR_MESSAGE_POLL_INTERVAL_MS = 1000;
24
24
  const WAIT_FOR_MESSAGE_HEARTBEAT_INTERVAL_MS = 15000;
25
+ const MCP_AGENT_LEASE_TTL_MS = 24 * 60 * 60 * 1000;
26
+ const MCP_AGENT_RECENT_HEARTBEAT_MS = 30 * 1000;
25
27
 
26
28
  function nowIso() {
27
29
  return new Date().toISOString();
@@ -70,6 +72,61 @@ function createCryptoSessionId() {
70
72
  return crypto.randomBytes(4).toString("hex");
71
73
  }
72
74
 
75
+ function createAgentHandle() {
76
+ return crypto.randomBytes(32).toString("base64url");
77
+ }
78
+
79
+ function hashAgentHandle(handle = "") {
80
+ return crypto.createHash("sha256").update(String(handle || ""), "utf8").digest("hex");
81
+ }
82
+
83
+ function leaseExpiryIso(nowMs = Date.now()) {
84
+ return new Date(nowMs + MCP_AGENT_LEASE_TTL_MS).toISOString();
85
+ }
86
+
87
+ function extendMcpAgentLease(meta, nowMs = Date.now()) {
88
+ meta.mcp_lease_expires_at = leaseExpiryIso(nowMs);
89
+ delete meta.mcp_revoked_at;
90
+ return meta.mcp_lease_expires_at;
91
+ }
92
+
93
+ function assertAgentHandle(bus, subscriber, args = {}, options = {}) {
94
+ const meta = assertSubscriberExists(bus, subscriber);
95
+ if (meta.mcp_bridge !== true || !meta.mcp_agent_handle_hash) {
96
+ const err = new Error(`subscriber is not an MCP-registered Agent: ${subscriber}`);
97
+ err.code = "agent_handle_not_available";
98
+ throw err;
99
+ }
100
+ const handle = String(args.agent_handle || args.agentHandle || "").trim();
101
+ if (!handle) {
102
+ const err = new Error("agent_handle is required");
103
+ err.code = "agent_handle_required";
104
+ throw err;
105
+ }
106
+ const actual = Buffer.from(hashAgentHandle(handle), "hex");
107
+ const expected = Buffer.from(String(meta.mcp_agent_handle_hash || ""), "hex");
108
+ if (actual.length !== expected.length || !crypto.timingSafeEqual(actual, expected)) {
109
+ const err = new Error("agent_handle does not own this subscriber");
110
+ err.code = "invalid_agent_handle";
111
+ throw err;
112
+ }
113
+ if (options.allowInactive !== true && meta.status !== "active") {
114
+ const err = new Error(`Agent registration is inactive: ${subscriber}`);
115
+ err.code = "agent_lease_inactive";
116
+ throw err;
117
+ }
118
+ const expiresAtMs = Date.parse(String(meta.mcp_lease_expires_at || ""));
119
+ if (
120
+ options.allowExpired !== true
121
+ && (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now())
122
+ ) {
123
+ const err = new Error(`Agent lease expired: ${subscriber}`);
124
+ err.code = "agent_lease_expired";
125
+ throw err;
126
+ }
127
+ return meta;
128
+ }
129
+
73
130
  function notifyDaemonRefresh(projectRoot) {
74
131
  if (!isRunning(projectRoot)) return;
75
132
  const sock = socketPath(projectRoot);
@@ -99,6 +156,10 @@ async function registerAgentFull(projectRoot, args = {}, options = {}) {
99
156
  const hostCapabilities = args.hostCapabilities && typeof args.hostCapabilities === "object"
100
157
  ? args.hostCapabilities
101
158
  : capabilities;
159
+ const clientInstanceId = String(
160
+ args.client_instance_id || args.clientInstanceId || ""
161
+ ).trim();
162
+ const bus = ensureBusLoaded(projectRoot);
102
163
 
103
164
  // Session ID: explicit > reuse > generate
104
165
  let sessionId;
@@ -113,7 +174,22 @@ async function registerAgentFull(projectRoot, args = {}, options = {}) {
113
174
  const reuseProviderSessionId = typeof reuseSession?.providerSessionId === "string"
114
175
  ? reuseSession.providerSessionId.trim() : "";
115
176
 
116
- if (explicitSessionId) {
177
+ const recoveredEntry = !validateParentPid && clientInstanceId
178
+ ? Object.entries(bus.busData.agents || {}).find(([, meta]) => (
179
+ meta
180
+ && meta.mcp_bridge === true
181
+ && meta.agent_type === agentType
182
+ && meta.mcp_client_instance_id === clientInstanceId
183
+ ))
184
+ : null;
185
+ const recoveredSubscriber = recoveredEntry ? recoveredEntry[0] : "";
186
+ const recoveredSessionId = recoveredSubscriber.startsWith(`${agentType}:`)
187
+ ? recoveredSubscriber.slice(agentType.length + 1)
188
+ : "";
189
+
190
+ if (recoveredSessionId) {
191
+ sessionId = recoveredSessionId;
192
+ } else if (explicitSessionId) {
117
193
  sessionId = explicitSessionId;
118
194
  } else if (reuseSessionId && reuseSubscriberId === `${agentType}:${reuseSessionId}`) {
119
195
  sessionId = reuseSessionId;
@@ -165,7 +241,18 @@ async function registerAgentFull(projectRoot, args = {}, options = {}) {
165
241
  if (reuseSessionId) joinOptions.reuseSessionId = reuseSessionId;
166
242
  if (reuseProviderSessionId) joinOptions.reuseProviderSessionId = reuseProviderSessionId;
167
243
 
168
- const bus = ensureBusLoaded(projectRoot);
244
+ const candidateSubscriber = `${agentType}:${sessionId}`;
245
+ const existingCandidate = bus.subscriberManager.getSubscriber(candidateSubscriber);
246
+ if (
247
+ !validateParentPid
248
+ && existingCandidate
249
+ && existingCandidate.mcp_agent_handle_hash
250
+ && (!clientInstanceId || existingCandidate.mcp_client_instance_id !== clientInstanceId)
251
+ ) {
252
+ const err = new Error(`subscriber is already registered: ${candidateSubscriber}`);
253
+ err.code = "subscriber_already_registered";
254
+ throw err;
255
+ }
169
256
  const result = await bus.subscriberManager.join(sessionId, agentType, finalNickname, joinOptions);
170
257
  const subscriber = result.subscriber;
171
258
  if (finalNickname) {
@@ -175,6 +262,14 @@ async function registerAgentFull(projectRoot, args = {}, options = {}) {
175
262
  meta.activity_state = String(args.activity_state || "ready");
176
263
  meta.activity_since = nowIso();
177
264
  meta.mcp_bridge = !validateParentPid;
265
+ let agentHandle = "";
266
+ if (!validateParentPid) {
267
+ agentHandle = createAgentHandle();
268
+ meta.mcp_agent_handle_hash = hashAgentHandle(agentHandle);
269
+ meta.mcp_client_instance_id = clientInstanceId;
270
+ meta.mcp_registered_at = meta.mcp_registered_at || nowIso();
271
+ extendMcpAgentLease(meta);
272
+ }
178
273
  if (hostCapabilities) meta.mcp_capabilities = hostCapabilities;
179
274
  bus.saveBusData();
180
275
  notifyDaemonRefresh(projectRoot);
@@ -188,6 +283,12 @@ async function registerAgentFull(projectRoot, args = {}, options = {}) {
188
283
  nickname: meta.nickname || result.nickname || finalNickname || "",
189
284
  scoped_nickname: meta.scoped_nickname || result.scopedNickname || scopedNickname || "",
190
285
  launch_mode: launchMode,
286
+ ...(agentHandle ? {
287
+ agent_handle: agentHandle,
288
+ lease_expires_at: meta.mcp_lease_expires_at,
289
+ client_instance_id: clientInstanceId,
290
+ recovered: Boolean(recoveredSubscriber),
291
+ } : {}),
191
292
  reuseProviderSessionId,
192
293
  skipSessionResolve: !!args.skipSessionResolve,
193
294
  };
@@ -203,9 +304,10 @@ async function registerAgent(projectRoot, args = {}) {
203
304
  async function heartbeatAgent(projectRoot, args = {}) {
204
305
  const subscriber = resolveSubscriberArg(args);
205
306
  const bus = ensureBusLoaded(projectRoot);
206
- const meta = assertSubscriberExists(bus, subscriber);
307
+ const meta = assertAgentHandle(bus, subscriber, args);
207
308
  bus.subscriberManager.updateLastSeen(subscriber);
208
309
  meta.status = "active";
310
+ const leaseExpiresAt = extendMcpAgentLease(meta);
209
311
  bus.saveBusData();
210
312
  notifyDaemonRefresh(projectRoot);
211
313
  return {
@@ -213,6 +315,7 @@ async function heartbeatAgent(projectRoot, args = {}) {
213
315
  project_root: projectRoot,
214
316
  subscriber,
215
317
  last_seen: meta.last_seen,
318
+ lease_expires_at: leaseExpiresAt,
216
319
  };
217
320
  }
218
321
 
@@ -225,7 +328,7 @@ async function publishActivityState(projectRoot, args = {}) {
225
328
  throw err;
226
329
  }
227
330
  const bus = ensureBusLoaded(projectRoot);
228
- const meta = assertSubscriberExists(bus, subscriber);
331
+ const meta = assertAgentHandle(bus, subscriber, args);
229
332
  bus.subscriberManager.updateLastSeen(subscriber);
230
333
  meta.status = "active";
231
334
  meta.activity_state = activityState;
@@ -246,7 +349,7 @@ async function publishActivityState(projectRoot, args = {}) {
246
349
  async function updateAgentMetadata(projectRoot, args = {}) {
247
350
  const subscriber = resolveSubscriberArg(args);
248
351
  const bus = ensureBusLoaded(projectRoot);
249
- const meta = assertSubscriberExists(bus, subscriber);
352
+ const meta = assertAgentHandle(bus, subscriber, args);
250
353
  const nickname = String(args.nickname || "").trim();
251
354
  if (nickname) {
252
355
  await bus.subscriberManager.rename(subscriber, nickname);
@@ -278,7 +381,7 @@ async function pollInbox(projectRoot, args = {}) {
278
381
  ? Math.floor(Number(args.limit))
279
382
  : 50;
280
383
  const bus = ensureBusLoaded(projectRoot);
281
- assertSubscriberExists(bus, subscriber);
384
+ assertAgentHandle(bus, subscriber, args);
282
385
  bus.subscriberManager.updateLastSeen(subscriber);
283
386
  bus.saveBusData();
284
387
  const pending = await bus.messageManager.check(subscriber);
@@ -365,14 +468,15 @@ function eventSeq(event = {}) {
365
468
  return Number.isInteger(seq) && seq > 0 ? seq : 0;
366
469
  }
367
470
 
368
- function touchWaitingSubscriber(bus, subscriber) {
471
+ function touchWaitingSubscriber(bus, subscriber, args = {}) {
369
472
  // Long waits span concurrent metadata updates from other agents. Reload
370
473
  // before writing heartbeat state so a stale in-memory registry cannot
371
474
  // overwrite those updates.
372
475
  bus.loadBusData();
373
- const meta = assertSubscriberExists(bus, subscriber);
476
+ const meta = assertAgentHandle(bus, subscriber, args);
374
477
  meta.status = "active";
375
478
  bus.subscriberManager.updateLastSeen(subscriber);
479
+ extendMcpAgentLease(meta);
376
480
  bus.saveBusData();
377
481
  }
378
482
 
@@ -393,7 +497,7 @@ async function waitForMessage(projectRoot, args = {}, options = {}) {
393
497
  const timeoutMs = timeoutSeconds * 1000;
394
498
 
395
499
  const bus = ensureBusLoaded(projectRoot);
396
- touchWaitingSubscriber(bus, subscriber);
500
+ touchWaitingSubscriber(bus, subscriber, args);
397
501
  const lease = acquirePollLease(path.join(
398
502
  bus.busDir,
399
503
  "pids",
@@ -418,7 +522,7 @@ async function waitForMessage(projectRoot, args = {}, options = {}) {
418
522
  if (unseen.length > 0) {
419
523
  const messages = unseen.slice(0, limit);
420
524
  const lastSeq = Math.max(afterSeq, ...messages.map(eventSeq));
421
- touchWaitingSubscriber(bus, subscriber);
525
+ touchWaitingSubscriber(bus, subscriber, args);
422
526
  return {
423
527
  ok: true,
424
528
  project_root: projectRoot,
@@ -436,7 +540,7 @@ async function waitForMessage(projectRoot, args = {}, options = {}) {
436
540
 
437
541
  const current = now();
438
542
  if (current >= deadline) {
439
- touchWaitingSubscriber(bus, subscriber);
543
+ touchWaitingSubscriber(bus, subscriber, args);
440
544
  return {
441
545
  ok: true,
442
546
  project_root: projectRoot,
@@ -453,7 +557,7 @@ async function waitForMessage(projectRoot, args = {}, options = {}) {
453
557
  }
454
558
 
455
559
  if (current >= nextHeartbeatAt) {
456
- touchWaitingSubscriber(bus, subscriber);
560
+ touchWaitingSubscriber(bus, subscriber, args);
457
561
  nextHeartbeatAt = current + heartbeatIntervalMs;
458
562
  }
459
563
 
@@ -469,6 +573,8 @@ async function waitForMessage(projectRoot, args = {}, options = {}) {
469
573
 
470
574
  async function reportAgentStatus(projectRoot, args = {}) {
471
575
  const subscriber = resolveSubscriberArg(args);
576
+ const bus = ensureBusLoaded(projectRoot);
577
+ assertAgentHandle(bus, subscriber, args);
472
578
  const report = normalizeReportInput({
473
579
  ...args,
474
580
  agent_id: subscriber,
@@ -488,6 +594,12 @@ async function reportAgentStatus(projectRoot, args = {}) {
488
594
  async function unregisterAgent(projectRoot, args = {}) {
489
595
  const subscriber = resolveSubscriberArg(args);
490
596
  const bus = ensureBusLoaded(projectRoot);
597
+ const meta = assertAgentHandle(bus, subscriber, args, {
598
+ allowExpired: true,
599
+ allowInactive: true,
600
+ });
601
+ meta.mcp_revoked_at = nowIso();
602
+ meta.mcp_lease_expires_at = meta.mcp_revoked_at;
491
603
  const ok = await bus.subscriberManager.leave(subscriber);
492
604
  bus.saveBusData();
493
605
  notifyDaemonRefresh(projectRoot);
@@ -504,6 +616,10 @@ module.exports = {
504
616
  assertSubscriberExists,
505
617
  resolveSubscriberArg,
506
618
  createSessionId,
619
+ createAgentHandle,
620
+ hashAgentHandle,
621
+ assertAgentHandle,
622
+ extendMcpAgentLease,
507
623
  notifyDaemonRefresh,
508
624
  registerAgentFull,
509
625
  registerAgent,
@@ -517,4 +633,6 @@ module.exports = {
517
633
  normalizeWaitForMessageArgs,
518
634
  WAIT_FOR_MESSAGE_DEFAULT_TIMEOUT_SECONDS,
519
635
  WAIT_FOR_MESSAGE_MAX_TIMEOUT_SECONDS,
636
+ MCP_AGENT_LEASE_TTL_MS,
637
+ MCP_AGENT_RECENT_HEARTBEAT_MS,
520
638
  };
@@ -45,6 +45,10 @@ const {
45
45
  checkAndCleanupNickname,
46
46
  } = require("./nicknameScope");
47
47
  const { resolveNodeExecutable } = require("../process/nodeExecutable");
48
+ const {
49
+ createProjectRuntimeControlPlane,
50
+ } = require("./projectRuntimeControlPlane");
51
+ const { loadConfig, normalizeMcpPort } = require("../../config");
48
52
 
49
53
  let providerSessions = null;
50
54
  let sessionResolveHandles = new Map();
@@ -1351,9 +1355,63 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
1351
1355
  },
1352
1356
  });
1353
1357
  deliveryScheduler.start();
1358
+ const runtimeControlPlane = createProjectRuntimeControlPlane({ projectRoot });
1359
+ let mcpHttpServer = null;
1360
+ if (isGlobalControllerProjectRoot(projectRoot) && process.env.UFOO_MCP_HTTP_DISABLED !== "1") {
1361
+ const { createGlobalMcpHttpServer } = require("./mcpHttpServer");
1362
+ const config = loadConfig(projectRoot);
1363
+ const configuredPort = process.env.UFOO_MCP_PORT || config.mcpPort;
1364
+ mcpHttpServer = createGlobalMcpHttpServer({
1365
+ projectRoot,
1366
+ port: normalizeMcpPort(configuredPort),
1367
+ log,
1368
+ });
1369
+ mcpHttpServer.start().catch((err) => {
1370
+ logSync(`MCP HTTP startup failed: ${formatFatalReason(err)}`);
1371
+ setImmediate(() => {
1372
+ throw err;
1373
+ });
1374
+ });
1375
+ }
1354
1376
 
1355
1377
  handleIpcRequest = async (req, socket) => {
1356
1378
  if (!req || typeof req !== "object") return;
1379
+ if (await runtimeControlPlane.handleRequest(req, socket)) return;
1380
+ if (req.type === IPC_REQUEST_TYPES.MCP_STATUS || req.type === IPC_REQUEST_TYPES.MCP_RESTART) {
1381
+ if (!isGlobalControllerProjectRoot(projectRoot)) {
1382
+ socket.write(`${JSON.stringify({
1383
+ type: IPC_RESPONSE_TYPES.ERROR,
1384
+ error: "MCP control is owned by the global controller daemon",
1385
+ })}\n`);
1386
+ return;
1387
+ }
1388
+ try {
1389
+ if (req.type === IPC_REQUEST_TYPES.MCP_RESTART) {
1390
+ if (!mcpHttpServer) {
1391
+ throw new Error("MCP HTTP listener is disabled");
1392
+ }
1393
+ await mcpHttpServer.stop();
1394
+ await mcpHttpServer.start();
1395
+ }
1396
+ socket.write(`${JSON.stringify({
1397
+ type: IPC_RESPONSE_TYPES.RESPONSE,
1398
+ data: {
1399
+ ok: true,
1400
+ operation: req.type === IPC_REQUEST_TYPES.MCP_RESTART ? "restart" : "status",
1401
+ mcp: mcpHttpServer
1402
+ ? mcpHttpServer.getStatus()
1403
+ : { running: false, disabled: true },
1404
+ },
1405
+ })}\n`);
1406
+ } catch (err) {
1407
+ socket.write(`${JSON.stringify({
1408
+ type: IPC_RESPONSE_TYPES.ERROR,
1409
+ error: err.message || String(err),
1410
+ code: err.code || "mcp_control_error",
1411
+ })}\n`);
1412
+ }
1413
+ return;
1414
+ }
1357
1415
  if (req.type === IPC_REQUEST_TYPES.STATUS) {
1358
1416
  cleanupInactiveSubscribers();
1359
1417
  const status = buildRuntimeStatus();
@@ -2595,6 +2653,13 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
2595
2653
  }
2596
2654
  daemonGroupOrchestrator = null;
2597
2655
 
2656
+ runtimeControlPlane.stop();
2657
+ if (mcpHttpServer) {
2658
+ void mcpHttpServer.stop().catch((err) => {
2659
+ writeLog(`MCP HTTP shutdown failed: ${formatFatalReason(err)}`);
2660
+ });
2661
+ }
2662
+
2598
2663
  // 清理所有子进程
2599
2664
  processManager.cleanup();
2600
2665
 
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const path = require("path");
6
+
7
+ const {
8
+ readConnectionFiles,
9
+ } = require("./mcpStdioProxy");
10
+ const {
11
+ resolveGlobalControllerProjectRoot,
12
+ } = require("../projects");
13
+
14
+ const MANAGED_BLOCK_START = "# >>> ufoo MCP (managed)";
15
+ const MANAGED_BLOCK_END = "# <<< ufoo MCP (managed)";
16
+
17
+ function tomlString(value = "") {
18
+ return JSON.stringify(String(value || ""));
19
+ }
20
+
21
+ function codexConfigPath(options = {}) {
22
+ if (options.configPath) return options.configPath;
23
+ const codexHome = String(options.codexHome || process.env.CODEX_HOME || "").trim();
24
+ return path.join(codexHome || path.join(os.homedir(), ".codex"), "config.toml");
25
+ }
26
+
27
+ function buildCodexManagedBlock(connection) {
28
+ return [
29
+ MANAGED_BLOCK_START,
30
+ "[mcp_servers.ufoo]",
31
+ `url = ${tomlString(connection.endpoint)}`,
32
+ `http_headers = { Authorization = ${tomlString(`Bearer ${connection.token}`)} }`,
33
+ "tool_timeout_sec = 610",
34
+ "enabled = true",
35
+ MANAGED_BLOCK_END,
36
+ ].join("\n");
37
+ }
38
+
39
+ function removeManagedBlock(text = "") {
40
+ const escapedStart = MANAGED_BLOCK_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
41
+ const escapedEnd = MANAGED_BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
42
+ return String(text || "").replace(
43
+ new RegExp(`(?:^|\\n)${escapedStart}\\n[\\s\\S]*?\\n${escapedEnd}(?=\\n|$)`, "g"),
44
+ ""
45
+ );
46
+ }
47
+
48
+ function findTomlSections(text = "") {
49
+ const sections = [];
50
+ const pattern = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/gm;
51
+ let match;
52
+ while ((match = pattern.exec(text)) !== null) {
53
+ sections.push({
54
+ header: match[1].trim(),
55
+ start: match.index,
56
+ contentStart: pattern.lastIndex,
57
+ });
58
+ }
59
+ return sections.map((section, index) => ({
60
+ ...section,
61
+ end: index + 1 < sections.length ? sections[index + 1].start : text.length,
62
+ }));
63
+ }
64
+
65
+ function isUfooMainSection(header = "") {
66
+ return /^(?:mcp_servers\.ufoo|mcp_servers\."ufoo")$/.test(String(header || ""));
67
+ }
68
+
69
+ function isUfooStdioEnvSection(header = "") {
70
+ return /^(?:mcp_servers\.ufoo|mcp_servers\."ufoo")\.env$/.test(String(header || ""));
71
+ }
72
+
73
+ function removeLegacyUfooTransportSections(text = "") {
74
+ const sections = findTomlSections(text);
75
+ const ranges = sections
76
+ .filter((section) => isUfooMainSection(section.header) || isUfooStdioEnvSection(section.header))
77
+ .map((section) => [section.start, section.end])
78
+ .sort((a, b) => b[0] - a[0]);
79
+ let next = text;
80
+ for (const [start, end] of ranges) {
81
+ next = `${next.slice(0, start)}${next.slice(end)}`;
82
+ }
83
+ return next;
84
+ }
85
+
86
+ function renderCodexConfig(existing, connection) {
87
+ const withoutManaged = removeManagedBlock(existing);
88
+ const withoutLegacy = removeLegacyUfooTransportSections(withoutManaged);
89
+ const trimmed = withoutLegacy.trimEnd();
90
+ return `${trimmed ? `${trimmed}\n\n` : ""}${buildCodexManagedBlock(connection)}\n`;
91
+ }
92
+
93
+ function configureCodexMcp(options = {}) {
94
+ const projectRoot = options.projectRoot || resolveGlobalControllerProjectRoot();
95
+ const connection = options.connection || readConnectionFiles(projectRoot);
96
+ const target = codexConfigPath(options);
97
+ const existing = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : "";
98
+ const next = renderCodexConfig(existing, connection);
99
+ if (options.dryRun === true) {
100
+ const redacted = renderCodexConfig(existing, {
101
+ ...connection,
102
+ token: "<redacted>",
103
+ });
104
+ return {
105
+ ok: true,
106
+ dry_run: true,
107
+ target,
108
+ transport: "streamable_http",
109
+ endpoint: connection.endpoint,
110
+ changed: next !== existing,
111
+ content: redacted,
112
+ };
113
+ }
114
+
115
+ fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
116
+ let backup = "";
117
+ if (fs.existsSync(target) && next !== existing) {
118
+ backup = `${target}.ufoo-backup-${Date.now()}`;
119
+ fs.copyFileSync(target, backup);
120
+ try {
121
+ fs.chmodSync(backup, 0o600);
122
+ } catch {
123
+ // Best effort for filesystems without POSIX modes.
124
+ }
125
+ }
126
+ fs.writeFileSync(target, next, { encoding: "utf8", mode: 0o600 });
127
+ try {
128
+ fs.chmodSync(target, 0o600);
129
+ } catch {
130
+ // Best effort for filesystems without POSIX modes.
131
+ }
132
+ return {
133
+ ok: true,
134
+ dry_run: false,
135
+ target,
136
+ backup: backup || null,
137
+ transport: "streamable_http",
138
+ endpoint: connection.endpoint,
139
+ changed: next !== existing,
140
+ };
141
+ }
142
+
143
+ function runMcpConfigureCli(host, options = {}) {
144
+ const normalized = String(host || "").trim().toLowerCase();
145
+ if (normalized !== "codex") {
146
+ const err = new Error(
147
+ `Direct HTTP auto-configuration is verified only for Codex App/CLI/IDE; keep ${normalized || "this host"} on the stateless "ufoo mcp" stdio proxy`
148
+ );
149
+ err.code = "unsupported_mcp_host_config";
150
+ throw err;
151
+ }
152
+ const result = configureCodexMcp(options);
153
+ if (options.dryRun === true) {
154
+ process.stdout.write(result.content);
155
+ } else {
156
+ process.stdout.write(`Configured Codex MCP at ${result.target}\n`);
157
+ process.stdout.write(`Transport: Streamable HTTP ${result.endpoint}\n`);
158
+ if (result.backup) process.stdout.write(`Backup: ${result.backup}\n`);
159
+ process.stdout.write("Restart Codex App/CLI/IDE to load the shared MCP configuration.\n");
160
+ }
161
+ return result;
162
+ }
163
+
164
+ module.exports = {
165
+ MANAGED_BLOCK_END,
166
+ MANAGED_BLOCK_START,
167
+ buildCodexManagedBlock,
168
+ codexConfigPath,
169
+ configureCodexMcp,
170
+ findTomlSections,
171
+ removeLegacyUfooTransportSections,
172
+ removeManagedBlock,
173
+ renderCodexConfig,
174
+ runMcpConfigureCli,
175
+ };
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+
3
+ const net = require("net");
4
+
5
+ const {
6
+ IPC_REQUEST_TYPES,
7
+ IPC_RESPONSE_TYPES,
8
+ } = require("../contracts/eventContract");
9
+ const {
10
+ isRunning,
11
+ socketPath,
12
+ } = require("./index");
13
+ const {
14
+ resolveGlobalControllerProjectRoot,
15
+ } = require("../projects");
16
+
17
+ function requestMcpControl(operation, options = {}) {
18
+ const projectRoot = options.projectRoot || resolveGlobalControllerProjectRoot();
19
+ const checkRunning = options.isRunning || isRunning;
20
+ const resolveSocketPath = options.socketPath || socketPath;
21
+ const connect = options.connect || ((target) => net.createConnection(target));
22
+ const requestType = operation === "restart"
23
+ ? IPC_REQUEST_TYPES.MCP_RESTART
24
+ : IPC_REQUEST_TYPES.MCP_STATUS;
25
+ if (!checkRunning(projectRoot)) {
26
+ const err = new Error("Global controller daemon is not running");
27
+ err.code = "global_daemon_not_running";
28
+ return Promise.reject(err);
29
+ }
30
+
31
+ return new Promise((resolve, reject) => {
32
+ const client = connect(resolveSocketPath(projectRoot));
33
+ let buffer = "";
34
+ let settled = false;
35
+ const timeoutMs = Number(options.timeoutMs) || 10000;
36
+ let timer = null;
37
+
38
+ const cleanup = () => {
39
+ if (timer) clearTimeout(timer);
40
+ timer = null;
41
+ client.removeAllListeners();
42
+ try {
43
+ client.end();
44
+ } catch {
45
+ // ignore
46
+ }
47
+ };
48
+ const finishResolve = (value) => {
49
+ if (settled) return;
50
+ settled = true;
51
+ cleanup();
52
+ resolve(value);
53
+ };
54
+ const finishReject = (err) => {
55
+ if (settled) return;
56
+ settled = true;
57
+ cleanup();
58
+ reject(err);
59
+ };
60
+
61
+ timer = setTimeout(() => {
62
+ finishReject(Object.assign(new Error("MCP control request timed out"), {
63
+ code: "mcp_control_timeout",
64
+ }));
65
+ }, timeoutMs);
66
+ if (typeof timer.unref === "function") timer.unref();
67
+
68
+ client.on("connect", () => {
69
+ client.write(`${JSON.stringify({ type: requestType })}\n`);
70
+ });
71
+ client.on("data", (chunk) => {
72
+ buffer += chunk.toString("utf8");
73
+ const lines = buffer.split(/\r?\n/);
74
+ buffer = lines.pop() || "";
75
+ for (const line of lines) {
76
+ if (!line.trim()) continue;
77
+ let response;
78
+ try {
79
+ response = JSON.parse(line);
80
+ } catch {
81
+ continue;
82
+ }
83
+ if (response.type === IPC_RESPONSE_TYPES.ERROR) {
84
+ const err = new Error(response.error || "MCP control failed");
85
+ err.code = response.code || "mcp_control_error";
86
+ finishReject(err);
87
+ return;
88
+ }
89
+ if (response.type === IPC_RESPONSE_TYPES.RESPONSE && response.data?.mcp) {
90
+ finishResolve(response.data);
91
+ return;
92
+ }
93
+ }
94
+ });
95
+ client.once("error", finishReject);
96
+ client.once("close", () => {
97
+ finishReject(Object.assign(new Error("Global controller closed the MCP control request"), {
98
+ code: "mcp_control_closed",
99
+ }));
100
+ });
101
+ });
102
+ }
103
+
104
+ async function runMcpControlCli(operation, options = {}) {
105
+ const result = await requestMcpControl(operation, options);
106
+ if (options.json === true) {
107
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
108
+ return result;
109
+ }
110
+ const status = result.mcp || {};
111
+ process.stdout.write(`MCP ${status.running ? "running" : "stopped"}\n`);
112
+ if (status.endpoint) process.stdout.write(`Endpoint: ${status.endpoint}\n`);
113
+ if (status.pid) process.stdout.write(`PID: ${status.pid}\n`);
114
+ process.stdout.write(`Sessions: ${status.session_count || 0}\n`);
115
+ process.stdout.write(`Active requests: ${status.active_request_count || 0}\n`);
116
+ process.stdout.write(`Active waits: ${status.active_wait_count || 0}\n`);
117
+ return result;
118
+ }
119
+
120
+ module.exports = {
121
+ requestMcpControl,
122
+ runMcpControlCli,
123
+ };