@minhspark/codex-mcp-bridge 1.13.5 → 1.13.6

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/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](ht
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.13.6] - 2026-09-06
8
+
9
+ ### Fixed
10
+
11
+ - Classify a Desktop sender by its human approval policy alone. `node_repl_auto_review_required` is a Codex model-catalog requirement (`gpt-6-astra` cannot run without auto review) and `auto_review_enabled` names an automated Guardian reviewer; neither adds a human prompt, so a verified full-access `approval_policy: never` sender is `bypass` whichever review flags the host sets. Since 1.13.5 such senders were downgraded to `prompting`, and a bypassing Claude Desktop recipient held every message behind a `peer_inbound_approval` dialog that Claude Desktop does not declare it can render, so nothing sent from that model could ever be delivered. Review flags remain required booleans and are still reported in status and receipts.
12
+ - Report the verified sender's approval policy in `claude_bridge_status`, and state in held receipts that Claude Desktop exposes no peer approval dialog.
13
+
14
+ ### Upgrade notes
15
+
16
+ - Reconnect the `claude-bridge` MCP in the Codex Desktop task after upgrading. A bridge process started before this version keeps the old classification until it is restarted; its status reports stale source and refuses new sends once the files change.
17
+
7
18
  ## [1.13.5] - 2026-09-06
8
19
 
9
20
  ### Fixed
package/README.md CHANGED
@@ -400,13 +400,13 @@ Sends to the same Claude session run in order. An outstanding message blocks all
400
400
 
401
401
  Both bridge status tools report the loaded source fingerprint and process identity. Source or Desktop-routing configuration changes make the running process stale and block new sends until reconnect, while Claude inbox/receipt reads remain available. This guard applies only after the version containing it has loaded; upgrading files cannot retrofit the guard into an older process already running.
402
402
 
403
- Claude's [inbound permission controls](https://code.claude.com/docs/en/cross-session-messaging#control-inbound-messages) remain authoritative. A missing sender permission class can cause a bypassing recipient to hold the message. Do not change `CLAUDE_BRIDGE_PERMISSION_MODE` or recipient settings merely to bypass a hold/refusal. Legacy CLI configuration must truthfully represent every session using that MCP entry; Desktop sends derive their class from the verified calling turn. If the Desktop UI does not expose the pending approval, report that boundary and keep the original receipt rather than creating a CLI session.
403
+ Claude's [inbound permission controls](https://code.claude.com/docs/en/cross-session-messaging#control-inbound-messages) remain authoritative. A missing or mismatched sender permission class makes a bypassing recipient hold the message. Claude Desktop does not declare the `peer_inbound_approval` dialog kind to Claude Code, so a message held by a Desktop-hosted session has no approval button there; Claude Code keeps it until `dialogExpiry` (five minutes by default) and then reports it as expired. A hold in Desktop mode is therefore a sender-classification problem to fix at the sender, never a prompt to wait for. Do not change `CLAUDE_BRIDGE_PERMISSION_MODE` or recipient settings merely to bypass a hold/refusal. Legacy CLI configuration must truthfully represent every session using that MCP entry; Desktop sends derive their class from the verified calling turn. If the Desktop UI does not expose the pending approval, report that boundary and keep the original receipt rather than creating a CLI session.
404
404
 
405
405
  Claude Desktop disables the CLI-native `SendMessage` tool. For Desktop targets, the bridge requests an ordinary answer in the destination conversation and reads only a completed assistant turn descended from the injected message UUID. Unrelated human prompts and sidechains are excluded. Late answers remain available through the inbox while the bridge process is running. This does not change Desktop tool permissions or the receiver's inbound policy.
406
406
 
407
407
  In Desktop-only mode, the sender is resolved separately for every MCP call. Codex supplies `x-codex-turn-metadata` with the calling task and turn IDs; the bridge matches them to one active, local Codex Desktop rollout and reads its effective permission profile and approval settings. The diagnostic `sandbox_mode` label is never used to authorize a send. Missing or invalid host review flags, a completed or superseded turn, or an unsupported permission profile blocks sending before any message bytes are written. Status distinguishes enabled, disabled, missing, and invalid review flags. Do not supply fabricated MCP metadata or use another process to make a blocked send succeed.
408
408
 
