@minhspark/codex-mcp-bridge 1.15.2 → 1.16.1
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 +20 -0
- package/README.md +212 -565
- package/package.json +3 -1
- package/scripts/collect-repo-analytics.mjs +163 -0
- package/scripts/install-native-relay.mjs +1 -1
- package/scripts/publish-repo-analytics.mjs +208 -0
- package/scripts/refresh-hosted-analytics.mjs +33 -0
- package/scripts/render-repo-analytics.mjs +50 -0
- package/scripts/usage-summary.sql +19 -0
- package/src/claude-bridge-launcher.mjs +2 -1
- package/src/claude-bridge.mjs +194 -19
- package/src/claude-desktop-context.mjs +19 -13
- package/src/claude-session-creation.mjs +216 -0
- package/src/claude-session-router.mjs +16 -0
- package/src/codex-bridge-launcher.mjs +2 -1
- package/src/codex-native-response.mjs +333 -0
- package/src/hardened-root-policy.mjs +95 -0
- package/src/index.mjs +17 -5
- package/src/native-relay-companion.mjs +268 -18
- package/src/native-relay.mjs +3 -0
- package/src/peer-protocol.mjs +111 -11
- package/src/platform.mjs +18 -0
- package/src/reply-forwarder.mjs +10 -3
- package/src/security-policy.mjs +4 -0
- package/src/telemetry-config.mjs +2 -0
- package/src/telemetry.mjs +149 -0
- package/src/thread-delivery.mjs +89 -11
- package/src/turn.mjs +35 -3
- package/src/windows-pipe-acl.mjs +27 -0
- package/src/windows-pipe-acl.ps1 +85 -0
package/src/claude-bridge.mjs
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
5
6
|
|
|
6
7
|
import { CodexAppServerClient } from "./app-server-client.mjs";
|
|
7
|
-
import { PLATFORM_LABEL } from "./platform.mjs";
|
|
8
|
-
import { PeerEndpoint, assertClaudeSessionCwd, assertClaudeSessionProcess, findClaudeSession, listClaudeSessions, readTranscript } from "./peer-protocol.mjs";
|
|
8
|
+
import { PLATFORM_LABEL, openClaudeCodeComposer } from "./platform.mjs";
|
|
9
|
+
import { PeerEndpoint, assertClaudeSessionCwd, assertClaudeSessionProcess, findClaudeSession, listClaudeSessions, readTranscript, readInitialTranscriptMessage } from "./peer-protocol.mjs";
|
|
9
10
|
import { createThreadDelivery } from "./thread-delivery.mjs";
|
|
10
11
|
import { exitForVersionRequest } from "./cli-version.mjs";
|
|
11
12
|
import { desktopTasksConfigured } from "./native-relay.mjs";
|
|
12
13
|
import { createRuntimeState } from "./runtime-state.mjs";
|
|
13
|
-
import { readClaudeDesktopContext } from "./claude-desktop-context.mjs";
|
|
14
|
+
import { readClaudeDesktopContext, readClaudeDesktopTasks } from "./claude-desktop-context.mjs";
|
|
15
|
+
import { ClaudeSessionCreation } from "./claude-session-creation.mjs";
|
|
14
16
|
import { readClaudeInboundPolicy } from "./claude-inbound-policy.mjs";
|
|
15
17
|
import { assertRecipientClass, preflightFailure } from "./recipient-preflight.mjs";
|
|
16
18
|
import { readCodexSenderContext } from "./codex-sender-context.mjs";
|
|
@@ -18,11 +20,13 @@ import { ReplyForwarder } from "./reply-forwarder.mjs";
|
|
|
18
20
|
import { assertRoutingReload, clientReloadReason, createReloadControl } from "./reload-control.mjs";
|
|
19
21
|
import { readClaudeAccountContext } from "./desktop-account-context.mjs";
|
|
20
22
|
import { assertAccountIdentity, bindUnsolicitedClaudeMessageAccount, publicAccountState, readBridgeAccounts, requireBridgeAccounts, sameAccountIdentity } from "./bridge-account-context.mjs";
|
|
21
|
-
import { resolveClaudeDesktopSession } from "./claude-session-router.mjs";
|
|
23
|
+
import { resolveClaudeDesktopSession, sameClaudeDesktopRecipient } from "./claude-session-router.mjs";
|
|
24
|
+
import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
|
|
22
25
|
|
|
23
26
|
exitForVersionRequest(import.meta.url);
|
|
24
27
|
|
|
25
|
-
const VERSION = "1.
|
|
28
|
+
const VERSION = "1.16.1";
|
|
29
|
+
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
26
30
|
const FORWARD_MIN_INTERVAL_MS = 5000;
|
|
27
31
|
const FORWARD_MAX_PER_SESSION = 50;
|
|
28
32
|
|
|
@@ -30,6 +34,7 @@ const log = (msg) => process.stderr.write(`[claude-bridge] ${msg}\n`);
|
|
|
30
34
|
|
|
31
35
|
const defaultPeerName = process.env.CLAUDE_BRIDGE_PEER_NAME ?? `codex-${process.pid}`;
|
|
32
36
|
const desktopOnly = desktopTasksConfigured();
|
|
37
|
+
const rootPolicy = createHardenedRootPolicy();
|
|
33
38
|
const runtime = createRuntimeState({ configuration: desktopTasksConfigured });
|
|
34
39
|
|
|
35
40
|
const peer = new PeerEndpoint({
|
|
@@ -55,6 +60,35 @@ const forwarding = {
|
|
|
55
60
|
threadId: process.env.CODEX_THREAD_ID ?? null,
|
|
56
61
|
};
|
|
57
62
|
|
|
63
|
+
const creationRootBindings = new Map();
|
|
64
|
+
|
|
65
|
+
const creations = new ClaudeSessionCreation({
|
|
66
|
+
open: openClaudeCodeComposer,
|
|
67
|
+
listTasks: (account) => readClaudeDesktopTasks({ account }).filter((task) => rootPolicy.allows(task.cwd)),
|
|
68
|
+
listSessions: () => listClaudeSessions().filter((session) => rootPolicy.allows(session.cwd)),
|
|
69
|
+
readContext: (session, account) => readClaudeDesktopContext(session, { account }),
|
|
70
|
+
readTranscript: (...args) => {
|
|
71
|
+
const binding = rootPolicy.capture(args[1], "Claude creation transcript working directory");
|
|
72
|
+
const transcript = readInitialTranscriptMessage(...args);
|
|
73
|
+
rootPolicy.recheck(binding, "Claude creation transcript working directory");
|
|
74
|
+
return transcript;
|
|
75
|
+
},
|
|
76
|
+
readAccount: readClaudeAccountContext,
|
|
77
|
+
assertProcess: assertClaudeSessionProcess,
|
|
78
|
+
});
|
|
79
|
+
function recheckScopeBindings(bindings) {
|
|
80
|
+
if (!rootPolicy.enabled) return;
|
|
81
|
+
if (!bindings?.sender || !bindings?.recipient) throw Object.assign(new Error("Hardened bridge record has no original sender and recipient root binding"), { code: "ROOT_BINDING_MISSING" });
|
|
82
|
+
rootPolicy.recheck(bindings.sender, "Original Codex sender working directory");
|
|
83
|
+
rootPolicy.recheck(bindings.recipient, "Original Claude recipient working directory");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function recordVisible(record, accounts = readBridgeAccounts()) {
|
|
87
|
+
if (desktopOnly && !sameAccountIdentity(record?.accountContext, accounts)) return false;
|
|
88
|
+
if (!rootPolicy.enabled) return true;
|
|
89
|
+
try { recheckScopeBindings(record?.scopeBindings); return true; } catch { return false; }
|
|
90
|
+
}
|
|
91
|
+
|
|
58
92
|
const replyForwarder = new ReplyForwarder({
|
|
59
93
|
minIntervalMs: FORWARD_MIN_INTERVAL_MS,
|
|
60
94
|
maxPerSession: FORWARD_MAX_PER_SESSION,
|
|
@@ -63,16 +97,17 @@ const replyForwarder = new ReplyForwarder({
|
|
|
63
97
|
throw Object.assign(new Error("Bridge routing changed; the reply was not forwarded. Inspect the original receipt before reconnecting."), { code: "REPLY_ROUTING_CHANGED" });
|
|
64
98
|
}
|
|
65
99
|
if (desktopOnly) assertAccountIdentity(record.accountContext);
|
|
100
|
+
recheckScopeBindings(record.scopeBindings);
|
|
66
101
|
},
|
|
67
102
|
deliver: (threadId, record) => delivery.deliver(threadId, `[message from Claude session ${record.fromSocket ?? "?"}]\n${record.absorbed ? "[This reply is the closing text of a turn that absorbed the message while it was running; it may not address the message.]\n" : ""}\n${record.text}`,
|
|
68
|
-
desktopOnly ? { beforeSend: () => assertAccountIdentity(record.accountContext), accountContext: record.accountContext } : {}),
|
|
103
|
+
desktopOnly ? { beforeSend: () => { assertAccountIdentity(record.accountContext); recheckScopeBindings(record.scopeBindings); }, accountContext: record.accountContext } : {}),
|
|
69
104
|
});
|
|
70
105
|
|
|
71
106
|
const reload = createReloadControl({
|
|
72
107
|
entry: "claude-bridge.mjs",
|
|
73
108
|
inspect: () => peer.reloadReason() ?? replyForwarder.reloadReason() ?? clientReloadReason(codex),
|
|
74
109
|
quiesce: async () => { await peer.quiesce(); codex.close(); },
|
|
75
|
-
exportState: () => ({ desktopOnly, peer: peer.exportReloadState(), forwarding: replyForwarder.exportReloadState(), threadId: forwarding.threadId }),
|
|
110
|
+
exportState: () => ({ desktopOnly, peer: peer.exportReloadState(), forwarding: replyForwarder.exportReloadState(), threadId: forwarding.threadId, creations: creations.exportState(), creationRootBindings: [...creationRootBindings] }),
|
|
76
111
|
restore: (state) => {
|
|
77
112
|
assertRoutingReload(state.desktopOnly, desktopOnly);
|
|
78
113
|
if (state.threadId !== null && (typeof state.threadId !== "string" || !state.threadId.trim())) {
|
|
@@ -81,6 +116,14 @@ const reload = createReloadControl({
|
|
|
81
116
|
peer.restoreReloadState(state.peer);
|
|
82
117
|
replyForwarder.restoreReloadState(state.forwarding);
|
|
83
118
|
forwarding.threadId = state.threadId;
|
|
119
|
+
if (state.creations !== undefined) creations.restoreState(state.creations);
|
|
120
|
+
if (state.creationRootBindings !== undefined) {
|
|
121
|
+
if (!Array.isArray(state.creationRootBindings) || state.creationRootBindings.length > 32) throw new Error("Invalid Claude creation root bindings");
|
|
122
|
+
for (const entry of state.creationRootBindings) {
|
|
123
|
+
if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string" || creationRootBindings.has(entry[0]) || !creations.requests.has(entry[0])) throw new Error("Invalid Claude creation root binding");
|
|
124
|
+
creationRootBindings.set(entry[0], entry[1]);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
84
127
|
},
|
|
85
128
|
activate: () => peer.start(),
|
|
86
129
|
resume: () => peer.resume(),
|
|
@@ -89,6 +132,9 @@ const reload = createReloadControl({
|
|
|
89
132
|
function readReceipt(msgId) {
|
|
90
133
|
const receipt = peer.readDelivery(msgId);
|
|
91
134
|
if (desktopOnly && receipt && !sameAccountIdentity(receipt.accountContext, readBridgeAccounts())) return null;
|
|
135
|
+
if (rootPolicy.enabled) {
|
|
136
|
+
try { recheckScopeBindings(receipt?.scopeBindings); } catch { return null; }
|
|
137
|
+
}
|
|
92
138
|
return receipt ? { ...receipt, forwarding: replyForwarder.read(msgId) ?? receipt.forwardingError ?? null } : null;
|
|
93
139
|
}
|
|
94
140
|
|
|
@@ -117,6 +163,10 @@ function withDesktopContext(session, account = readClaudeAccountContext()) {
|
|
|
117
163
|
? { ...session, desktop: readClaudeDesktopContext(session, { account }), inbound: readClaudeInboundPolicy(session.cwd) } : session;
|
|
118
164
|
}
|
|
119
165
|
|
|
166
|
+
function assertSessionRoot(session, label = "Claude session") {
|
|
167
|
+
return rootPolicy.assert(session?.cwd, `${label} working directory`);
|
|
168
|
+
}
|
|
169
|
+
|
|
120
170
|
function assertDesktopTask(session, expectedTaskId) {
|
|
121
171
|
if (session.desktop?.status !== "matched") {
|
|
122
172
|
throw preflightFailure("CLAUDE_DESKTOP_TASK_UNVERIFIED", session.desktop?.reason ?? "The live session could not be matched to a Claude Desktop task.");
|
|
@@ -134,6 +184,47 @@ function assertSender(meta) {
|
|
|
134
184
|
return sender;
|
|
135
185
|
}
|
|
136
186
|
|
|
187
|
+
function assertCreationRoots(requestId, sender) {
|
|
188
|
+
if (!rootPolicy.enabled) return;
|
|
189
|
+
rootPolicy.assert(sender.cwd, "Codex sender working directory");
|
|
190
|
+
recheckScopeBindings(creationRootBindings.get(requestId));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function startClaudeCreation({ requestId, cwd, prompt }, meta) {
|
|
194
|
+
runtime.assertCurrent();
|
|
195
|
+
if (!desktopOnly) throw new Error("New Claude sessions require Desktop-only mode");
|
|
196
|
+
const sender = assertSender(meta);
|
|
197
|
+
const accounts = readBridgeAccounts();
|
|
198
|
+
const selectedAccounts = requireBridgeAccounts(accounts);
|
|
199
|
+
if (rootPolicy.enabled && creations.requests.has(requestId)) assertCreationRoots(requestId, sender);
|
|
200
|
+
const scopeBindings = rootPolicy.enabled ? creationRootBindings.get(requestId) ?? {
|
|
201
|
+
sender: rootPolicy.capture(sender.cwd, "Codex sender working directory"),
|
|
202
|
+
recipient: rootPolicy.capture(cwd, "Claude creation working directory"),
|
|
203
|
+
} : null;
|
|
204
|
+
const receipt = await creations.start({ requestId, cwd, prompt, account: accounts.claude, senderThreadId: sender.threadId,
|
|
205
|
+
beforeOpen: () => {
|
|
206
|
+
runtime.assertCurrent();
|
|
207
|
+
assertAccountIdentity(selectedAccounts);
|
|
208
|
+
recheckScopeBindings(scopeBindings);
|
|
209
|
+
if (rootPolicy.enabled && !rootPolicy.same(cwd, scopeBindings.recipient.path)) throw new Error("The Claude creation working directory changed before opening Desktop");
|
|
210
|
+
const current = assertSender(meta);
|
|
211
|
+
if (JSON.stringify(current) !== JSON.stringify(sender)) throw new Error("The calling Codex turn changed before opening Claude Desktop");
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
if (rootPolicy.enabled) creationRootBindings.set(requestId, scopeBindings);
|
|
215
|
+
return { ...textResult(JSON.stringify(receipt, null, 2)), structuredContent: { creation: receipt } };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function inspectClaudeCreation(requestId, meta) {
|
|
219
|
+
const sender = assertSender(meta);
|
|
220
|
+
assertCreationRoots(requestId, sender);
|
|
221
|
+
const accounts = readBridgeAccounts();
|
|
222
|
+
requireBridgeAccounts(accounts);
|
|
223
|
+
const receipt = await creations.inspect(requestId, { account: accounts.claude, senderThreadId: sender.threadId });
|
|
224
|
+
assertCreationRoots(requestId, sender);
|
|
225
|
+
return { ...textResult(JSON.stringify(receipt, null, 2)), structuredContent: { creation: receipt } };
|
|
226
|
+
}
|
|
227
|
+
|
|
137
228
|
function forwardToCodexThread(record) {
|
|
138
229
|
const threadId = record.replyThreadId ?? (desktopOnly ? null : forwarding.threadId);
|
|
139
230
|
if (!threadId) return;
|
|
@@ -146,6 +237,9 @@ function forwardToCodexThread(record) {
|
|
|
146
237
|
}
|
|
147
238
|
|
|
148
239
|
peer.onMessage((record) => {
|
|
240
|
+
if (rootPolicy.enabled && !record.cwd) { record.forwardingError = { status: "blocked", reasonCode: "ROOT_UNAUTHORIZED", reason: "Unsolicited inbound messages are disabled in hardened mode without a verified root binding" }; return; }
|
|
241
|
+
try { if (rootPolicy.enabled) rootPolicy.assert(record.cwd, "Inbound Claude message working directory"); }
|
|
242
|
+
catch (err) { record.forwardingError = { status: "blocked", reasonCode: "ROOT_UNAUTHORIZED", reason: err.message }; return; }
|
|
149
243
|
if (desktopOnly) bindUnsolicitedClaudeMessageAccount(record, {
|
|
150
244
|
hasPendingSource: (socket) => [...peer.pendingMessages.values()].some((pending) => pending.targetSocket === socket),
|
|
151
245
|
});
|
|
@@ -178,6 +272,50 @@ function registerTool(name, definition, handler) {
|
|
|
178
272
|
});
|
|
179
273
|
}
|
|
180
274
|
|
|
275
|
+
registerTool(
|
|
276
|
+
"start_claude_session",
|
|
277
|
+
{
|
|
278
|
+
title: "Prepare a new Claude Desktop Code session",
|
|
279
|
+
description: "Use only when the user explicitly requests a NEW Claude Desktop session. Open the official Code composer in an existing project with a prefilled prompt. Claude Desktop requires the user to confirm the folder and press Send; opening the link is not session creation. Retain requestId and inspect read_claude_creation. Never substitute an existing task or launch a CLI session.",
|
|
280
|
+
inputSchema: {
|
|
281
|
+
requestId: z.string().describe("Stable UUID for this creation; reuse exactly when inspecting or retrying"),
|
|
282
|
+
cwd: z.string().describe("Exact absolute directory of the existing Claude Desktop project"),
|
|
283
|
+
prompt: z.string().describe("Initial prompt for the NEW conversation; shown for user confirmation"),
|
|
284
|
+
},
|
|
285
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
286
|
+
},
|
|
287
|
+
async (args, extra) => startClaudeCreation(args, extra?._meta),
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
registerTool(
|
|
291
|
+
"read_claude_creation",
|
|
292
|
+
{
|
|
293
|
+
title: "Verify a new Claude Desktop session",
|
|
294
|
+
description: "Inspect an existing creation request without reopening or sending. A created receipt requires a new native task, matching account/project, live process identity, and the exact initial user prompt. awaiting_project_confirmation means the prompt arrived in a new task but its folder still differs; the user must adopt the requested project before creation can be confirmed. An old task resumed with a new process never counts as a new session.",
|
|
295
|
+
inputSchema: { requestId: z.string().describe("Original requestId returned by start_claude_session") },
|
|
296
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
297
|
+
},
|
|
298
|
+
async ({ requestId }, extra) => inspectClaudeCreation(requestId, extra?._meta),
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
registerTool(
|
|
302
|
+
"abandon_claude_creation",
|
|
303
|
+
{
|
|
304
|
+
title: "Release a cancelled Claude creation request",
|
|
305
|
+
description: "Use only when the user explicitly cancels a pending creation request. Release its composer reservation while retaining the receipt. This does not close Claude Desktop or prove that the prefilled prompt was never submitted; the old request remains inspectable.",
|
|
306
|
+
inputSchema: { requestId: z.string().describe("Exact creation requestId the user cancelled") },
|
|
307
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
308
|
+
},
|
|
309
|
+
async ({ requestId }, extra) => {
|
|
310
|
+
const sender = assertSender(extra?._meta);
|
|
311
|
+
assertCreationRoots(requestId, sender);
|
|
312
|
+
const accounts = readBridgeAccounts();
|
|
313
|
+
requireBridgeAccounts(accounts);
|
|
314
|
+
const receipt = await creations.abandon(requestId, { account: accounts.claude, senderThreadId: sender.threadId });
|
|
315
|
+
return { ...textResult(JSON.stringify(receipt, null, 2)), structuredContent: { creation: receipt } };
|
|
316
|
+
},
|
|
317
|
+
);
|
|
318
|
+
|
|
181
319
|
registerTool(
|
|
182
320
|
"list_claude_sessions",
|
|
183
321
|
{
|
|
@@ -196,11 +334,13 @@ registerTool(
|
|
|
196
334
|
},
|
|
197
335
|
async ({ includeDead, expectedCwd }) => {
|
|
198
336
|
try {
|
|
337
|
+
if (rootPolicy.enabled && expectedCwd !== undefined) rootPolicy.assert(expectedCwd, "Requested working directory");
|
|
199
338
|
if (expectedCwd !== undefined) assertClaudeSessionCwd({ cwd: expectedCwd }, expectedCwd);
|
|
200
339
|
const account = readClaudeAccountContext();
|
|
201
340
|
if (desktopOnly && account.status !== "verified") return { ...textResult(`Claude account ${account.status}: ${account.reason} Sign in and retry discovery.`, true), structuredContent: { account: publicAccountState(account), sessions: [] } };
|
|
202
341
|
const sessions = listClaudeSessions({ includeDead: includeDead ?? false }).filter(
|
|
203
342
|
(s) => s.pid !== process.pid && (!desktopOnly || s.entrypoint === "claude-desktop"),
|
|
343
|
+
).filter((session) => !rootPolicy.enabled || rootPolicy.allows(session.cwd)
|
|
204
344
|
).filter((session) => {
|
|
205
345
|
if (expectedCwd === undefined) return true;
|
|
206
346
|
try { assertClaudeSessionCwd(session, expectedCwd); return true; }
|
|
@@ -221,13 +361,14 @@ registerTool(
|
|
|
221
361
|
title: "Send a message to a Claude session",
|
|
222
362
|
description:
|
|
223
363
|
"Send a message to a running Claude Code session's peer transport and wait for its reply. " +
|
|
364
|
+
"Only when the user requests a NEW conversation, target new prepares the native Desktop composer in expectedCwd; it requires user confirmation and returns a creation receipt, not a delivery receipt. " +
|
|
224
365
|
"A socket write alone does not confirm that Claude received the message. Set waitSec to 0 to send without confirmation. " +
|
|
225
366
|
"Every send is refused while earlier messages to that session still await replies, including waitSec 0; " +
|
|
226
367
|
"wait for those replies and read_claude_inbox before trying again. Desktop-only mode refuses CLI or unknown " +
|
|
227
368
|
"entrypoints, partial names, ambiguous targets, and a missing or mismatched expectedCwd or expectedTaskId before sending. Use target auto and expectedCwd for automatic current-account discovery; expectedTaskId is optional only when auto finds exactly one matching task. " +
|
|
228
369
|
"The sender's current permissions are verified per call; environment overrides and manual binding cannot bypass an unknown sender. It never creates a replacement session.",
|
|
229
370
|
inputSchema: {
|
|
230
|
-
target: z.string().describe("Use auto for current-account discovery in expectedCwd,
|
|
371
|
+
target: z.string().describe("Use auto for current-account discovery in expectedCwd, an exact session identity, or new to prepare a separate Desktop conversation requiring user confirmation"),
|
|
231
372
|
message: z.string().describe("The message text to deliver"),
|
|
232
373
|
expectedCwd: z.string().optional().describe("Exact absolute project directory independently verified by the caller; required in Desktop-only mode"),
|
|
233
374
|
expectedTaskId: z.string().optional().describe("Exact native Claude Desktop task ID from list_claude_sessions; verify its title in the app before sending"),
|
|
@@ -249,6 +390,12 @@ registerTool(
|
|
|
249
390
|
async ({ target, message, waitSec, expectedCwd, expectedTaskId }, extra) => {
|
|
250
391
|
try {
|
|
251
392
|
runtime.assertCurrent();
|
|
393
|
+
if (target === "new") {
|
|
394
|
+
if (expectedTaskId !== undefined) throw new Error("A new Claude conversation cannot target an existing task ID");
|
|
395
|
+
const sender = assertSender(extra?._meta);
|
|
396
|
+
const requestId = createHash("sha256").update(JSON.stringify([sender.threadId, sender.turnId, expectedCwd, message])).digest("hex");
|
|
397
|
+
return await startClaudeCreation({ requestId, cwd: expectedCwd, prompt: message }, extra?._meta);
|
|
398
|
+
}
|
|
252
399
|
const accounts = desktopOnly ? readBridgeAccounts() : null;
|
|
253
400
|
const selectedAccounts = desktopOnly ? requireBridgeAccounts(accounts) : null;
|
|
254
401
|
const found = desktopOnly ? resolveClaudeDesktopSession({ target, expectedCwd, expectedTaskId,
|
|
@@ -257,6 +404,7 @@ registerTool(
|
|
|
257
404
|
}) : findClaudeSession(target, { desktopOnly });
|
|
258
405
|
if (!found) return textResult(desktopOnly ? missingDesktopSession : `No live Claude session matches "${target}".`, true);
|
|
259
406
|
const session = desktopOnly ? { ...found, inbound: readClaudeInboundPolicy(found.cwd) } : withDesktopContext(found);
|
|
407
|
+
assertSessionRoot(session);
|
|
260
408
|
const selectedTaskId = target === "auto" ? session.desktop?.taskId : expectedTaskId;
|
|
261
409
|
if (desktopOnly || expectedCwd !== undefined) assertClaudeSessionCwd(session, expectedCwd);
|
|
262
410
|
if (desktopOnly) {
|
|
@@ -264,6 +412,10 @@ registerTool(
|
|
|
264
412
|
assertClaudeSessionProcess(session);
|
|
265
413
|
}
|
|
266
414
|
const sender = desktopOnly ? assertSender(extra?._meta) : null;
|
|
415
|
+
const scopeBindings = rootPolicy.enabled ? {
|
|
416
|
+
sender: rootPolicy.capture(sender?.cwd, "Codex sender working directory"),
|
|
417
|
+
recipient: rootPolicy.capture(session.cwd, "Claude recipient working directory"),
|
|
418
|
+
} : null;
|
|
267
419
|
if (desktopOnly) assertRecipientClass(session, sender);
|
|
268
420
|
await peer.start();
|
|
269
421
|
|
|
@@ -275,19 +427,28 @@ registerTool(
|
|
|
275
427
|
...(selectedAccounts ? { accountContext: selectedAccounts } : {}),
|
|
276
428
|
...(sender ? { permissionMode: sender.mode, replyThreadId: sender.threadId, senderReview: sender.review, senderApprovalPolicy: sender.approvalPolicy } : {}),
|
|
277
429
|
...(desktop ? { recipient: { permissionMode: session.desktop.permissionMode ?? null, permissionClass: session.desktop.permissionClass ?? null, inboundPolicy: session.inbound?.value ?? null } } : {}),
|
|
430
|
+
...(scopeBindings ? { scopeBindings } : {}),
|
|
278
431
|
beforeSend: () => {
|
|
279
432
|
runtime.assertCurrent();
|
|
280
433
|
if (desktopOnly) assertAccountIdentity(selectedAccounts);
|
|
281
434
|
const current = findClaudeSession(session.sessionId ?? String(session.pid), { desktopOnly });
|
|
282
|
-
|
|
435
|
+
const destinationUnchanged = desktopOnly
|
|
436
|
+
? sameClaudeDesktopRecipient(session, current)
|
|
437
|
+
: Boolean(current && current.pid === session.pid && current.socket === session.socket);
|
|
438
|
+
if (!destinationUnchanged) {
|
|
283
439
|
throw new Error("The Claude destination changed while this message was queued. No message was sent; inspect the existing Desktop session.");
|
|
284
440
|
}
|
|
285
441
|
if (desktopOnly || expectedCwd !== undefined) assertClaudeSessionCwd(current, expectedCwd);
|
|
442
|
+
assertSessionRoot(current);
|
|
286
443
|
if (desktopOnly) {
|
|
287
444
|
assertClaudeSessionProcess(current);
|
|
288
445
|
const refreshed = withDesktopContext(current, readClaudeAccountContext());
|
|
289
446
|
assertDesktopTask(refreshed, selectedTaskId);
|
|
290
447
|
const active = assertSender(extra?._meta);
|
|
448
|
+
if (rootPolicy.enabled) {
|
|
449
|
+
rootPolicy.recheck(scopeBindings.sender, "Codex sender working directory");
|
|
450
|
+
rootPolicy.recheck(scopeBindings.recipient, "Claude recipient working directory");
|
|
451
|
+
}
|
|
291
452
|
if (active.threadId !== sender.threadId || active.turnId !== sender.turnId || active.cwd !== sender.cwd || active.mode !== sender.mode || active.approvalPolicy !== sender.approvalPolicy || JSON.stringify(active.review) !== JSON.stringify(sender.review)) {
|
|
292
453
|
throw preflightFailure("CODEX_SENDER_CONTEXT_CHANGED", "The sender's active turn or permissions changed while this message was queued.");
|
|
293
454
|
}
|
|
@@ -339,11 +500,12 @@ registerTool(
|
|
|
339
500
|
"read_claude_delivery",
|
|
340
501
|
{
|
|
341
502
|
title: "Inspect a Claude message receipt without resending",
|
|
342
|
-
description: "Read the latest recipient control receipt or correlated reply for a message sent by this MCP process. Unknown IDs do not prove non-delivery; retain the original receipt after a reconnect and inspect the existing Claude session before any resend.",
|
|
503
|
+
description: "Read the latest recipient control receipt or correlated reply for a message sent by this MCP process. Also accepts the 64-character requestId returned by target new to inspect creation without reopening. Unknown IDs do not prove non-delivery; retain the original receipt after a reconnect and inspect the existing Claude session before any resend.",
|
|
343
504
|
inputSchema: { msgId: z.string().describe("The original message ID returned by send_to_claude_session") },
|
|
344
505
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
345
506
|
},
|
|
346
|
-
async ({ msgId }) => {
|
|
507
|
+
async ({ msgId }, extra) => {
|
|
508
|
+
if (/^[0-9a-f]{64}$/i.test(msgId)) return inspectClaudeCreation(msgId, extra?._meta);
|
|
347
509
|
const receipt = readReceipt(msgId);
|
|
348
510
|
if (!receipt) return textResult("This MCP process has no receipt for that message ID. It may belong to a previous process; do not infer failure or resend. Inspect the original Claude Desktop session.", true);
|
|
349
511
|
return { ...textResult(JSON.stringify(receipt, null, 2)), structuredContent: { receipt } };
|
|
@@ -371,7 +533,7 @@ registerTool(
|
|
|
371
533
|
try {
|
|
372
534
|
await peer.start();
|
|
373
535
|
const accounts = desktopOnly ? readBridgeAccounts() : null;
|
|
374
|
-
const messages = peer.drainInbox(limit ?? 20, (record) =>
|
|
536
|
+
const messages = peer.drainInbox(limit ?? 20, (record) => recordVisible(record, accounts));
|
|
375
537
|
if (!messages.length) return textResult(desktopOnly ? "No messages for the current accounts." : "Inbox is empty.");
|
|
376
538
|
const result = textResult(
|
|
377
539
|
messages
|
|
@@ -382,7 +544,8 @@ registerTool(
|
|
|
382
544
|
})
|
|
383
545
|
.join("\n\n"),
|
|
384
546
|
);
|
|
385
|
-
|
|
547
|
+
const remaining = peer.inbox.filter((record) => recordVisible(record, accounts)).length;
|
|
548
|
+
return { ...result, structuredContent: { messages: messages.map((record) => ({ ...record, forwarding: replyForwarder.read(record.inReplyTo ?? record.msgId) ?? record.forwardingError ?? null })), remaining } };
|
|
386
549
|
} catch (err) {
|
|
387
550
|
return failure(err);
|
|
388
551
|
}
|
|
@@ -407,10 +570,18 @@ registerTool(
|
|
|
407
570
|
try {
|
|
408
571
|
const session = findClaudeSession(target, { desktopOnly });
|
|
409
572
|
if (!session) return textResult(`No live Claude session matches "${target}".`, true);
|
|
573
|
+
const transcriptRoot = rootPolicy.enabled ? rootPolicy.capture(session.cwd, "Claude transcript working directory") : null;
|
|
574
|
+
assertSessionRoot(session);
|
|
410
575
|
if (desktopOnly) {
|
|
411
576
|
const verified = withDesktopContext(session);
|
|
412
577
|
if (verified.desktop?.status !== "matched") throw preflightFailure("CLAUDE_DESKTOP_TASK_UNVERIFIED", verified.desktop?.reason ?? "This task is not in the signed-in Claude account.");
|
|
413
578
|
}
|
|
579
|
+
if (transcriptRoot) {
|
|
580
|
+
const current = findClaudeSession(session.sessionId ?? String(session.pid), { desktopOnly });
|
|
581
|
+
if (!current || current.pid !== session.pid || current.socket !== session.socket) throw new Error("The Claude transcript target changed before it could be read");
|
|
582
|
+
rootPolicy.recheck(transcriptRoot, "Claude transcript working directory");
|
|
583
|
+
if (desktopOnly) assertClaudeSessionProcess(current);
|
|
584
|
+
}
|
|
414
585
|
const { file, messages } = readTranscript(session.sessionId, session.cwd, limit ?? 10);
|
|
415
586
|
if (!messages.length) return textResult(`No transcript entries found (looked at ${file}).`);
|
|
416
587
|
const body = messages.map((m) => `[${m.role}] ${m.text}`).join("\n\n");
|
|
@@ -474,7 +645,11 @@ registerTool(
|
|
|
474
645
|
const sessions = listClaudeSessions().filter((s) => s.pid !== process.pid);
|
|
475
646
|
const accounts = readBridgeAccounts();
|
|
476
647
|
const eligible = sessions.filter((session) => !desktopOnly || session.entrypoint === "claude-desktop")
|
|
648
|
+
.filter((session) => !rootPolicy.enabled || rootPolicy.allows(session.cwd))
|
|
477
649
|
.filter((session) => !desktopOnly || withDesktopContext(session, accounts.claude).desktop?.status === "matched");
|
|
650
|
+
const visibleInbox = peer.inbox.filter((record) => recordVisible(record, accounts));
|
|
651
|
+
const visiblePending = [...peer.pendingMessages.keys()].filter((id) => readReceipt(id));
|
|
652
|
+
const visibleForwarding = replyForwarder.status((record) => recordVisible(record, accounts));
|
|
478
653
|
const sender = desktopOnly ? readCodexSenderContext(extra?._meta) : null;
|
|
479
654
|
const lines = [
|
|
480
655
|
`platform: ${PLATFORM_LABEL} (${process.platform}/${process.arch})`,
|
|
@@ -489,17 +664,17 @@ registerTool(
|
|
|
489
664
|
`Claude account: ${accounts.claude.status}${accounts.claude.fingerprint ? ` (${accounts.claude.fingerprint.slice(0, 12)})` : ` - ${accounts.claude.reason}`}`,
|
|
490
665
|
`Codex account: ${accounts.codex.status}${accounts.codex.fingerprint ? ` (${accounts.codex.fingerprint.slice(0, 12)})` : ` - ${accounts.codex.reason}`}`,
|
|
491
666
|
`live sessions: ${eligible.length}`,
|
|
492
|
-
`excluded: ${sessions.length - eligible.length} non-Desktop or inactive-account session(s)
|
|
667
|
+
...(rootPolicy.enabled ? [] : [`excluded: ${sessions.length - eligible.length} non-Desktop or inactive-account session(s)`]),
|
|
493
668
|
`relay thread: ${forwarding.threadId ?? "(none - use bind_codex_thread)"}`,
|
|
494
669
|
`delivery: ${delivery.describe()}`,
|
|
495
|
-
`inbox: ${
|
|
496
|
-
`outstanding: ${
|
|
497
|
-
`reply forwarding: ${JSON.stringify(
|
|
498
|
-
...
|
|
670
|
+
`inbox: ${visibleInbox.length} pending message(s)`,
|
|
671
|
+
`outstanding: ${visiblePending.length} message(s) awaiting receipt or reply`,
|
|
672
|
+
`reply forwarding: ${JSON.stringify(visibleForwarding)}`,
|
|
673
|
+
...visiblePending.map((id) => `pending message: ${id} (${peer.readDelivery(id)?.status ?? "sent_unconfirmed"})`),
|
|
499
674
|
];
|
|
500
675
|
const state = runtime.status();
|
|
501
676
|
lines.push(`runtime pid: ${state.pid}`, `loaded source: ${state.revision}`, `disk source: ${state.diskRevision ?? "unreadable"}`, `runtime state: ${state.current ? "current" : `STALE - ${state.reason}; reconnect this MCP server in the existing task`}`);
|
|
502
|
-
return { ...textResult(lines.join("\n"), !state.current), structuredContent: { runtime: state, accounts: { claude: publicAccountState(accounts.claude), codex: publicAccountState(accounts.codex) }, replyForwarding:
|
|
677
|
+
return { ...textResult(lines.join("\n"), !state.current), structuredContent: { runtime: state, accounts: { claude: publicAccountState(accounts.claude), codex: publicAccountState(accounts.codex) }, replyForwarding: visibleForwarding, ...(sender ? { sender } : {}) } };
|
|
503
678
|
} catch (err) {
|
|
504
679
|
return failure(err);
|
|
505
680
|
}
|
|
@@ -133,22 +133,11 @@ export function readClaudeDesktopContext(session, { platform = process.platform,
|
|
|
133
133
|
(typeof session.bridgeSessionId !== "string" || !session.bridgeSessionId.trim())) {
|
|
134
134
|
return result("mismatch", "The live Claude bridge session identity is invalid.");
|
|
135
135
|
}
|
|
136
|
-
const directory = root ?? (account ? path.join(account.root, "claude-code-sessions") : sessionsRoot(platform, env));
|
|
137
136
|
let records;
|
|
138
137
|
try {
|
|
138
|
+
const directory = root ?? (account ? path.join(account.root, "claude-code-sessions") : sessionsRoot(platform, env));
|
|
139
139
|
if (!fs.existsSync(directory)) return result("missing", "Claude Desktop task metadata is not available on this host.");
|
|
140
|
-
|
|
141
|
-
const budget = { bytes: 0, versions: new Map(), contents: new Map() };
|
|
142
|
-
records = files.map((file) => readMetadata(file, budget));
|
|
143
|
-
const currentFiles = metadataFiles(directory, account?.accountId);
|
|
144
|
-
if (files.length !== currentFiles.length || files.some((file, index) => file !== currentFiles[index])) {
|
|
145
|
-
return result("mismatch", "Claude Desktop task metadata changed during inspection; inspect the existing task again.");
|
|
146
|
-
}
|
|
147
|
-
for (const file of files) {
|
|
148
|
-
const current = fs.lstatSync(file);
|
|
149
|
-
if (!current.isFile() || !sameVersion(current, budget.versions.get(file))) throw new Error("metadata changed");
|
|
150
|
-
if (!rereadMetadata(file).equals(budget.contents.get(file))) throw new Error("metadata changed");
|
|
151
|
-
}
|
|
140
|
+
records = readClaudeDesktopTasks({ platform, env, root, account });
|
|
152
141
|
} catch {
|
|
153
142
|
return result("mismatch", "Claude Desktop task metadata could not be read completely and consistently; no task identity is confirmed.");
|
|
154
143
|
}
|
|
@@ -202,3 +191,20 @@ export function readClaudeDesktopContext(session, { platform = process.platform,
|
|
|
202
191
|
accountFingerprint: account?.fingerprint,
|
|
203
192
|
});
|
|
204
193
|
}
|
|
194
|
+
|
|
195
|
+
export function readClaudeDesktopTasks({ platform = process.platform, env = process.env, root, account } = {}) {
|
|
196
|
+
if (account !== undefined && (account.status !== "verified" || !ACCOUNT_ID.test(account.accountId ?? "") || !account.root)) {
|
|
197
|
+
throw new Error("Claude Desktop account is not verified");
|
|
198
|
+
}
|
|
199
|
+
const directory = root ?? (account ? path.join(account.root, "claude-code-sessions") : sessionsRoot(platform, env));
|
|
200
|
+
const files = metadataFiles(directory, account?.accountId);
|
|
201
|
+
const budget = { bytes: 0, versions: new Map(), contents: new Map() };
|
|
202
|
+
const records = files.map((file) => readMetadata(file, budget));
|
|
203
|
+
const currentFiles = metadataFiles(directory, account?.accountId);
|
|
204
|
+
if (files.length !== currentFiles.length || files.some((file, index) => file !== currentFiles[index])) throw new Error("metadata changed");
|
|
205
|
+
for (const file of files) {
|
|
206
|
+
const current = fs.lstatSync(file);
|
|
207
|
+
if (!current.isFile() || !sameVersion(current, budget.versions.get(file)) || !rereadMetadata(file).equals(budget.contents.get(file))) throw new Error("metadata changed");
|
|
208
|
+
}
|
|
209
|
+
return records;
|
|
210
|
+
}
|