@minhspark/codex-mcp-bridge 1.27.1 → 1.27.2

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-mcp-bridge",
3
- "version": "1.27.1",
3
+ "version": "1.27.2",
4
4
  "description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
5
5
  "author": {
6
6
  "name": "Bui Dang Minh"
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](https://semver.org/).
4
4
 
5
+ ## [1.27.2] - 2026-11-05
6
+
7
+ ### Fixed
8
+
9
+ - The native relay and the Antigravity sidecar now recover when an owner is killed between publishing its socket and removing the private name used to create it. That left the socket with a second hard link, which the safety check refused to remove, so every later start failed and `codex_bridge_status` kept reporting `RELAY_UNREACHABLE` with `ECONNREFUSED`, even after restarting Codex Desktop. Once a socket has refused a connection, a replacement removes only hard links of that same socket named like a private socket and owned by the same user. A socket with any other hard link is still refused, and a live owner that is confirmed by the probe is left alone. The removal is now bound to the socket that was probed, so an owner that replaces it during the probe keeps its socket. The native relay then retries, and the Antigravity sidecar start fails without touching that owner.
10
+
11
+ ### Verification and limits
12
+
13
+ - Reproduced on macOS: Codex Desktop killed a relay companion that exceeded its 30-second startup limit under heavy load, and the relay stayed down until the extra link was removed by hand. Regression tests kill a real process inside that window, and also cover a live owner and a foreign hard link.
14
+ - Private socket names left unlinked by earlier crashes stay in `~/.codex`. They are harmless and are not removed, because a live companion may be using a name of that form.
15
+ - A running supervised companion loads the new version once idle and then recovers on its own; a companion without a supervisor needs its host to reconnect. This reload was checked with a test harness, not on a machine stuck in this state with live Codex Desktop.
16
+ - In the native relay, a liveness probe that fails or times out under heavy load still counts as a dead owner. That existing limit is unchanged and now also covers an owner still inside the brief link step. The Antigravity sidecar treats a timeout as a live owner.
17
+ - `codex-native-relay-install --remove` still stops with `Unsafe relay socket file` on a socket in this state. Starting a companion recovers it, or the leftover private name can be removed by hand.
18
+ - Upgrade with `npm install -g @minhspark/codex-mcp-bridge@latest --allow-scripts=@minhspark/codex-mcp-bridge --foreground-scripts`.
19
+
5
20
  ## [1.27.1] - 2026-11-05
6
21
 
7
22
  ### Fixed
@@ -15,7 +30,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](ht
15
30
 
16
31
  - Reproduced on v1.27.0 with `CODEX_BRIDGE_THREAD_POLICY=roots` and no policy: inspection said `needs_setup` while `codex_bridge_status` said `authorized=true` for the same directory.
17
32
  - Delivery enforcement is unchanged: authorization, same-project, receipt and project-match decisions make the same choices as before. Only diagnostics, one error code and the message text change. Without a policy, the Codex-to-Claude bridge has no directory allowlist and now says so.
18
- - The cause of the reported `No saved local Codex Desktop project` refusal is still unknown. The new status and message will identify it on the next occurrence; this release does not change which projects match.
33
+ - The cause of the reported `No saved local Codex Desktop project` refusal is still unknown. The new status and message should narrow it down on the next occurrence; this release does not change which projects match.
19
34
  - Upgrade with `npm install -g @minhspark/codex-mcp-bridge@latest --allow-scripts=@minhspark/codex-mcp-bridge --foreground-scripts`, then reconnect the MCP clients so they load the new version.
20
35
 
21
36
  ## [1.27.0] - 2026-11-05
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhspark/codex-mcp-bridge",
3
- "version": "1.27.1",
3
+ "version": "1.27.2",
4
4
  "description": "Connect Claude, Codex, and Antigravity through MCP, with native Desktop handoffs and bridge-owned Antigravity CLI sessions.",
5
5
  "keywords": [
6
6
  "mcp",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "codex-mcp-bridge",
4
- "version": "1.27.1",
4
+ "version": "1.27.2",
5
5
  "description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
6
6
  "author": {
7
7
  "name": "Bui Dang Minh",
@@ -25,7 +25,7 @@ import { removeRelayFile } from "../src/native-relay-files.mjs";
25
25
 
26
26
  exitForVersionRequest(import.meta.url);
27
27
 
28
- const VERSION = "1.27.1";
28
+ const VERSION = "1.27.2";
29
29
  const root = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
30
30
  const entry = path.join(root, "src", "mcp-supervisor.mjs");
31
31
  const entryArgs = ["native-relay-companion.mjs"];
@@ -4,7 +4,7 @@ import net from "node:net";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { protectCurrentUserPipe } from "./windows-pipe-acl.mjs";
7
- import { relayFileIdentity, removeRelayFile } from "./native-relay-files.mjs";
7
+ import { releasePrivateSocketLinks, relayFileIdentity, removeRelayFile, staleSocketIdentity } from "./native-relay-files.mjs";
8
8
  import { bridgeDiagnostic } from "./recipient-preflight.mjs";
9
9
 
10
10
  const LIMIT = 2 * 1024 * 1024;
@@ -131,9 +131,12 @@ export async function serveAntigravityDesktop(handler, { directory = antigravity
131
131
  try { fs.linkSync(privatePath, socketPath); }
132
132
  catch (error) {
133
133
  if (error.code !== "EEXIST") throw error;
134
- const existing = relayFileIdentity(socketPath, "socket");
135
- if (!await socketRefused(socketPath)) throw new Error("Another Antigravity sidecar owns the socket");
136
- removeRelayFile(socketPath, { expected: existing });
134
+ const existing = staleSocketIdentity(socketPath);
135
+ if (existing !== null) {
136
+ if (!await socketRefused(socketPath)) throw new Error("Another Antigravity sidecar owns the socket");
137
+ releasePrivateSocketLinks(socketPath, existing);
138
+ removeRelayFile(socketPath, { expected: existing });
139
+ }
137
140
  fs.linkSync(privatePath, socketPath);
138
141
  }
139
142
  fs.unlinkSync(privatePath);
@@ -31,7 +31,7 @@ import { BACKGROUND_WORKER_GUIDANCE, registerBackgroundTools } from "./backgroun
31
31
 
32
32
  exitForVersionRequest(import.meta.url);
33
33
 
34
- const VERSION = "1.27.1";
34
+ const VERSION = "1.27.2";
35
35
  void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
36
36
  const FORWARD_MIN_INTERVAL_MS = 5000;
37
37
  const FORWARD_MAX_PER_SESSION = 50;
package/src/index.mjs CHANGED
@@ -44,7 +44,7 @@ import { BACKGROUND_WORKER_GUIDANCE, registerBackgroundTools } from "./backgroun
44
44
 
45
45
  exitForVersionRequest(import.meta.url);
46
46
 
47
- const VERSION = "1.27.1";
47
+ const VERSION = "1.27.2";
48
48
  void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
49
49
  const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
50
50
 
@@ -35,7 +35,7 @@ import { createRuntimeState } from "./runtime-state.mjs";
35
35
  import { hardenedBridgeEnabled } from "./hardened-root-policy.mjs";
36
36
  import { protectCurrentUserPipe } from "./windows-pipe-acl.mjs";
37
37
  import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
38
- import { removeRelayFile, relayFileIdentity, verifyRelaySocketMode } from "./native-relay-files.mjs";
38
+ import { releasePrivateSocketLinks, removeRelayFile, relayFileIdentity, staleSocketIdentity, verifyRelaySocketMode } from "./native-relay-files.mjs";
39
39
 
40
40
  import { NativeCreationReceipts, creationIdValid } from "./native-creation-receipts.mjs";
41
41
  import { registerRemoteCodexTools } from "./remote-codex-tools.mjs";
@@ -43,7 +43,7 @@ import { remoteCodexSocketPath, remoteNativeAdapter, remoteRequestResponse } fro
43
43
 
44
44
  exitForVersionRequest(import.meta.url);
45
45
 
46
- const VERSION = "1.27.1";
46
+ const VERSION = "1.27.2";
47
47
  const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
48
48
 
49
49
  function errorResponse(code, message, sent, details) {
@@ -565,9 +565,13 @@ export class RelaySocketServer {
565
565
  try { fs.linkSync(privatePath, this.socketPath); break; }
566
566
  catch (error) {
567
567
  if (error.code !== "EEXIST") throw error;
568
- if (!replaceStale || attempt || await this.#socketIsLive()) throw new Error(`another native relay companion already owns ${this.socketPath}`);
568
+ if (!replaceStale || attempt) throw new Error(`another native relay companion already owns ${this.socketPath}`);
569
+ const stale = staleSocketIdentity(this.socketPath);
570
+ if (stale === null) continue;
571
+ if (await this.#socketIsLive()) throw new Error(`another native relay companion already owns ${this.socketPath}`);
569
572
  this.log(`removing the stale relay socket left at ${this.socketPath}`);
570
- removeRelayFile(this.socketPath);
573
+ releasePrivateSocketLinks(this.socketPath, stale);
574
+ removeRelayFile(this.socketPath, { expected: stale });
571
575
  }
572
576
  }
573
577
  const identity = fs.lstatSync(this.socketPath, { bigint: true });
@@ -17,6 +17,49 @@ export function relayFileIdentity(target, kind, expected) {
17
17
  return current;
18
18
  }
19
19
 
20
+ const PRIVATE_SOCKET_LINK = /^\.[0-9a-f]{12}\.sock$/;
21
+
22
+ const ownedSocket = (stat) => stat.isSocket() && (typeof process.getuid !== "function" || stat.uid === BigInt(process.getuid()));
23
+
24
+ export function staleSocketIdentity(target) {
25
+ let root;
26
+ try { root = fs.realpathSync.native(path.dirname(target)); }
27
+ catch (error) { if (error.code === "ENOENT") return null; throw error; }
28
+ const file = path.resolve(root, path.basename(target));
29
+ if (!file.startsWith(root.endsWith(path.sep) ? root : `${root}${path.sep}`)) throw new Error("Relay file escaped its directory");
30
+ let current;
31
+ try { current = fs.lstatSync(file, { bigint: true }); }
32
+ catch (error) { if (error.code === "ENOENT") return null; throw error; }
33
+ if (!ownedSocket(current)) throw new Error(`Unsafe relay socket file: ${target}`);
34
+ return { dev: current.dev, ino: current.ino };
35
+ }
36
+
37
+ export function releasePrivateSocketLinks(target, expected) {
38
+ let root;
39
+ try { root = fs.realpathSync.native(path.dirname(target)); }
40
+ catch (error) { if (error.code === "ENOENT") return 0; throw error; }
41
+ const prefix = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
42
+ const file = path.resolve(root, path.basename(target));
43
+ if (!file.startsWith(prefix)) throw new Error("Relay file escaped its directory");
44
+ let current;
45
+ try { current = fs.lstatSync(file, { bigint: true }); }
46
+ catch (error) { if (error.code === "ENOENT") return 0; throw error; }
47
+ if (current.nlink === 1n || !ownedSocket(current) || current.dev !== expected.dev || current.ino !== expected.ino) return 0;
48
+ let released = 0;
49
+ for (const name of fs.readdirSync(root)) {
50
+ if (!PRIVATE_SOCKET_LINK.test(name)) continue;
51
+ const link = path.resolve(root, name);
52
+ if (!link.startsWith(prefix)) continue;
53
+ let stat;
54
+ try { stat = fs.lstatSync(link, { bigint: true }); }
55
+ catch (error) { if (error.code === "ENOENT") continue; throw error; }
56
+ if (!ownedSocket(stat) || stat.dev !== current.dev || stat.ino !== current.ino) continue;
57
+ fs.unlinkSync(link);
58
+ released += 1;
59
+ }
60
+ return released;
61
+ }
62
+
20
63
  export function removeRelayFile(target, { kind = "socket", directory = path.dirname(target), expected } = {}) {
21
64
  let root;
22
65
  try { root = fs.realpathSync.native(directory); }