@minhspark/codex-mcp-bridge 1.22.17 → 1.22.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.
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +30 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/install-native-relay.mjs +3 -3
- package/src/claude-bridge.mjs +1 -1
- package/src/index.mjs +1 -1
- package/src/mcp-supervisor.mjs +11 -7
- package/src/native-relay-companion.mjs +18 -17
- package/src/native-relay-files.mjs +40 -0
- package/src/thread-delivery.mjs +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](ht
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.22.19] - 2026-10-22
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Relay cleanup validates the canonical parent, expected file type, ownership, link count and identity before removing a configuration file or Unix socket. A regular file selected through a socket path is preserved, and symlinks and hardlinks are rejected.
|
|
12
|
+
- Unix relay sockets are created with mode `0600` before accepting connections. Startup verifies the socket identity and permissions without changing permissions through an environment-derived path, and restores the process umask after binding.
|
|
13
|
+
- Supervisor timer intervals use explicit lower and upper bounds, including Node's timer overflow limit, so CodeQL can verify the resource limits.
|
|
14
|
+
|
|
15
|
+
### Tests
|
|
16
|
+
|
|
17
|
+
- Regression tests cover ordinary files, hardlinks, symlinks, custom Unix sockets, private socket creation, umask restoration, replaced socket identities and timer boundaries.
|
|
18
|
+
|
|
19
|
+
## [1.22.18] - 2026-10-22
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- `wait_codex_reply` with a short `timeoutSec` no longer fails with `The response deadline elapsed while another operation held this thread` when no other operation held the thread. The tool's deadline starts before the caller's identity check, and that check spawns PowerShell on Windows. When it used up the whole budget, the observation never started and the bridge reported a held thread instead of a timeout. It now returns the normal timeout result with `nextAction: wait_codex_reply`. The held-thread message is now used only when another operation actually holds the thread; otherwise the error says the deadline elapsed before the operation started.
|
|
24
|
+
|
|
25
|
+
### Tests
|
|
26
|
+
|
|
27
|
+
- The account-change wait test no longer depends on timing. Its relay fixture parks `wait_threads` on a gate that the test releases, instead of a fixed 2.5 s sleep against a 1 s budget. That timing failed on a stalled `windows-latest` runner.
|
|
28
|
+
- The relay fixture in `test/bridge-integration.test.mjs` provisions its creation receipts once per file, so Windows tests no longer spend four PowerShell cold starts inside their own timing.
|
|
29
|
+
- The Desktop task integration assertions now print the tool result when they fail.
|
|
30
|
+
|
|
31
|
+
### Verification
|
|
32
|
+
|
|
33
|
+
- New unit tests cover a deadline that elapses before an unheld operation starts, and one that elapses while another operation holds the thread. The first fails on 1.22.17.
|
|
34
|
+
- Simulating a 1.2 s caller check reproduced the CI failure text exactly on 1.22.17. The rewritten test passes with 1.2 s and 3 s simulated delays.
|
|
35
|
+
- Full suite on macOS: 1558 pass, 0 fail, 23 skipped.
|
|
36
|
+
|
|
7
37
|
## [1.22.17] - 2026-10-22
|
|
8
38
|
|
|
9
39
|
### Fixed
|
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.22.
|
|
4
|
+
"version": "1.22.19",
|
|
5
5
|
"description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Bui Dang Minh",
|
|
@@ -21,10 +21,11 @@ import { applyStdioMcpRegistration, stdioMcpRegistration } from "../src/codex-mc
|
|
|
21
21
|
import { createReleaseSnapshot, snapshotRoot } from "../src/release-snapshot.mjs";
|
|
22
22
|
import { codexAccessPaths, enableCodexFullAccess, fullAccessEnabled } from "../src/codex-full-access.mjs";
|
|
23
23
|
import { stableInstallPath } from "../src/install-paths.mjs";
|
|
24
|
+
import { removeRelayFile } from "../src/native-relay-files.mjs";
|
|
24
25
|
|
|
25
26
|
exitForVersionRequest(import.meta.url);
|
|
26
27
|
|
|
27
|
-
const VERSION = "1.22.
|
|
28
|
+
const VERSION = "1.22.19";
|
|
28
29
|
const root = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
|
|
29
30
|
const entry = path.join(root, "src", "mcp-supervisor.mjs");
|
|
30
31
|
const entryArgs = ["native-relay-companion.mjs"];
|
|
@@ -50,8 +51,7 @@ if (remove) {
|
|
|
50
51
|
const retiredExecutor = readRelayConfig()?.relayThreadId;
|
|
51
52
|
const sockets = IS_WINDOWS ? [] : [...new Set([relaySocketPath(), desktopTaskSocketPath(), accountRelaySocketPath()])];
|
|
52
53
|
for (const leftover of [relayConfigPath(), ...sockets]) {
|
|
53
|
-
if (
|
|
54
|
-
fs.rmSync(leftover, { force: true });
|
|
54
|
+
if (removeRelayFile(leftover, { kind: leftover === relayConfigPath() ? "config" : "socket" })) {
|
|
55
55
|
console.log(`removed ${leftover}`);
|
|
56
56
|
}
|
|
57
57
|
}
|
package/src/claude-bridge.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import { discoverClaudeProject } from "./claude-project-discovery.mjs";
|
|
|
30
30
|
|
|
31
31
|
exitForVersionRequest(import.meta.url);
|
|
32
32
|
|
|
33
|
-
const VERSION = "1.22.
|
|
33
|
+
const VERSION = "1.22.19";
|
|
34
34
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
35
35
|
const FORWARD_MIN_INTERVAL_MS = 5000;
|
|
36
36
|
const FORWARD_MAX_PER_SESSION = 50;
|
package/src/index.mjs
CHANGED
|
@@ -41,7 +41,7 @@ import { createRelayWake, relayKeeperEnabled, relayWakeStateFile, startRelayKeep
|
|
|
41
41
|
|
|
42
42
|
exitForVersionRequest(import.meta.url);
|
|
43
43
|
|
|
44
|
-
const VERSION = "1.22.
|
|
44
|
+
const VERSION = "1.22.19";
|
|
45
45
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
46
46
|
const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
|
|
47
47
|
|
package/src/mcp-supervisor.mjs
CHANGED
|
@@ -18,6 +18,14 @@ const hasId = (message) => Object.hasOwn(message, "id");
|
|
|
18
18
|
const idKey = (id) => JSON.stringify(id);
|
|
19
19
|
const errorMessage = (id, message) => ({ jsonrpc: "2.0", id, error: { code: -32603, message } });
|
|
20
20
|
|
|
21
|
+
export function supervisorInterval(value, fallback) {
|
|
22
|
+
const parsed = Number(value);
|
|
23
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
24
|
+
if (parsed < 100) return 100;
|
|
25
|
+
if (parsed > 2147483647) return 2147483647;
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
function lines(stream, receive, failed) {
|
|
22
30
|
let buffer = Buffer.alloc(0);
|
|
23
31
|
stream.on("data", (chunk) => {
|
|
@@ -156,13 +164,9 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
156
164
|
const pipe = await resolveNativeToolsPipePath();
|
|
157
165
|
if (pipe) env.CODEX_APP_TOOLS_PIPE_PATH = pipe;
|
|
158
166
|
}
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
};
|
|
163
|
-
const pollMs = interval(env.CODEX_BRIDGE_RELOAD_POLL_MS, 1000);
|
|
164
|
-
const settleMs = interval(env.CODEX_BRIDGE_RELOAD_SETTLE_MS, 1500);
|
|
165
|
-
const pruneDelayMs = interval(env.CODEX_BRIDGE_RUNTIME_PRUNE_DELAY_MS, 60000);
|
|
167
|
+
const pollMs = supervisorInterval(env.CODEX_BRIDGE_RELOAD_POLL_MS, 1000);
|
|
168
|
+
const settleMs = supervisorInterval(env.CODEX_BRIDGE_RELOAD_SETTLE_MS, 1500);
|
|
169
|
+
const pruneDelayMs = supervisorInterval(env.CODEX_BRIDGE_RUNTIME_PRUNE_DELAY_MS, 60000);
|
|
166
170
|
let active;
|
|
167
171
|
let initializeParams;
|
|
168
172
|
let initializeResult;
|
|
@@ -34,12 +34,13 @@ import { createRuntimeState } from "./runtime-state.mjs";
|
|
|
34
34
|
import { hardenedBridgeEnabled } from "./hardened-root-policy.mjs";
|
|
35
35
|
import { protectCurrentUserPipe } from "./windows-pipe-acl.mjs";
|
|
36
36
|
import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
|
|
37
|
+
import { removeRelayFile, relayFileIdentity, verifyRelaySocketMode } from "./native-relay-files.mjs";
|
|
37
38
|
|
|
38
39
|
import { NativeCreationReceipts, creationIdValid } from "./native-creation-receipts.mjs";
|
|
39
40
|
|
|
40
41
|
exitForVersionRequest(import.meta.url);
|
|
41
42
|
|
|
42
|
-
const VERSION = "1.22.
|
|
43
|
+
const VERSION = "1.22.19";
|
|
43
44
|
const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
|
|
44
45
|
|
|
45
46
|
function errorResponse(code, message, sent, details) {
|
|
@@ -393,8 +394,8 @@ export class RelaySocketServer {
|
|
|
393
394
|
requireAccountContext = false,
|
|
394
395
|
strict = hardenedBridgeEnabled(),
|
|
395
396
|
protectSocket = protectCurrentUserPipe,
|
|
396
|
-
restrictSocket = (target) => {
|
|
397
|
-
if (!IS_WINDOWS)
|
|
397
|
+
restrictSocket = (target, expected) => {
|
|
398
|
+
if (!IS_WINDOWS) verifyRelaySocketMode(target, expected);
|
|
398
399
|
},
|
|
399
400
|
signalExit = true,
|
|
400
401
|
log: logFn = () => {},
|
|
@@ -468,7 +469,7 @@ export class RelaySocketServer {
|
|
|
468
469
|
this.aclReady = true;
|
|
469
470
|
for (const provisional of this.provisionalSockets) provisional.destroy();
|
|
470
471
|
this.provisionalSockets.clear();
|
|
471
|
-
} else this.restrictSocket(this.socketPath);
|
|
472
|
+
} else this.restrictSocket(this.socketPath, this.boundIdentity);
|
|
472
473
|
if (generation !== this.startGeneration) throw new Error("Relay socket startup was cancelled");
|
|
473
474
|
} catch (err) {
|
|
474
475
|
this.aclReady = !IS_WINDOWS || !this.strict;
|
|
@@ -524,10 +525,15 @@ export class RelaySocketServer {
|
|
|
524
525
|
#bind(target) {
|
|
525
526
|
return new Promise((resolve, reject) => {
|
|
526
527
|
this.server.once("error", reject);
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
528
|
+
const previousMask = IS_WINDOWS ? null : process.umask(0o177);
|
|
529
|
+
try {
|
|
530
|
+
this.server.listen(target, () => {
|
|
531
|
+
this.server.off("error", reject);
|
|
532
|
+
resolve();
|
|
533
|
+
});
|
|
534
|
+
} finally {
|
|
535
|
+
if (previousMask !== null) process.umask(previousMask);
|
|
536
|
+
}
|
|
531
537
|
});
|
|
532
538
|
}
|
|
533
539
|
|
|
@@ -553,7 +559,7 @@ export class RelaySocketServer {
|
|
|
553
559
|
if (error.code !== "EEXIST") throw error;
|
|
554
560
|
if (!replaceStale || attempt || await this.#socketIsLive()) throw new Error(`another native relay companion already owns ${this.socketPath}`);
|
|
555
561
|
this.log(`removing the stale relay socket left at ${this.socketPath}`);
|
|
556
|
-
|
|
562
|
+
removeRelayFile(this.socketPath);
|
|
557
563
|
}
|
|
558
564
|
}
|
|
559
565
|
const identity = fs.lstatSync(this.socketPath, { bigint: true });
|
|
@@ -570,13 +576,8 @@ export class RelaySocketServer {
|
|
|
570
576
|
async #listen({ replaceStale }) {
|
|
571
577
|
if (!IS_WINDOWS && await this.#listenLinked(replaceStale)) return;
|
|
572
578
|
try {
|
|
573
|
-
await
|
|
574
|
-
|
|
575
|
-
this.server.listen(this.socketPath, () => {
|
|
576
|
-
this.server.off("error", reject);
|
|
577
|
-
resolve();
|
|
578
|
-
});
|
|
579
|
-
});
|
|
579
|
+
await this.#bind(this.socketPath);
|
|
580
|
+
if (!IS_WINDOWS) this.boundIdentity = relayFileIdentity(this.socketPath, "socket");
|
|
580
581
|
} catch (err) {
|
|
581
582
|
if (err.code !== "EADDRINUSE" || !replaceStale) throw err;
|
|
582
583
|
if (IS_WINDOWS) {
|
|
@@ -590,7 +591,7 @@ export class RelaySocketServer {
|
|
|
590
591
|
throw new Error(`another native relay companion already owns ${this.socketPath}`);
|
|
591
592
|
}
|
|
592
593
|
this.log(`removing the stale relay socket left at ${this.socketPath}`);
|
|
593
|
-
|
|
594
|
+
removeRelayFile(this.socketPath);
|
|
594
595
|
await this.#listen({ replaceStale: false });
|
|
595
596
|
}
|
|
596
597
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function relayFileIdentity(target, kind, expected) {
|
|
5
|
+
const root = fs.realpathSync.native(path.dirname(target));
|
|
6
|
+
const name = path.basename(target);
|
|
7
|
+
if (kind === "config" && name !== "native-relay.json") throw new Error("Unexpected relay configuration filename");
|
|
8
|
+
const file = path.resolve(root, name);
|
|
9
|
+
if (!file.startsWith(root.endsWith(path.sep) ? root : `${root}${path.sep}`)) throw new Error("Relay file escaped its directory");
|
|
10
|
+
const current = fs.lstatSync(file, { bigint: true });
|
|
11
|
+
if (current.isSymbolicLink() || (kind === "config" ? !current.isFile() : !current.isSocket())
|
|
12
|
+
|| current.nlink !== 1n
|
|
13
|
+
|| typeof process.getuid === "function" && current.uid !== BigInt(process.getuid())
|
|
14
|
+
|| expected && (current.dev !== expected.dev || current.ino !== expected.ino)) {
|
|
15
|
+
throw new Error(`Unsafe relay ${kind} file: ${target}`);
|
|
16
|
+
}
|
|
17
|
+
return current;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function removeRelayFile(target, { kind = "socket", directory = path.dirname(target), expected } = {}) {
|
|
21
|
+
let root;
|
|
22
|
+
try { root = fs.realpathSync.native(directory); }
|
|
23
|
+
catch (error) { if (error.code === "ENOENT") return false; throw error; }
|
|
24
|
+
const name = kind === "config" ? "native-relay.json" : path.basename(target);
|
|
25
|
+
if (path.basename(target) !== name) throw new Error("Unexpected relay configuration filename");
|
|
26
|
+
const file = path.resolve(root, name);
|
|
27
|
+
if (!file.startsWith(root.endsWith(path.sep) ? root : `${root}${path.sep}`)) throw new Error("Relay file escaped its directory");
|
|
28
|
+
let identity;
|
|
29
|
+
try { identity = relayFileIdentity(file, kind, expected); }
|
|
30
|
+
catch (error) { if (error.code === "ENOENT") return false; throw error; }
|
|
31
|
+
if (fs.realpathSync.native(file) !== file) throw new Error("Relay file is redirected");
|
|
32
|
+
relayFileIdentity(file, kind, identity);
|
|
33
|
+
fs.unlinkSync(file);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function verifyRelaySocketMode(target, expected) {
|
|
38
|
+
const current = relayFileIdentity(target, "socket", expected);
|
|
39
|
+
if ((current.mode & 0o777n) !== 0o600n) throw new Error("Relay socket was not created with mode 0600");
|
|
40
|
+
}
|
package/src/thread-delivery.mjs
CHANGED
|
@@ -160,10 +160,12 @@ export class DesktopTaskDelivery {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
async withThread(threadId, operation, { deadline, onDeadline } = {}) {
|
|
163
|
+
const held = this.threadOperations.has(threadId);
|
|
163
164
|
const previous = this.threadOperations.get(threadId) ?? Promise.resolve();
|
|
164
165
|
let expired = false;
|
|
165
166
|
const current = previous.catch(() => {}).then(() => {
|
|
166
|
-
if (
|
|
167
|
+
if (deadline !== undefined && this.now() >= deadline) expired = true;
|
|
168
|
+
if (expired) throw new Error(`The response deadline elapsed ${held ? "while another operation held this thread" : "before the operation started"}. No new prompt was sent.`);
|
|
167
169
|
return operation();
|
|
168
170
|
});
|
|
169
171
|
this.threadOperations.set(threadId, current);
|