@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.
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +16 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/install-native-relay.mjs +1 -1
- package/src/antigravity-ipc.mjs +7 -4
- package/src/claude-bridge.mjs +1 -1
- package/src/index.mjs +1 -1
- package/src/native-relay-companion.mjs +8 -4
- package/src/native-relay-files.mjs +43 -0
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
|
|
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
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.
|
|
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.
|
|
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"];
|
package/src/antigravity-ipc.mjs
CHANGED
|
@@ -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 =
|
|
135
|
-
if (
|
|
136
|
-
|
|
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);
|
package/src/claude-bridge.mjs
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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); }
|