@minhspark/codex-mcp-bridge 1.13.9 → 1.15.1

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.9";
33
+ const VERSION = "1.15.1";
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,14 @@ 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;
372
+ let nativeConnected = false;
317
373
  const attempt = async () => {
318
374
  if (stopped) return;
375
+ nativeConnected = false;
319
376
  try {
320
377
  await nativeTools.connect();
378
+ nativeConnected = true;
321
379
  if (stopped) {
322
380
  nativeTools.close();
323
381
  return;
@@ -331,8 +389,8 @@ export function startRelayWhenAvailable({ nativeTools, relay, log: logFn = () =>
331
389
  resolveReady(true);
332
390
  } catch (err) {
333
391
  if (stopped) return;
334
- nativeTools.close();
335
- if (!nativeTools.socketPath) {
392
+ if (!nativeConnected) nativeTools.close();
393
+ if (!nativeTools.socketPath && !nativeTools.hasDiscoveredSocket) {
336
394
  logFn(`native relay unavailable (${err.message})`);
337
395
  resolveReady(false);
338
396
  return;
@@ -340,22 +398,90 @@ export function startRelayWhenAvailable({ nativeTools, relay, log: logFn = () =>
340
398
  logFn(`native relay unavailable (${err.message}); retrying in ${delayMs}ms`);
341
399
  timer = globalThis.setTimeout(() => {
342
400
  timer = null;
343
- void attempt();
401
+ runAttempt();
344
402
  }, delayMs);
345
403
  timer.unref();
346
404
  delayMs = Math.min(delayMs * 2, maxRetryDelayMs);
347
405
  }
348
406
  };
349
- void attempt();
407
+ const runAttempt = () => {
408
+ const running = attempt();
409
+ pendingAttempt = running;
410
+ void running.finally(() => { if (pendingAttempt === running) pendingAttempt = null; });
411
+ return running;
412
+ };
413
+ const firstAttempt = runAttempt();
350
414
  return {
351
415
  ready,
416
+ firstAttempt,
417
+ get connected() { return nativeConnected; },
418
+ get busy() { return pendingAttempt !== null; },
352
419
  stop() {
353
- if (stopped) return;
420
+ if (stopped) return pendingAttempt ?? Promise.resolve();
354
421
  stopped = true;
355
422
  globalThis.clearTimeout(timer);
356
423
  nativeTools.close();
357
424
  relay.stop();
358
425
  resolveReady(false);
426
+ return (pendingAttempt ?? Promise.resolve()).then(() => relay.closed);
427
+ },
428
+ };
429
+ }
430
+
431
+ export function createNativeRelayLifecycle({ nativeTools, relays, log: logFn = () => {} }) {
432
+ const servers = [...new Set(relays)];
433
+ let startups = [];
434
+ let ownedSockets = [];
435
+ const inspect = () => {
436
+ if (servers.some((relay) => relay.handlers.size)) return "Native relay requests are still active";
437
+ if (servers.some((relay) => relay.connections.size)) return "Native relay clients are still connected";
438
+ if (nativeTools.pending.size) return "Native Desktop dispatches are still pending";
439
+ if (nativeTools.connecting || startups.some((startup) => startup.busy)) return "Native relay startup is still pending";
440
+ return null;
441
+ };
442
+ const stop = async () => {
443
+ for (const relay of servers) relay.accepting = false;
444
+ const active = startups;
445
+ startups = [];
446
+ await Promise.all(active.map((startup) => startup.stop()));
447
+ for (const relay of servers) relay.stop();
448
+ await Promise.all(servers.map((relay) => relay.closed));
449
+ nativeTools.close();
450
+ };
451
+ const activate = async () => {
452
+ if (startups.length) {
453
+ for (const relay of servers) relay.accepting = true;
454
+ return;
455
+ }
456
+ for (const relay of servers) relay.accepting = true;
457
+ startups = servers.map((relay) => startRelayWhenAvailable({ nativeTools, relay, log: logFn }));
458
+ await Promise.all(startups.map((startup) => startup.firstAttempt));
459
+ const restoredListeners = await Promise.all(servers.map(async (relay, index) =>
460
+ !ownedSockets.includes(relay.socketPath) || startups[index].connected && await relay.isListening()));
461
+ if (restoredListeners.some((available) => !available)) {
462
+ await stop();
463
+ throw new Error("The replacement native relay could not reclaim its listening sockets");
464
+ }
465
+ };
466
+ return {
467
+ inspect,
468
+ activate,
469
+ stop,
470
+ async quiesce() {
471
+ for (const relay of servers) relay.accepting = false;
472
+ const reason = inspect();
473
+ if (reason) throw new Error(reason);
474
+ ownedSockets = servers.filter((relay) => relay.started).map((relay) => relay.socketPath);
475
+ await stop();
476
+ },
477
+ exportState: () => ({ ownedSockets }),
478
+ restore(payload) {
479
+ if (Object.keys(payload).some((key) => key !== "ownedSockets") || !Array.isArray(payload.ownedSockets)
480
+ || payload.ownedSockets.length > servers.length || new Set(payload.ownedSockets).size !== payload.ownedSockets.length
481
+ || payload.ownedSockets.some((socket) => typeof socket !== "string" || !servers.some((relay) => relay.socketPath === socket))) {
482
+ throw new Error("Native relay listener state does not match this worker");
483
+ }
484
+ ownedSockets = [...payload.ownedSockets];
359
485
  },
360
486
  };
361
487
  }
@@ -379,19 +505,29 @@ if (invokedDirectly) {
379
505
  );
380
506
 
381
507
  const nativeTools = new NativeToolsClient();
382
- const dispatch = (args) => nativeTools.dispatch(args);
508
+ const dispatch = (args, options) => nativeTools.dispatch(args, options);
383
509
 
384
510
  const relay = new RelaySocketServer({
385
511
  socketPath: relaySocketPath(),
386
512
  dispatch,
387
- dispatchDesktop: (args) => nativeTools.dispatchDesktop(args),
513
+ dispatchDesktop: (args, options) => nativeTools.dispatchDesktop(args, options),
388
514
  log,
389
515
  });
390
516
  const desktopRelay = desktopTaskSocketPath() === relay.socketPath ? relay : new RelaySocketServer({
391
517
  socketPath: desktopTaskSocketPath(),
392
- dispatchDesktop: (args) => nativeTools.dispatchDesktop(args),
518
+ dispatchDesktop: (args, options) => nativeTools.dispatchDesktop(args, options),
393
519
  log,
394
520
  });
521
+ const accountRelay = new RelaySocketServer({
522
+ socketPath: accountRelaySocketPath(),
523
+ requireAccountContext: true,
524
+ dispatch,
525
+ dispatchDesktop: (args, options) => nativeTools.dispatchDesktop(args, options),
526
+ log,
527
+ });
528
+ const runtime = createRuntimeState();
529
+ const lifecycle = createNativeRelayLifecycle({ nativeTools, relays: [relay, desktopRelay, accountRelay], log });
530
+ const reload = createReloadControl({ entry: "native-relay-companion.mjs", ...lifecycle });
395
531
 
396
532
  mcp.registerTool(
397
533
  "native_relay_status",
@@ -406,7 +542,7 @@ if (invokedDirectly) {
406
542
  openWorldHint: false,
407
543
  },
408
544
  },
409
- async () => {
545
+ async () => reload.run(async () => {
410
546
  const listening = await relay.isListening();
411
547
  let executor = "(unconfigured)";
412
548
  try {
@@ -416,14 +552,17 @@ if (invokedDirectly) {
416
552
  executor = err.message;
417
553
  }
418
554
  return {
555
+ structuredContent: { runtime: runtime.status() },
419
556
  content: [
420
557
  {
421
558
  type: "text",
422
559
  text: [
423
560
  `platform: ${PLATFORM_LABEL} (${process.platform}/${process.arch})`,
424
561
  `companion: codex-native-relay ${VERSION}`,
562
+ `runtime state: ${runtime.status().current ? "current" : "stale"} (${reload.inspect().phase}, PID ${process.pid})`,
425
563
  `relay socket: ${relay.started ? relay.socketPath : `${relay.socketPath} (${listening ? "shared companion listening" : "not listening"})`}`,
426
564
  `desktop tasks: ${desktopRelay.socketPath} (${await desktopRelay.isListening() ? "listening" : "not listening"})`,
565
+ `account relay: ${accountRelay.socketPath} (protocol ${ACCOUNT_RELAY_PROTOCOL_VERSION}, ${await accountRelay.isListening() ? "listening" : "not listening"})`,
427
566
  `executor: ${executor}`,
428
567
  `dispatch: ${process.env.CODEX_NATIVE_RELAY_METHOD ?? NATIVE_DISPATCH_METHOD}`,
429
568
  `native pipe: ${nativeTools.socketPath ?? "unavailable (requires Codex Desktop)"}`,
@@ -431,12 +570,12 @@ if (invokedDirectly) {
431
570
  },
432
571
  ],
433
572
  };
434
- },
573
+ }),
435
574
  );
436
575
 
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(); };
576
+ if (!reload.staged) void lifecycle.activate().catch((error) => log(`native relay startup failed: ${error.message}`));
577
+ mcp.server.onclose = () => { void lifecycle.stop(); };
440
578
  await mcp.connect(new StdioServerTransport());
579
+ reload.listen();
441
580
  log(`ready on ${PLATFORM_LABEL} (${relay.started ? relay.socketPath : "socket down"})`);
442
581
  }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runSupervisor } from "./mcp-supervisor.mjs";
3
+ await runSupervisor("native-relay-companion.mjs");