409
- Only an explicitly disabled permission profile with a matching full-access sandbox, user approval reviewer, disabled host review flags, and `approval_policy: never` maps to `bypass`. The same verified profile with a known prompting approval policy or either host review flag explicitly enabled maps to `prompting`. Both flags must be present as booleans; enabled review never grants bypass permissions. Other profiles remain unsupported and fail closed. Host review still applies to the MCP call, and the receiver may require approval for a prompting sender. The context, approval policy, and review flags are checked again before each connection attempt and immediately before writing, so queued messages cannot inherit another turn's permissions. Receipts preserve the original sender's review state.
409
+ Only an explicitly disabled permission profile with a matching full-access sandbox and user approval reviewer is supported. With `approval_policy: never` it maps to `bypass`; with `on-request`, `on-failure`, or `untrusted` it maps to `prompting`. The class answers the question Claude's parity gate asks, whether a human still prompts the sender, so Codex's two host review flags never change it: `node_repl_auto_review_required` is a model catalog requirement (Codex refuses such a model without auto review) and `auto_review_enabled` names an automated Guardian reviewer; neither adds a human prompt. Both flags must still be present as booleans and are reported in status and preserved in receipts as evidence. Other profiles remain unsupported and fail closed. The receiver may still require approval for a prompting sender. The context, approval policy, and review flags are checked again before each connection attempt and immediately before writing, so queued messages cannot inherit another turn's permissions.
410
410
 
411
411
  `CLAUDE_BRIDGE_PERMISSION_MODE` remains a legacy CLI setting and is ignored for Desktop sends. A manually bound relay task is not proof of the sender's identity. Correlated Desktop replies retain the original sending task ID even if the relay binding later changes.
412
412
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhspark/codex-mcp-bridge",
3
- "version": "1.13.5",
3
+ "version": "1.13.6",
4
4
  "description": "Two-way MCP bridge between Claude and Codex: prompts into a live Codex thread, messages into a running Claude Code session.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -20,7 +20,7 @@ import { exitForVersionRequest } from "../src/cli-version.mjs";
20
20
 
21
21
  exitForVersionRequest(import.meta.url);
22
22
 
23
- const VERSION = "1.13.5";
23
+ const VERSION = "1.13.6";
24
24
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
25
25
  const entry = path.join(root, "src", "native-relay-companion.mjs");
26
26
  const serverName = process.env.CODEX_NATIVE_RELAY_NAME ?? "codex-native-relay";
@@ -16,7 +16,7 @@ import { ReplyForwarder } from "./reply-forwarder.mjs";
16
16
 
17
17
  exitForVersionRequest(import.meta.url);
18
18
 
19
- const VERSION = "1.13.5";
19
+ const VERSION = "1.13.6";
20
20
  const FORWARD_MIN_INTERVAL_MS = 5000;
21
21
  const FORWARD_MAX_PER_SESSION = 50;
22
22
 
@@ -138,6 +138,7 @@ const server = new McpServer(
138
138
  "In Desktop-only mode, both destinations must belong to their Desktop apps. " +
139
139
  "Read the Desktop task title and task ID as well as the exact project directory and sessionId before sending. " +
140
140
  "The host's current MCP turn metadata identifies the sender; unknown or stale permission context blocks sending. " +
141
+ "The sender class follows the verified approval policy; automated review flags are reported but never change it. " +
141
142
  "Never launch a CLI session or an external app-server as a substitute. A receipt confirms a reply, not visual verification in the app. " +
142
143
  "A held receipt does not prove that Desktop exposes an approval button; verify the UI before asking the user to approve.",
143
144
  },
