@minhspark/codex-mcp-bridge 1.22.18 → 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 +12 -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/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ 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
|
+
|
|
7
19
|
## [1.22.18] - 2026-10-22
|
|
8
20
|
|
|
9
21
|
### 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
|
+
}
|