@minhspark/codex-mcp-bridge 1.13.8 → 1.15.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.
@@ -11,7 +11,11 @@ import {
11
11
  MAX_FRAME_BYTES,
12
12
  NATIVE_DISPATCH_METHOD,
13
13
  NativeToolsClient,
14
+ NativeRelayError,
14
15
  RELAY_PROTOCOL_VERSION,
16
+ ACCOUNT_RELAY_PROTOCOL_VERSION,
17
+ accountRelaySocketPath,
18
+ relayAccountContext,
15
19
  relaySocketPath,
16
20
  resolveRelayThreadId,
17
21
  desktopTaskSocketPath,
@@ -20,14 +24,32 @@ import {
20
24
  } from "./native-relay.mjs";
21
25
  import { IS_WINDOWS, PLATFORM_LABEL } from "./platform.mjs";
22
26
  import { exitForVersionRequest } from "./cli-version.mjs";
27
+ import { assertAccountIdentity } from "./bridge-account-context.mjs";
28
+ import { createReloadControl } from "./reload-control.mjs";
29
+ import { createRuntimeState } from "./runtime-state.mjs";
23
30
 
24
31
  exitForVersionRequest(import.meta.url);
25
32
 
26
- const VERSION = "1.13.8";
33
+ const VERSION = "1.15.0";
27
34
  const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
28
35
 
29
- function errorResponse(code, message) {
30
- return { ok: false, v: RELAY_PROTOCOL_VERSION, error: { code, message } };
36
+ function errorResponse(code, message, sent) {
37
+ return { ok: false, v: RELAY_PROTOCOL_VERSION, error: { code, message, ...(sent === false ? { sent: false } : {}) } };
38
+ }
39
+
40
+ function boundRequest(payload) {
41
+ return payload?.v === ACCOUNT_RELAY_PROTOCOL_VERSION;
42
+ }
43
+
44
+ function validProtocol(payload) {
45
+ return boundRequest(payload) ? Object.hasOwn(payload, "accountContext") : (payload?.v === undefined || payload.v === RELAY_PROTOCOL_VERSION) && !Object.hasOwn(payload, "accountContext");
46
+ }
47
+
48
+ async function checkAccountContext(payload, assertAccount) {
49
+ const accounts = relayAccountContext(payload.accountContext);
50
+ if (boundRequest(payload) && !accounts) throw new NativeRelayError("Protocol 2 requires the original account context", "RELAY_BAD_REQUEST");
51
+ if (accounts) await assertAccount(accounts);
52
+ return accounts;
31
53
  }
32
54
 
33
55
  /**
@@ -46,14 +68,13 @@ function errorCode(err) {
46
68
  */
47
69
  export async function handleRelayRequest(
48
70
  payload,
49
- { dispatch, dispatchDesktop, resolveExecutor = resolveRelayThreadId, env = process.env } = {},
71
+ { dispatch, dispatchDesktop, resolveExecutor = resolveRelayThreadId, env = process.env, assertAccount = assertAccountIdentity } = {},
50
72
  ) {
51
73
  if (payload && typeof payload === "object" && Object.hasOwn(payload, "operation")) {
52
- return handleDesktopRequest(payload, { dispatchDesktop, resolveExecutor, env });
74
+ return handleDesktopRequest(payload, { dispatchDesktop, resolveExecutor, env, assertAccount });
53
75
  }
54
76
  if (!payload || typeof payload !== "object" || Array.isArray(payload) ||
55
- Object.keys(payload).some((key) => !["v", "targetThreadId", "message"].includes(key)) ||
56
- (payload.v !== undefined && payload.v !== RELAY_PROTOCOL_VERSION)) {
77
+ Object.keys(payload).some((key) => !["v", "targetThreadId", "message", "accountContext"].includes(key)) || !validProtocol(payload)) {
57
78
  return errorResponse("RELAY_BAD_REQUEST", "expected a relay request with targetThreadId and message");
58
79
  }
59
80
  const targetThreadId = typeof payload?.targetThreadId === "string" ? payload.targetThreadId.trim() : "";
@@ -63,10 +84,12 @@ export async function handleRelayRequest(
63
84
  if (!message.trim()) return errorResponse("RELAY_BAD_REQUEST", "message must be a non-empty string");
64
85
 
65
86
  let executorThreadId;
87
+ let accountContext;
66
88
  try {
89
+ accountContext = await checkAccountContext(payload, assertAccount);
67
90
  executorThreadId = resolveExecutor(env).threadId;
68
91
  } catch (err) {
69
- return errorResponse(errorCode(err), err.message);
92
+ return errorResponse(errorCode(err), err.message, false);
70
93
  }
71
94
 
72
95
  /**
@@ -83,24 +106,30 @@ export async function handleRelayRequest(
83
106
  }
84
107
 
85
108
  try {
86
- const result = await dispatch({ executorThreadId, targetThreadId, message });
109
+ const result = await dispatch({ executorThreadId, targetThreadId, message }, { accountContext });
87
110
  if (result?.success !== true || result?.isError === true) {
88
111
  const detail = typeof result?.error === "string" ? result.error : result?.error?.message;
89
112
  return errorResponse("NATIVE_DISPATCH_FAILED", detail ?? "Codex Desktop did not confirm successful native dispatch");
90
113
  }
91
- return { ok: true, v: RELAY_PROTOCOL_VERSION, targetThreadId, executorThreadId, result: result ?? null };
114
+ return { ok: true, v: boundRequest(payload) ? ACCOUNT_RELAY_PROTOCOL_VERSION : RELAY_PROTOCOL_VERSION, targetThreadId, executorThreadId, result: result ?? null };
92
115
  } catch (err) {
93
- return errorResponse(errorCode(err), err?.message ?? String(err));
116
+ return errorResponse(errorCode(err), err?.message ?? String(err), err?.sent === false && err?.reachedCompanion !== true ? false : undefined);
94
117
  }
95
118
  }
96
119
 
97
- async function handleDesktopRequest(payload, { dispatchDesktop, resolveExecutor, env }) {
98
- if (Array.isArray(payload) || payload.v !== RELAY_PROTOCOL_VERSION ||
99
- Object.keys(payload).some((key) => !["v", "operation", "arguments"].includes(key))) {
120
+ async function handleDesktopRequest(payload, { dispatchDesktop, resolveExecutor, env, assertAccount }) {
121
+ if (Array.isArray(payload) || ![RELAY_PROTOCOL_VERSION, ACCOUNT_RELAY_PROTOCOL_VERSION].includes(payload.v) || !validProtocol(payload) ||
122
+ Object.keys(payload).some((key) => !["v", "operation", "arguments", "accountContext"].includes(key))) {
100
123
  return errorResponse("RELAY_BAD_REQUEST", "expected an allowlisted Desktop operation");
101
124
  }
102
125
  try {
103
126
  validateDesktopOperation(payload.operation, payload.arguments);
127
+ let accountContext;
128
+ try {
129
+ accountContext = await checkAccountContext(payload, assertAccount);
130
+ } catch (error) {
131
+ return errorResponse(errorCode(error), error.message, false);
132
+ }
104
133
  if (typeof dispatchDesktop !== "function") {
105
134
  return errorResponse("NATIVE_OPERATION_UNAVAILABLE", "This companion does not support Desktop operations; reload the native relay");
106
135
  }
@@ -112,16 +141,16 @@ async function handleDesktopRequest(payload, { dispatchDesktop, resolveExecutor,
112
141
  executorThreadId,
113
142
  operation: payload.operation,
114
143
  arguments: payload.arguments,
115
- });
144
+ }, { accountContext });
116
145
  return {
117
146
  ok: true,
118
- v: RELAY_PROTOCOL_VERSION,
147
+ v: boundRequest(payload) ? ACCOUNT_RELAY_PROTOCOL_VERSION : RELAY_PROTOCOL_VERSION,
119
148
  operation: payload.operation,
120
149
  executorThreadId,
121
150
  result: decodeNativeToolResult(nativeResult),
122
151
  };
123
152
  } catch (err) {
124
- return errorResponse(errorCode(err), err?.message ?? String(err));
153
+ return errorResponse(errorCode(err), err?.message ?? String(err), err?.sent === false && err?.reachedCompanion !== true ? false : undefined);
125
154
  }
126
155
  }
127
156
 
@@ -137,6 +166,8 @@ export class RelaySocketServer {
137
166
  dispatch,
138
167
  dispatchDesktop,
139
168
  resolveExecutor = resolveRelayThreadId,
169
+ assertAccount = assertAccountIdentity,
170
+ requireAccountContext = false,
140
171
  restrictSocket = (target) => {
141
172
  if (!IS_WINDOWS) fs.chmodSync(target, 0o600);
142
173
  },
@@ -146,11 +177,16 @@ export class RelaySocketServer {
146
177
  this.dispatch = dispatch;
147
178
  this.dispatchDesktop = dispatchDesktop;
148
179
  this.resolveExecutor = resolveExecutor;
180
+ this.assertAccount = assertAccount;
181
+ this.requireAccountContext = requireAccountContext;
149
182
  this.restrictSocket = restrictSocket;
150
183
  this.log = logFn;
151
184
  this.server = null;
152
185
  this.started = false;
153
186
  this.connections = new Set();
187
+ this.handlers = new Set();
188
+ this.accepting = true;
189
+ this.closed = Promise.resolve();
154
190
  this.processHandlers = new Map();
155
191
  }
156
192
 
@@ -248,6 +284,11 @@ export class RelaySocketServer {
248
284
  }
249
285
 
250
286
  #handleConnection(socket) {
287
+ if (!this.accepting) {
288
+ socket.on("error", () => {});
289
+ socket.end(`${JSON.stringify(errorResponse("RELAY_RELOADING", "The native relay is reloading; no message was sent", false))}\n`);
290
+ return;
291
+ }
251
292
  let buffer = Buffer.alloc(0);
252
293
  let handled = false;
253
294
  this.connections.add(socket);
@@ -265,7 +306,12 @@ export class RelaySocketServer {
265
306
  const index = buffer.indexOf(10);
266
307
  if (index < 0) return;
267
308
  handled = true;
268
- void this.#handleLine(socket, buffer.subarray(0, index).toString("utf8"));
309
+ const handler = this.#handleLine(socket, buffer.subarray(0, index).toString("utf8"));
310
+ this.handlers.add(handler);
311
+ void handler.catch((error) => {
312
+ this.log(`relay request failed: ${error.message}`);
313
+ this.#reply(socket, errorResponse("NATIVE_DISPATCH_FAILED", "The native relay request failed before delivery was confirmed"));
314
+ }).finally(() => this.handlers.delete(handler));
269
315
  buffer = Buffer.alloc(0);
270
316
  });
271
317
  }
@@ -278,10 +324,15 @@ export class RelaySocketServer {
278
324
  this.#reply(socket, errorResponse("RELAY_BAD_REQUEST", `malformed JSON: ${err.message}`));
279
325
  return;
280
326
  }
327
+ if (this.requireAccountContext && !boundRequest(payload)) {
328
+ this.#reply(socket, errorResponse("RELAY_BAD_REQUEST", "The account relay requires protocol 2 and the original account context", false));
329
+ return;
330
+ }
281
331
  const response = await handleRelayRequest(payload, {
282
332
  dispatch: this.dispatch,
283
333
  dispatchDesktop: this.dispatchDesktop,
284
334
  resolveExecutor: this.resolveExecutor,
335
+ assertAccount: this.assertAccount,
285
336
  });
286
337
  if (!response.ok) this.log(`relay refused ${payload?.targetThreadId ?? "?"}: ${response.error.message}`);
287
338
  else this.log(response.operation ? `completed Desktop operation ${response.operation}` : `relayed a message into thread ${response.targetThreadId}`);
@@ -298,9 +349,12 @@ export class RelaySocketServer {
298
349
  this.processHandlers.clear();
299
350
  for (const socket of this.connections) socket.destroy();
300
351
  this.connections.clear();
301
- try {
302
- this.server?.close();
303
- } catch {}
352
+ const server = this.server;
353
+ this.closed = new Promise((resolve) => {
354
+ if (!server) return resolve();
355
+ try { server.close(() => resolve()); }
356
+ catch { resolve(); }
357
+ });
304
358
  try {
305
359
  if (this.started && !IS_WINDOWS) fs.rmSync(this.socketPath, { force: true });
306
360
  } catch {}
@@ -314,10 +368,13 @@ export function startRelayWhenAvailable({ nativeTools, relay, log: logFn = () =>
314
368
  let delayMs = retryDelayMs;
315
369
  let resolveReady;
316
370
  const ready = new Promise((resolve) => { resolveReady = resolve; });
371
+ let pendingAttempt = null;
317
372
  const attempt = async () => {
318
373
  if (stopped) return;
374
+ let nativeConnected = false;
319
375
  try {
320
376
  await nativeTools.connect();
377
+ nativeConnected = true;
321
378
  if (stopped) {
322
379
  nativeTools.close();
323
380
  return;
@@ -331,8 +388,8 @@ export function startRelayWhenAvailable({ nativeTools, relay, log: logFn = () =>
331
388
  resolveReady(true);
332
389
  } catch (err) {
333
390
  if (stopped) return;
334
- nativeTools.close();
335
- if (!nativeTools.socketPath) {
391
+ if (!nativeConnected) nativeTools.close();
392
+ if (!nativeTools.socketPath && !nativeTools.hasDiscoveredSocket) {
336
393
  logFn(`native relay unavailable (${err.message})`);
337
394
  resolveReady(false);
338
395
  return;
@@ -340,22 +397,87 @@ export function startRelayWhenAvailable({ nativeTools, relay, log: logFn = () =>
340
397
  logFn(`native relay unavailable (${err.message}); retrying in ${delayMs}ms`);
341
398
  timer = globalThis.setTimeout(() => {
342
399
  timer = null;
343
- void attempt();
400
+ runAttempt();
344
401
  }, delayMs);
345
402
  timer.unref();
346
403
  delayMs = Math.min(delayMs * 2, maxRetryDelayMs);
347
404
  }
348
405
  };
349
- void attempt();
406
+ const runAttempt = () => {
407
+ const running = attempt();
408
+ pendingAttempt = running;
409
+ void running.finally(() => { if (pendingAttempt === running) pendingAttempt = null; });
410
+ return running;
411
+ };
412
+ const firstAttempt = runAttempt();
350
413
  return {
351
414
  ready,
415
+ firstAttempt,
416
+ get busy() { return pendingAttempt !== null; },
352
417
  stop() {
353
- if (stopped) return;
418
+ if (stopped) return pendingAttempt ?? Promise.resolve();
354
419
  stopped = true;
355
420
  globalThis.clearTimeout(timer);
356
421
  nativeTools.close();
357
422
  relay.stop();
358
423
  resolveReady(false);
424
+ return (pendingAttempt ?? Promise.resolve()).then(() => relay.closed);
425
+ },
426
+ };
427
+ }
428
+
429
+ export function createNativeRelayLifecycle({ nativeTools, relays, log: logFn = () => {} }) {
430
+ const servers = [...new Set(relays)];
431
+ let startups = [];
432
+ let ownedSockets = [];
433
+ const inspect = () => {
434
+ if (servers.some((relay) => relay.handlers.size)) return "Native relay requests are still active";
435
+ if (servers.some((relay) => relay.connections.size)) return "Native relay clients are still connected";
436
+ if (nativeTools.pending.size) return "Native Desktop dispatches are still pending";
437
+ if (nativeTools.connecting || startups.some((startup) => startup.busy)) return "Native relay startup is still pending";
438
+ return null;
439
+ };
440
+ const stop = async () => {
441
+ for (const relay of servers) relay.accepting = false;
442
+ const active = startups;
443
+ startups = [];
444
+ await Promise.all(active.map((startup) => startup.stop()));
445
+ for (const relay of servers) relay.stop();
446
+ await Promise.all(servers.map((relay) => relay.closed));
447
+ nativeTools.close();
448
+ };
449
+ const activate = async () => {
450
+ if (startups.length) {
451
+ for (const relay of servers) relay.accepting = true;
452
+ return;
453
+ }
454
+ for (const relay of servers) relay.accepting = true;
455
+ startups = servers.map((relay) => startRelayWhenAvailable({ nativeTools, relay, log: logFn }));
456
+ await Promise.all(startups.map((startup) => startup.firstAttempt));
457
+ if (servers.some((relay) => ownedSockets.includes(relay.socketPath) && !relay.started)) {
458
+ await stop();
459
+ throw new Error("The replacement native relay could not reclaim its listening sockets");
460
+ }
461
+ };
462
+ return {
463
+ inspect,
464
+ activate,
465
+ stop,
466
+ async quiesce() {
467
+ for (const relay of servers) relay.accepting = false;
468
+ const reason = inspect();
469
+ if (reason) throw new Error(reason);
470
+ ownedSockets = servers.filter((relay) => relay.started).map((relay) => relay.socketPath);
471
+ await stop();
472
+ },
473
+ exportState: () => ({ ownedSockets }),
474
+ restore(payload) {
475
+ if (Object.keys(payload).some((key) => key !== "ownedSockets") || !Array.isArray(payload.ownedSockets)
476
+ || payload.ownedSockets.length > servers.length || new Set(payload.ownedSockets).size !== payload.ownedSockets.length
477
+ || payload.ownedSockets.some((socket) => typeof socket !== "string" || !servers.some((relay) => relay.socketPath === socket))) {
478
+ throw new Error("Native relay listener state does not match this worker");
479
+ }
480
+ ownedSockets = [...payload.ownedSockets];
359
481
  },
360
482
  };
361
483
  }
@@ -379,19 +501,29 @@ if (invokedDirectly) {
379
501
  );
380
502
 
381
503
  const nativeTools = new NativeToolsClient();
382
- const dispatch = (args) => nativeTools.dispatch(args);
504
+ const dispatch = (args, options) => nativeTools.dispatch(args, options);
383
505
 
384
506
  const relay = new RelaySocketServer({
385
507
  socketPath: relaySocketPath(),
386
508
  dispatch,
387
- dispatchDesktop: (args) => nativeTools.dispatchDesktop(args),
509
+ dispatchDesktop: (args, options) => nativeTools.dispatchDesktop(args, options),
388
510
  log,
389
511
  });
390
512
  const desktopRelay = desktopTaskSocketPath() === relay.socketPath ? relay : new RelaySocketServer({
391
513
  socketPath: desktopTaskSocketPath(),
392
- dispatchDesktop: (args) => nativeTools.dispatchDesktop(args),
514
+ dispatchDesktop: (args, options) => nativeTools.dispatchDesktop(args, options),
393
515
  log,
394
516
  });
517
+ const accountRelay = new RelaySocketServer({
518
+ socketPath: accountRelaySocketPath(),
519
+ requireAccountContext: true,
520
+ dispatch,
521
+ dispatchDesktop: (args, options) => nativeTools.dispatchDesktop(args, options),
522
+ log,
523
+ });
524
+ const runtime = createRuntimeState();
525
+ const lifecycle = createNativeRelayLifecycle({ nativeTools, relays: [relay, desktopRelay, accountRelay], log });
526
+ const reload = createReloadControl({ entry: "native-relay-companion.mjs", ...lifecycle });
395
527
 
396
528
  mcp.registerTool(
397
529
  "native_relay_status",
@@ -406,7 +538,7 @@ if (invokedDirectly) {
406
538
  openWorldHint: false,
407
539
  },
408
540
  },
409
- async () => {
541
+ async () => reload.run(async () => {
410
542
  const listening = await relay.isListening();
411
543
  let executor = "(unconfigured)";
412
544
  try {
@@ -416,14 +548,17 @@ if (invokedDirectly) {
416
548
  executor = err.message;
417
549
  }
418
550
  return {
551
+ structuredContent: { runtime: runtime.status() },
419
552
  content: [
420
553
  {
421
554
  type: "text",
422
555
  text: [
423
556
  `platform: ${PLATFORM_LABEL} (${process.platform}/${process.arch})`,
424
557
  `companion: codex-native-relay ${VERSION}`,
558
+ `runtime state: ${runtime.status().current ? "current" : "stale"} (${reload.inspect().phase}, PID ${process.pid})`,
425
559
  `relay socket: ${relay.started ? relay.socketPath : `${relay.socketPath} (${listening ? "shared companion listening" : "not listening"})`}`,
426
560
  `desktop tasks: ${desktopRelay.socketPath} (${await desktopRelay.isListening() ? "listening" : "not listening"})`,
561
+ `account relay: ${accountRelay.socketPath} (protocol ${ACCOUNT_RELAY_PROTOCOL_VERSION}, ${await accountRelay.isListening() ? "listening" : "not listening"})`,
427
562
  `executor: ${executor}`,
428
563
  `dispatch: ${process.env.CODEX_NATIVE_RELAY_METHOD ?? NATIVE_DISPATCH_METHOD}`,
429
564
  `native pipe: ${nativeTools.socketPath ?? "unavailable (requires Codex Desktop)"}`,
@@ -431,12 +566,12 @@ if (invokedDirectly) {
431
566
  },
432
567
  ],
433
568
  };
434
- },
569
+ }),
435
570
  );
436
571
 
437
- const startup = startRelayWhenAvailable({ nativeTools, relay, log });
438
- const desktopStartup = desktopRelay === relay ? startup : startRelayWhenAvailable({ nativeTools, relay: desktopRelay, log });
439
- mcp.server.onclose = () => { startup.stop(); desktopStartup.stop(); };
572
+ if (!reload.staged) void lifecycle.activate().catch((error) => log(`native relay startup failed: ${error.message}`));
573
+ mcp.server.onclose = () => { void lifecycle.stop(); };
440
574
  await mcp.connect(new StdioServerTransport());
575
+ reload.listen();
441
576
  log(`ready on ${PLATFORM_LABEL} (${relay.started ? relay.socketPath : "socket down"})`);
442
577
  }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runSupervisor } from "./mcp-supervisor.mjs";
3
+ await runSupervisor("native-relay-companion.mjs");