@@ -256,7 +257,7 @@ server.registerTool(
256
257
  const targetLabel = `${session.desktop?.title ?? session.name ?? session.pid} (pid ${session.pid}, session ${session.sessionId ?? "?"}, via ${session.entrypoint ?? "unknown"}, cwd ${session.cwd ?? "?"})`;
257
258
 
258
259
  if (!reply && delivery && delivery.status !== "delivered") {
259
- return result(`Claude inbox reported ${delivery.status} for ${targetLabel}.\n${delivery.reason}\nMessage id: ${msgId}\nInspect read_claude_delivery with this message ID. Do not resend, change the sender permission class, or alter recipient permissions to bypass this receipt. The approval UI has not been verified; do not tell the user an approval button exists without inspecting this exact Desktop task.`, true);
260
+ return result(`Claude inbox reported ${delivery.status} for ${targetLabel}.\n${delivery.reason}\nMessage id: ${msgId}\nInspect read_claude_delivery with this message ID. Do not resend, change the sender permission class, or alter recipient permissions to bypass this receipt. The approval UI has not been verified; Claude Desktop declares no peer approval dialog, so do not tell the user an approval button exists without inspecting this exact Desktop task.`, true);
260
261
  }
261
262
 
262
263
  if (wait === 0) {
@@ -418,6 +419,7 @@ server.registerTool(
418
419
  `peer socket: ${peer.socketPath}`,
419
420
  `sender mode: ${sender?.mode ?? (desktopOnly ? "unverified - Desktop sends blocked" : peer.permissionMode ?? "unknown")}`,
420
421
  ...(sender ? [`sender context: ${sender.status} (${sender.source ?? "unavailable"})`, `sender task: ${sender.threadId ?? "unknown"}`, `sender turn: ${sender.turnId ?? "unknown"}`, ...(sender.reason ? [`sender detail: ${sender.reason}`] : [])] : []),
422
+ ...(sender?.approvalPolicy ? [`sender approval policy: ${sender.approvalPolicy}`] : []),
421
423
  ...(sender?.review ? [`sender auto review: ${sender.review.autoReview}`, `sender Node REPL review: ${sender.review.nodeReplReview}`] : []),
422
424
  `session policy: ${desktopOnly ? "desktop-only" : "all Claude Code entrypoints"}`,
423
425
  `live sessions: ${eligible.length}`,
@@ -96,6 +96,16 @@ function readState(file, maxBytes) {
96
96
  }
97
97
  }
98
98
 
99
+ /**
100
+ * Claude's inbound parity gate groups sessions by one question: does a human
101
+ * still prompt the sender? Codex answers it with approval_policy. Its two host
102
+ * review flags describe automated review instead - node_repl_auto_review_required
103
+ * is a model catalog requirement (Codex refuses such a model without auto
104
+ * review) and auto_review_enabled names a Guardian reviewer - so they are
105
+ * required as evidence and reported, but never move a sender between classes.
106
+ * Downgrading them held every send from such a model behind an approval
107
+ * dialog that Claude Desktop does not render.
108
+ */
99
109
  function permissionClass(context, metadata) {
100
110
  for (const field of ["auto_review_enabled", "node_repl_auto_review_required"]) {
101
111
  if (!Object.hasOwn(metadata, field) || typeof metadata[field] !== "boolean") throw new Error(`The caller's ${field} review evidence is ${reviewFlag(metadata, field)}; the host must supply an explicit boolean`);
@@ -103,8 +113,9 @@ function permissionClass(context, metadata) {
103
113
  if (context.approvals_reviewer !== "user") throw new Error("The caller's effective approval reviewer is unverified");
104
114
  if (!exactObject(context.permission_profile, ["type"]) || context.permission_profile.type !== "disabled") throw new Error("The caller's permission profile is restricted or unsupported; no permission class was inferred");
105
115
  if (!exactObject(context.sandbox_policy, ["type"]) || context.sandbox_policy.type !== "danger-full-access") throw new Error("The caller's effective sandbox policy does not match its disabled permission profile");
106
- if (!["never", "on-request", "on-failure", "untrusted"].includes(context.approval_policy)) throw new Error("The caller's effective approval policy is unsupported");
107
- return context.approval_policy === "never" && !metadata.auto_review_enabled && !metadata.node_repl_auto_review_required ? "bypass" : "prompting";
116
+ if (context.approval_policy === "never") return "bypass";
117
+ if (["on-request", "on-failure", "untrusted"].includes(context.approval_policy)) return "prompting";
118
+ throw new Error("The caller's effective approval policy is unsupported");
108
119
  }
109
120
 
110
121
  export function readCodexSenderContext(meta, { env = process.env, maxRolloutBytes = MAX_ROLLOUT_BYTES } = {}) {
package/src/index.mjs CHANGED
@@ -29,7 +29,7 @@ import { createRuntimeState } from "./runtime-state.mjs";
29
29
 
30
30
  exitForVersionRequest(import.meta.url);
31
31
 
32
- const VERSION = "1.13.5";
32
+ const VERSION = "1.13.6";
33
33
  const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
34
34
 
35
35
  /**
@@ -23,7 +23,7 @@ import { exitForVersionRequest } from "./cli-version.mjs";
23
23
 
24
24
  exitForVersionRequest(import.meta.url);
25
25
 
26
- const VERSION = "1.13.5";
26
+ const VERSION = "1.13.6";
27
27
  const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
28
28
 
29
29
  function errorResponse(code, message) {