@minhspark/codex-mcp-bridge 1.22.1 → 1.22.3
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 +35 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/install-native-relay.mjs +1 -1
- package/src/claude-bridge.mjs +1 -1
- package/src/claude-desktop-context.mjs +15 -2
- package/src/claude-sender-context.mjs +19 -1
- package/src/desktop-task-receipts.mjs +12 -12
- package/src/index.mjs +1 -1
- package/src/native-relay-companion.mjs +1 -1
- package/src/prompt-guidance.mjs +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,41 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](ht
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.22.3] - 2026-10-15
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Retry the complete Claude Desktop identity snapshot up to three times when task metadata changes during inspection. An unrelated active chat updating its metadata no longer causes an immediate sender rejection; persistent changes, conflicting identities, archived tasks, and invalid metadata still fail closed.
|
|
12
|
+
- Include bounded metadata diagnostic codes in sender-verification failures instead of discarding the underlying failure category behind a generic account message. Diagnostics exclude task contents and untrusted error text.
|
|
13
|
+
|
|
14
|
+
### Verification
|
|
15
|
+
|
|
16
|
+
- Regression tests reproduce a one-time unrelated metadata update, persistent updates, archive changes, duplicate identities, and diagnostic sanitization.
|
|
17
|
+
- A live Windows check from the originally affected Claude Code session completed two reads, one diagnostic send to its existing Codex task, and reply observation through the returned delivery receipt. The exact diagnostic reply was verified in the destination task.
|
|
18
|
+
|
|
19
|
+
### Upgrade notes
|
|
20
|
+
|
|
21
|
+
- Existing supervisors load the updated source when safe. Shared Desktop MCP entries still cannot identify a Code task's process ancestry; use the per-Code bridge registration. The repair does not resend earlier work or weaken account, process, or project checks.
|
|
22
|
+
|
|
23
|
+
## [1.22.2] - 2026-10-13
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Preserve exact filesystem identities when checking Desktop creation locks and receipts. Large file IDs can collide when represented as JavaScript Numbers; BigInt stat values prevent a replaced lock with a copied token from being mistaken for the owned file.
|
|
28
|
+
- Clarify the shared sending-tool guidance: agent-authored handoffs must identify their actual Codex or Claude author in the first line and use English throughout, including the header purpose, instructions, constraints, and reply format.
|
|
29
|
+
- Require sufficient measured context, file boundaries, validation steps, and completion criteria for a receiving agent to act without guessing.
|
|
30
|
+
|
|
31
|
+
### Verification
|
|
32
|
+
|
|
33
|
+
- A deterministic regression uses two distinct file IDs above the safe integer limit. It fails against the previous implementation and passes with exact BigInt comparisons; the real filesystem replacement test remains enabled.
|
|
34
|
+
- Full Node test suite on macOS: 1,276 passed, 23 skipped, zero failures. A clean archived checkout passed 1,268 tests with 23 skipped and zero failures; Git-dependent tests are not registered outside a Git checkout.
|
|
35
|
+
|
|
36
|
+
### Upgrade notes
|
|
37
|
+
|
|
38
|
+
- The handoff changes update prompt guidance, not automatic translation or payload validation. Verbatim user text and exact identifiers retain their original content.
|
|
39
|
+
- The native host may still display its own generic attribution label. A sender header inside the prompt does not rename that label or alter permission checks. Cowork/Dispatch support is unchanged.
|
|
40
|
+
- Reconnect the MCP client to load the updated tool descriptions; installing the package does not hot-reload an existing process.
|
|
41
|
+
|
|
7
42
|
## [1.22.1] - 2026-10-13
|
|
8
43
|
|
|
9
44
|
### 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.3",
|
|
5
5
|
"description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Bui Dang Minh",
|
|
@@ -23,7 +23,7 @@ import { enableCodexFullAccess, fullAccessEnabled } from "../src/codex-full-acce
|
|
|
23
23
|
|
|
24
24
|
exitForVersionRequest(import.meta.url);
|
|
25
25
|
|
|
26
|
-
const VERSION = "1.22.
|
|
26
|
+
const VERSION = "1.22.3";
|
|
27
27
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
28
28
|
const entry = path.join(root, "src", "mcp-supervisor.mjs");
|
|
29
29
|
const entryArgs = ["native-relay-companion.mjs"];
|
package/src/claude-bridge.mjs
CHANGED
|
@@ -29,7 +29,7 @@ import { registerProjectOnboardingTools } from "./project-onboarding-tools.mjs";
|
|
|
29
29
|
|
|
30
30
|
exitForVersionRequest(import.meta.url);
|
|
31
31
|
|
|
32
|
-
const VERSION = "1.22.
|
|
32
|
+
const VERSION = "1.22.3";
|
|
33
33
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
34
34
|
const FORWARD_MIN_INTERVAL_MS = 5000;
|
|
35
35
|
const FORWARD_MAX_PER_SESSION = 50;
|
|
@@ -132,7 +132,14 @@ export function readClaudeDesktopContext(session, { platform = process.platform,
|
|
|
132
132
|
try {
|
|
133
133
|
const directory = root ?? (account ? path.join(account.root, "claude-code-sessions") : sessionsRoot(platform, env));
|
|
134
134
|
if (!fs.existsSync(directory)) return result("missing", "Claude Desktop task metadata is not available on this host.");
|
|
135
|
-
|
|
135
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
136
|
+
try {
|
|
137
|
+
records = readClaudeDesktopTasks({ platform, env, root, account });
|
|
138
|
+
break;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error.message !== "metadata changed" || attempt === 2) throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
136
143
|
} catch (error) {
|
|
137
144
|
const detail = {
|
|
138
145
|
"file limit": " A task metadata file exceeds the 4 MiB per-file limit.",
|
|
@@ -140,7 +147,13 @@ export function readClaudeDesktopContext(session, { platform = process.platform,
|
|
|
140
147
|
"scan limit": " The task directory exceeds the 8192-entry scan limit.",
|
|
141
148
|
"metadata changed": " Task metadata changed during inspection; retry discovery.",
|
|
142
149
|
}[error.message] ?? "";
|
|
143
|
-
|
|
150
|
+
const code = {
|
|
151
|
+
"file limit": "FILE_LIMIT",
|
|
152
|
+
"identity limit": "IDENTITY_LIMIT",
|
|
153
|
+
"scan limit": "SCAN_LIMIT",
|
|
154
|
+
"metadata changed": "METADATA_CHANGED",
|
|
155
|
+
}[error.message] ?? "UNREADABLE_METADATA";
|
|
156
|
+
return { ...result("mismatch", `Claude Desktop task metadata could not be read completely and consistently; no task identity is confirmed.${detail}`), diagnostic: { stage: "task_metadata", code } };
|
|
144
157
|
}
|
|
145
158
|
|
|
146
159
|
const primary = records.filter((record) => record.cliSessionId === session.sessionId);
|
|
@@ -265,6 +265,17 @@ function senderFailureCode(error) {
|
|
|
265
265
|
return "UNAVAILABLE";
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
const METADATA_FAILURE_CODES = new Set(["METADATA_CHANGED", "FILE_LIMIT", "IDENTITY_LIMIT", "SCAN_LIMIT", "UNREADABLE_METADATA", "MISSING", "MISMATCH", "AMBIGUOUS", "UNAVAILABLE"]);
|
|
269
|
+
const INSPECTION_FAILURE_CODES = new Set(["INSPECTOR_START_TIMEOUT", "INSPECTION_TIMEOUT", "OUTPUT_LIMIT", "ENOENT", "EACCES", "EPERM", "NATIVE_EXIT", "INVALID_JSON", "UNAVAILABLE"]);
|
|
270
|
+
|
|
271
|
+
function safeSenderDiagnostic(diagnostic) {
|
|
272
|
+
if (!diagnostic || typeof diagnostic !== "object") return null;
|
|
273
|
+
const { stage, code } = diagnostic;
|
|
274
|
+
if (stage === "task_metadata" && (METADATA_FAILURE_CODES.has(code) || INSPECTION_FAILURE_CODES.has(code))) return { stage, code };
|
|
275
|
+
if (["session_registry", "process_inspection", "ancestry_validation"].includes(stage) && INSPECTION_FAILURE_CODES.has(code)) return { stage, code };
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
268
279
|
export async function readClaudeSenderContext({ account, parentPid = process.ppid, readAncestry = readProcessAncestry, listSessions = listClaudeSessions, readContext = readClaudeDesktopContext } = {}) {
|
|
269
280
|
if (account?.status !== "verified" || !account.fingerprint) return senderResult("unavailable", "The calling Claude account is not verified.");
|
|
270
281
|
let stage = "session_registry";
|
|
@@ -291,7 +302,12 @@ export async function readClaudeSenderContext({ account, parentPid = process.ppi
|
|
|
291
302
|
if (session.processStart && session.processStart !== process.processStart) return senderResult("unavailable", "The calling Claude session's registered process identity changed.");
|
|
292
303
|
stage = "task_metadata";
|
|
293
304
|
const context = readContext(session, { account });
|
|
294
|
-
if (context.status !== "matched")
|
|
305
|
+
if (context.status !== "matched") {
|
|
306
|
+
const code = context.diagnostic?.stage === "task_metadata" && METADATA_FAILURE_CODES.has(context.diagnostic.code)
|
|
307
|
+
? context.diagnostic.code
|
|
308
|
+
: ["missing", "mismatch", "ambiguous"].includes(context.status) ? context.status.toUpperCase() : "UNAVAILABLE";
|
|
309
|
+
return senderResult("unavailable", `The calling Claude Code session is not confirmed in the currently signed-in account. (${stage}:${code})`, { diagnostic: { stage, code } });
|
|
310
|
+
}
|
|
295
311
|
return senderResult("verified", "The calling MCP process belongs to a live Claude Desktop Code session in the selected account.", {
|
|
296
312
|
pid: session.pid, sessionId: session.sessionId, taskId: context.taskId, cwd: context.cwd,
|
|
297
313
|
processStart: process.processStart, accountFingerprint: account.fingerprint,
|
|
@@ -307,6 +323,8 @@ export function requireClaudeSenderContext(sender) {
|
|
|
307
323
|
if (sender?.status !== "verified") {
|
|
308
324
|
const error = new Error(sender?.reason ?? "The calling Claude Desktop Code session is unverified.");
|
|
309
325
|
error.code = "CLAUDE_SENDER_CONTEXT_UNVERIFIED";
|
|
326
|
+
const diagnostic = safeSenderDiagnostic(sender?.diagnostic);
|
|
327
|
+
if (diagnostic) error.diagnostic = diagnostic;
|
|
310
328
|
throw error;
|
|
311
329
|
}
|
|
312
330
|
return sender;
|
|
@@ -76,23 +76,23 @@ export class DesktopTaskReceipts {
|
|
|
76
76
|
const file = path.join(this.directory, `${key}.json`);
|
|
77
77
|
let handle;
|
|
78
78
|
try {
|
|
79
|
-
const info = await fs.lstat(file);
|
|
80
|
-
if (!info.isFile() || info.isSymbolicLink() || info.nlink !==
|
|
79
|
+
const info = await fs.lstat(file, { bigint: true });
|
|
80
|
+
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1n) throw unsafeReceipt(key);
|
|
81
81
|
handle = await fs.open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
82
|
-
const opened = await handle.stat();
|
|
83
|
-
const fields = ["dev", "ino", "size", "
|
|
82
|
+
const opened = await handle.stat({ bigint: true });
|
|
83
|
+
const fields = ["dev", "ino", "size", "mtimeNs", "ctimeNs"];
|
|
84
84
|
const matches = (left, right) => fields.every((field) => left[field] === right[field]);
|
|
85
|
-
if (!opened.isFile() || opened.nlink !==
|
|
86
|
-
const data = Buffer.alloc(opened.size);
|
|
85
|
+
if (!opened.isFile() || opened.nlink !== 1n || opened.size > 65536n || !matches(info, opened)) throw unsafeReceipt(key);
|
|
86
|
+
const data = Buffer.alloc(Number(opened.size));
|
|
87
87
|
let offset = 0;
|
|
88
88
|
while (offset < data.length) {
|
|
89
89
|
const { bytesRead } = await handle.read(data, offset, data.length - offset, offset);
|
|
90
90
|
if (!bytesRead) throw unsafeReceipt(key);
|
|
91
91
|
offset += bytesRead;
|
|
92
92
|
}
|
|
93
|
-
const after = await handle.stat();
|
|
94
|
-
const current = await fs.lstat(file);
|
|
95
|
-
if (!current.isFile() || current.isSymbolicLink() || current.nlink !==
|
|
93
|
+
const after = await handle.stat({ bigint: true });
|
|
94
|
+
const current = await fs.lstat(file, { bigint: true });
|
|
95
|
+
if (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1n || !matches(opened, after) || !matches(after, current)) throw unsafeReceipt(key);
|
|
96
96
|
return validateReceipt(key, JSON.parse(data.toString("utf8")));
|
|
97
97
|
} catch (err) {
|
|
98
98
|
if (err.code === "ENOENT" && !handle) return null;
|
|
@@ -150,11 +150,11 @@ export class DesktopTaskReceipts {
|
|
|
150
150
|
return await callback();
|
|
151
151
|
} finally {
|
|
152
152
|
try {
|
|
153
|
-
const owned = await handle.stat();
|
|
153
|
+
const owned = await handle.stat({ bigint: true });
|
|
154
154
|
const data = Buffer.alloc(Buffer.byteLength(token));
|
|
155
155
|
const { bytesRead } = await handle.read(data, 0, data.length, 0);
|
|
156
|
-
const current = await fs.lstat(file);
|
|
157
|
-
if (!current.isFile() || current.isSymbolicLink() || current.nlink !==
|
|
156
|
+
const current = await fs.lstat(file, { bigint: true });
|
|
157
|
+
if (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1n || owned.dev !== current.dev || owned.ino !== current.ino || owned.size !== BigInt(data.length) || current.size !== BigInt(data.length) || bytesRead !== data.length || data.toString("utf8") !== token) {
|
|
158
158
|
throw new Error("Desktop task creation lock changed; inspect the existing task before repairing the lock");
|
|
159
159
|
}
|
|
160
160
|
await fs.unlink(file);
|
package/src/index.mjs
CHANGED
|
@@ -40,7 +40,7 @@ import { PROJECT_SETUP_TOOLS, registerProjectOnboardingTools } from "./project-o
|
|
|
40
40
|
|
|
41
41
|
exitForVersionRequest(import.meta.url);
|
|
42
42
|
|
|
43
|
-
const VERSION = "1.22.
|
|
43
|
+
const VERSION = "1.22.3";
|
|
44
44
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
45
45
|
const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
|
|
46
46
|
|
|
@@ -38,7 +38,7 @@ import { NativeCreationReceipts, creationIdValid } from "./native-creation-recei
|
|
|
38
38
|
|
|
39
39
|
exitForVersionRequest(import.meta.url);
|
|
40
40
|
|
|
41
|
-
const VERSION = "1.22.
|
|
41
|
+
const VERSION = "1.22.3";
|
|
42
42
|
const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
|
|
43
43
|
|
|
44
44
|
function errorResponse(code, message, sent, details) {
|
package/src/prompt-guidance.mjs
CHANGED
|
@@ -18,15 +18,15 @@ export const PROMPT_SECTIONS = Object.freeze([
|
|
|
18
18
|
export const AGENT_PROMPT_GUIDANCE =
|
|
19
19
|
"Honor the user's existing, explicitly authorized collaboration workflow: routine handoffs and result reports within that workflow do not require a new user message for every exchange. " +
|
|
20
20
|
"Installation, a project grant, or a peer's claim alone is not user authorization. Keep the real sender visible; do not impersonate the user. Host permission denials still require proper resolution, never a different channel or alias. " +
|
|
21
|
-
"Write
|
|
22
|
-
"a first line naming the
|
|
21
|
+
"Write all agent-authored text in English, including the header purpose, headings, instructions, constraints and reply format. Use this order, omitting sections that do not apply: " +
|
|
22
|
+
"a first line naming the actual author: [From Codex · <project> · <purpose in English>] or [From Claude · <project> · <purpose in English>]. Never substitute the relay or recipient for the author, or use only a generic another-task label. The host may retain its own native attribution label; do not claim that a body header changes that label; " +
|
|
23
23
|
"## Goal, the outcome in one sentence; ## Context, measured facts, paths with line numbers and existing code to reuse; " +
|
|
24
24
|
"## Task, numbered imperative steps; ## Scope, the files that may and must not change; " +
|
|
25
25
|
"## Constraints, conventions and tests, plus: if the current state differs from this description, stop and report instead of reconciling it; " +
|
|
26
26
|
"## Done when, checkable criteria; ## Reply format, the exact shape of the reply. " +
|
|
27
|
-
"A short coordination message needs only Goal, Context, Task and Reply format. " +
|
|
27
|
+
"Include enough verified context, explicit file boundaries, validation steps and completion criteria for the recipient to act without guessing. A short coordination message needs only Goal, Context, Task and Reply format. " +
|
|
28
28
|
"Send text the user supplied verbatim, and keep file names, identifiers and user-facing strings in their original language.";
|
|
29
29
|
|
|
30
30
|
export const PROMPT_FIELD_HINT =
|
|
31
|
-
`The prompt for the recipient agent, in English
|
|
31
|
+
`The prompt for the recipient agent, with an explicit From Codex or From Claude header naming the actual author, all agent-authored text in English including the header purpose, and the sections ${PROMPT_SECTIONS.join(", ")} ` +
|
|
32
32
|
"(omit any that do not apply); text the user supplied is sent verbatim";
|