@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 +11 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/scripts/install-native-relay.mjs +1 -1
- package/src/claude-bridge.mjs +4 -2
- package/src/codex-sender-context.mjs +13 -2
- package/src/index.mjs +1 -1
- package/src/native-relay-companion.mjs +1 -1
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
|
|
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
|
|
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
|
@@ -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.
|
|
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";
|
package/src/claude-bridge.mjs
CHANGED
|
@@ -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.
|
|
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 (
|
|
107
|
-
|
|
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
|
@@ -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.
|
|
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) {
|