@minhspark/codex-mcp-bridge 1.16.0 → 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 +12 -0
- package/README.md +212 -574
- 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 +102 -16
- 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 +88 -11
- 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
|
@@ -20,11 +20,13 @@ import { ReplyForwarder } from "./reply-forwarder.mjs";
|
|
|
20
20
|
import { assertRoutingReload, clientReloadReason, createReloadControl } from "./reload-control.mjs";
|
|
21
21
|
import { readClaudeAccountContext } from "./desktop-account-context.mjs";
|
|
22
22
|
import { assertAccountIdentity, bindUnsolicitedClaudeMessageAccount, publicAccountState, readBridgeAccounts, requireBridgeAccounts, sameAccountIdentity } from "./bridge-account-context.mjs";
|
|
23
|
-
import { resolveClaudeDesktopSession } from "./claude-session-router.mjs";
|
|
23
|
+
import { resolveClaudeDesktopSession, sameClaudeDesktopRecipient } from "./claude-session-router.mjs";
|
|
24
|
+
import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
|
|
24
25
|
|
|
25
26
|
exitForVersionRequest(import.meta.url);
|
|
26
27
|
|
|
27
|
-
const VERSION = "1.16.
|
|
28
|
+
const VERSION = "1.16.1";
|
|
29
|
+
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
28
30
|
const FORWARD_MIN_INTERVAL_MS = 5000;
|
|
29
31
|
const FORWARD_MAX_PER_SESSION = 50;
|
|
30
32
|
|
|
@@ -32,6 +34,7 @@ const log = (msg) => process.stderr.write(`[claude-bridge] ${msg}\n`);
|
|
|
32
34
|
|
|
33
35
|
const defaultPeerName = process.env.CLAUDE_BRIDGE_PEER_NAME ?? `codex-${process.pid}`;
|
|
34
36
|
const desktopOnly = desktopTasksConfigured();
|
|
37
|
+
const rootPolicy = createHardenedRootPolicy();
|
|
35
38
|
const runtime = createRuntimeState({ configuration: desktopTasksConfigured });
|
|
36
39
|
|
|
37
40
|
const peer = new PeerEndpoint({
|
|
@@ -57,15 +60,34 @@ const forwarding = {
|
|
|
57
60
|
threadId: process.env.CODEX_THREAD_ID ?? null,
|
|
58
61
|
};
|
|
59
62
|
|
|
63
|
+
const creationRootBindings = new Map();
|
|
64
|
+
|
|
60
65
|
const creations = new ClaudeSessionCreation({
|
|
61
66
|
open: openClaudeCodeComposer,
|
|
62
|
-
listTasks: (account) => readClaudeDesktopTasks({ account }),
|
|
63
|
-
listSessions: () => listClaudeSessions(),
|
|
67
|
+
listTasks: (account) => readClaudeDesktopTasks({ account }).filter((task) => rootPolicy.allows(task.cwd)),
|
|
68
|
+
listSessions: () => listClaudeSessions().filter((session) => rootPolicy.allows(session.cwd)),
|
|
64
69
|
readContext: (session, account) => readClaudeDesktopContext(session, { account }),
|
|
65
|
-
readTranscript:
|
|
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
|
+
},
|
|
66
76
|
readAccount: readClaudeAccountContext,
|
|
67
77
|
assertProcess: assertClaudeSessionProcess,
|
|
68
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
|
+
}
|
|
69
91
|
|
|
70
92
|
const replyForwarder = new ReplyForwarder({
|
|
71
93
|
minIntervalMs: FORWARD_MIN_INTERVAL_MS,
|
|
@@ -75,16 +97,17 @@ const replyForwarder = new ReplyForwarder({
|
|
|
75
97
|
throw Object.assign(new Error("Bridge routing changed; the reply was not forwarded. Inspect the original receipt before reconnecting."), { code: "REPLY_ROUTING_CHANGED" });
|
|
76
98
|
}
|
|
77
99
|
if (desktopOnly) assertAccountIdentity(record.accountContext);
|
|
100
|
+
recheckScopeBindings(record.scopeBindings);
|
|
78
101
|
},
|
|
79
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}`,
|
|
80
|
-
desktopOnly ? { beforeSend: () => assertAccountIdentity(record.accountContext), accountContext: record.accountContext } : {}),
|
|
103
|
+
desktopOnly ? { beforeSend: () => { assertAccountIdentity(record.accountContext); recheckScopeBindings(record.scopeBindings); }, accountContext: record.accountContext } : {}),
|
|
81
104
|
});
|
|
82
105
|
|
|
83
106
|
const reload = createReloadControl({
|
|
84
107
|
entry: "claude-bridge.mjs",
|
|
85
108
|
inspect: () => peer.reloadReason() ?? replyForwarder.reloadReason() ?? clientReloadReason(codex),
|
|
86
109
|
quiesce: async () => { await peer.quiesce(); codex.close(); },
|
|
87
|
-
exportState: () => ({ desktopOnly, peer: peer.exportReloadState(), forwarding: replyForwarder.exportReloadState(), threadId: forwarding.threadId, creations: creations.exportState() }),
|
|
110
|
+
exportState: () => ({ desktopOnly, peer: peer.exportReloadState(), forwarding: replyForwarder.exportReloadState(), threadId: forwarding.threadId, creations: creations.exportState(), creationRootBindings: [...creationRootBindings] }),
|
|
88
111
|
restore: (state) => {
|
|
89
112
|
assertRoutingReload(state.desktopOnly, desktopOnly);
|
|
90
113
|
if (state.threadId !== null && (typeof state.threadId !== "string" || !state.threadId.trim())) {
|
|
@@ -94,6 +117,13 @@ const reload = createReloadControl({
|
|
|
94
117
|
replyForwarder.restoreReloadState(state.forwarding);
|
|
95
118
|
forwarding.threadId = state.threadId;
|
|
96
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
|
+
}
|
|
97
127
|
},
|
|
98
128
|
activate: () => peer.start(),
|
|
99
129
|
resume: () => peer.resume(),
|
|
@@ -102,6 +132,9 @@ const reload = createReloadControl({
|
|
|
102
132
|
function readReceipt(msgId) {
|
|
103
133
|
const receipt = peer.readDelivery(msgId);
|
|
104
134
|
if (desktopOnly && receipt && !sameAccountIdentity(receipt.accountContext, readBridgeAccounts())) return null;
|
|
135
|
+
if (rootPolicy.enabled) {
|
|
136
|
+
try { recheckScopeBindings(receipt?.scopeBindings); } catch { return null; }
|
|
137
|
+
}
|
|
105
138
|
return receipt ? { ...receipt, forwarding: replyForwarder.read(msgId) ?? receipt.forwardingError ?? null } : null;
|
|
106
139
|
}
|
|
107
140
|
|
|
@@ -130,6 +163,10 @@ function withDesktopContext(session, account = readClaudeAccountContext()) {
|
|
|
130
163
|
? { ...session, desktop: readClaudeDesktopContext(session, { account }), inbound: readClaudeInboundPolicy(session.cwd) } : session;
|
|
131
164
|
}
|
|
132
165
|
|
|
166
|
+
function assertSessionRoot(session, label = "Claude session") {
|
|
167
|
+
return rootPolicy.assert(session?.cwd, `${label} working directory`);
|
|
168
|
+
}
|
|
169
|
+
|
|
133
170
|
function assertDesktopTask(session, expectedTaskId) {
|
|
134
171
|
if (session.desktop?.status !== "matched") {
|
|
135
172
|
throw preflightFailure("CLAUDE_DESKTOP_TASK_UNVERIFIED", session.desktop?.reason ?? "The live session could not be matched to a Claude Desktop task.");
|
|
@@ -147,28 +184,44 @@ function assertSender(meta) {
|
|
|
147
184
|
return sender;
|
|
148
185
|
}
|
|
149
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
|
+
|
|
150
193
|
async function startClaudeCreation({ requestId, cwd, prompt }, meta) {
|
|
151
194
|
runtime.assertCurrent();
|
|
152
195
|
if (!desktopOnly) throw new Error("New Claude sessions require Desktop-only mode");
|
|
153
196
|
const sender = assertSender(meta);
|
|
154
197
|
const accounts = readBridgeAccounts();
|
|
155
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;
|
|
156
204
|
const receipt = await creations.start({ requestId, cwd, prompt, account: accounts.claude, senderThreadId: sender.threadId,
|
|
157
205
|
beforeOpen: () => {
|
|
158
206
|
runtime.assertCurrent();
|
|
159
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");
|
|
160
210
|
const current = assertSender(meta);
|
|
161
211
|
if (JSON.stringify(current) !== JSON.stringify(sender)) throw new Error("The calling Codex turn changed before opening Claude Desktop");
|
|
162
212
|
},
|
|
163
213
|
});
|
|
214
|
+
if (rootPolicy.enabled) creationRootBindings.set(requestId, scopeBindings);
|
|
164
215
|
return { ...textResult(JSON.stringify(receipt, null, 2)), structuredContent: { creation: receipt } };
|
|
165
216
|
}
|
|
166
217
|
|
|
167
218
|
async function inspectClaudeCreation(requestId, meta) {
|
|
168
219
|
const sender = assertSender(meta);
|
|
220
|
+
assertCreationRoots(requestId, sender);
|
|
169
221
|
const accounts = readBridgeAccounts();
|
|
170
222
|
requireBridgeAccounts(accounts);
|
|
171
223
|
const receipt = await creations.inspect(requestId, { account: accounts.claude, senderThreadId: sender.threadId });
|
|
224
|
+
assertCreationRoots(requestId, sender);
|
|
172
225
|
return { ...textResult(JSON.stringify(receipt, null, 2)), structuredContent: { creation: receipt } };
|
|
173
226
|
}
|
|
174
227
|
|
|
@@ -184,6 +237,9 @@ function forwardToCodexThread(record) {
|
|
|
184
237
|
}
|
|
185
238
|
|
|
186
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; }
|
|
187
243
|
if (desktopOnly) bindUnsolicitedClaudeMessageAccount(record, {
|
|
188
244
|
hasPendingSource: (socket) => [...peer.pendingMessages.values()].some((pending) => pending.targetSocket === socket),
|
|
189
245
|
});
|
|
@@ -252,6 +308,7 @@ registerTool(
|
|
|
252
308
|
},
|
|
253
309
|
async ({ requestId }, extra) => {
|
|
254
310
|
const sender = assertSender(extra?._meta);
|
|
311
|
+
assertCreationRoots(requestId, sender);
|
|
255
312
|
const accounts = readBridgeAccounts();
|
|
256
313
|
requireBridgeAccounts(accounts);
|
|
257
314
|
const receipt = await creations.abandon(requestId, { account: accounts.claude, senderThreadId: sender.threadId });
|
|
@@ -277,11 +334,13 @@ registerTool(
|
|
|
277
334
|
},
|
|
278
335
|
async ({ includeDead, expectedCwd }) => {
|
|
279
336
|
try {
|
|
337
|
+
if (rootPolicy.enabled && expectedCwd !== undefined) rootPolicy.assert(expectedCwd, "Requested working directory");
|
|
280
338
|
if (expectedCwd !== undefined) assertClaudeSessionCwd({ cwd: expectedCwd }, expectedCwd);
|
|
281
339
|
const account = readClaudeAccountContext();
|
|
282
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: [] } };
|
|
283
341
|
const sessions = listClaudeSessions({ includeDead: includeDead ?? false }).filter(
|
|
284
342
|
(s) => s.pid !== process.pid && (!desktopOnly || s.entrypoint === "claude-desktop"),
|
|
343
|
+
).filter((session) => !rootPolicy.enabled || rootPolicy.allows(session.cwd)
|
|
285
344
|
).filter((session) => {
|
|
286
345
|
if (expectedCwd === undefined) return true;
|
|
287
346
|
try { assertClaudeSessionCwd(session, expectedCwd); return true; }
|
|
@@ -345,6 +404,7 @@ registerTool(
|
|
|
345
404
|
}) : findClaudeSession(target, { desktopOnly });
|
|
346
405
|
if (!found) return textResult(desktopOnly ? missingDesktopSession : `No live Claude session matches "${target}".`, true);
|
|
347
406
|
const session = desktopOnly ? { ...found, inbound: readClaudeInboundPolicy(found.cwd) } : withDesktopContext(found);
|
|
407
|
+
assertSessionRoot(session);
|
|
348
408
|
const selectedTaskId = target === "auto" ? session.desktop?.taskId : expectedTaskId;
|
|
349
409
|
if (desktopOnly || expectedCwd !== undefined) assertClaudeSessionCwd(session, expectedCwd);
|
|
350
410
|
if (desktopOnly) {
|
|
@@ -352,6 +412,10 @@ registerTool(
|
|
|
352
412
|
assertClaudeSessionProcess(session);
|
|
353
413
|
}
|
|
354
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;
|
|
355
419
|
if (desktopOnly) assertRecipientClass(session, sender);
|
|
356
420
|
await peer.start();
|
|
357
421
|
|
|
@@ -363,19 +427,28 @@ registerTool(
|
|
|
363
427
|
...(selectedAccounts ? { accountContext: selectedAccounts } : {}),
|
|
364
428
|
...(sender ? { permissionMode: sender.mode, replyThreadId: sender.threadId, senderReview: sender.review, senderApprovalPolicy: sender.approvalPolicy } : {}),
|
|
365
429
|
...(desktop ? { recipient: { permissionMode: session.desktop.permissionMode ?? null, permissionClass: session.desktop.permissionClass ?? null, inboundPolicy: session.inbound?.value ?? null } } : {}),
|
|
430
|
+
...(scopeBindings ? { scopeBindings } : {}),
|
|
366
431
|
beforeSend: () => {
|
|
367
432
|
runtime.assertCurrent();
|
|
368
433
|
if (desktopOnly) assertAccountIdentity(selectedAccounts);
|
|
369
434
|
const current = findClaudeSession(session.sessionId ?? String(session.pid), { desktopOnly });
|
|
370
|
-
|
|
435
|
+
const destinationUnchanged = desktopOnly
|
|
436
|
+
? sameClaudeDesktopRecipient(session, current)
|
|
437
|
+
: Boolean(current && current.pid === session.pid && current.socket === session.socket);
|
|
438
|
+
if (!destinationUnchanged) {
|
|
371
439
|
throw new Error("The Claude destination changed while this message was queued. No message was sent; inspect the existing Desktop session.");
|
|
372
440
|
}
|
|
373
441
|
if (desktopOnly || expectedCwd !== undefined) assertClaudeSessionCwd(current, expectedCwd);
|
|
442
|
+
assertSessionRoot(current);
|
|
374
443
|
if (desktopOnly) {
|
|
375
444
|
assertClaudeSessionProcess(current);
|
|
376
445
|
const refreshed = withDesktopContext(current, readClaudeAccountContext());
|
|
377
446
|
assertDesktopTask(refreshed, selectedTaskId);
|
|
378
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
|
+
}
|
|
379
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)) {
|
|
380
453
|
throw preflightFailure("CODEX_SENDER_CONTEXT_CHANGED", "The sender's active turn or permissions changed while this message was queued.");
|
|
381
454
|
}
|
|
@@ -460,7 +533,7 @@ registerTool(
|
|
|
460
533
|
try {
|
|
461
534
|
await peer.start();
|
|
462
535
|
const accounts = desktopOnly ? readBridgeAccounts() : null;
|
|
463
|
-
const messages = peer.drainInbox(limit ?? 20, (record) =>
|
|
536
|
+
const messages = peer.drainInbox(limit ?? 20, (record) => recordVisible(record, accounts));
|
|
464
537
|
if (!messages.length) return textResult(desktopOnly ? "No messages for the current accounts." : "Inbox is empty.");
|
|
465
538
|
const result = textResult(
|
|
466
539
|
messages
|
|
@@ -471,7 +544,8 @@ registerTool(
|
|
|
471
544
|
})
|
|
472
545
|
.join("\n\n"),
|
|
473
546
|
);
|
|
474
|
-
|
|
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 } };
|
|
475
549
|
} catch (err) {
|
|
476
550
|
return failure(err);
|
|
477
551
|
}
|
|
@@ -496,10 +570,18 @@ registerTool(
|
|
|
496
570
|
try {
|
|
497
571
|
const session = findClaudeSession(target, { desktopOnly });
|
|
498
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);
|
|
499
575
|
if (desktopOnly) {
|
|
500
576
|
const verified = withDesktopContext(session);
|
|
501
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.");
|
|
502
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
|
+
}
|
|
503
585
|
const { file, messages } = readTranscript(session.sessionId, session.cwd, limit ?? 10);
|
|
504
586
|
if (!messages.length) return textResult(`No transcript entries found (looked at ${file}).`);
|
|
505
587
|
const body = messages.map((m) => `[${m.role}] ${m.text}`).join("\n\n");
|
|
@@ -563,7 +645,11 @@ registerTool(
|
|
|
563
645
|
const sessions = listClaudeSessions().filter((s) => s.pid !== process.pid);
|
|
564
646
|
const accounts = readBridgeAccounts();
|
|
565
647
|
const eligible = sessions.filter((session) => !desktopOnly || session.entrypoint === "claude-desktop")
|
|
648
|
+
.filter((session) => !rootPolicy.enabled || rootPolicy.allows(session.cwd))
|
|
566
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));
|
|
567
653
|
const sender = desktopOnly ? readCodexSenderContext(extra?._meta) : null;
|
|
568
654
|
const lines = [
|
|
569
655
|
`platform: ${PLATFORM_LABEL} (${process.platform}/${process.arch})`,
|
|
@@ -578,17 +664,17 @@ registerTool(
|
|
|
578
664
|
`Claude account: ${accounts.claude.status}${accounts.claude.fingerprint ? ` (${accounts.claude.fingerprint.slice(0, 12)})` : ` - ${accounts.claude.reason}`}`,
|
|
579
665
|
`Codex account: ${accounts.codex.status}${accounts.codex.fingerprint ? ` (${accounts.codex.fingerprint.slice(0, 12)})` : ` - ${accounts.codex.reason}`}`,
|
|
580
666
|
`live sessions: ${eligible.length}`,
|
|
581
|
-
`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)`]),
|
|
582
668
|
`relay thread: ${forwarding.threadId ?? "(none - use bind_codex_thread)"}`,
|
|
583
669
|
`delivery: ${delivery.describe()}`,
|
|
584
|
-
`inbox: ${
|
|
585
|
-
`outstanding: ${
|
|
586
|
-
`reply forwarding: ${JSON.stringify(
|
|
587
|
-
...
|
|
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"})`),
|
|
588
674
|
];
|
|
589
675
|
const state = runtime.status();
|
|
590
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`}`);
|
|
591
|
-
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 } : {}) } };
|
|
592
678
|
} catch (err) {
|
|
593
679
|
return failure(err);
|
|
594
680
|
}
|
|
@@ -23,6 +23,19 @@ function uniqueLiveSessions(sessions) {
|
|
|
23
23
|
return [...unique.values()];
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export function sameClaudeDesktopRecipient(selected, current) {
|
|
27
|
+
return Boolean(
|
|
28
|
+
selected && current &&
|
|
29
|
+
selected.pid === current.pid &&
|
|
30
|
+
typeof selected.sessionId === "string" && selected.sessionId.length > 0 &&
|
|
31
|
+
selected.sessionId === current.sessionId &&
|
|
32
|
+
typeof selected.socket === "string" && selected.socket.length > 0 &&
|
|
33
|
+
selected.socket === current.socket &&
|
|
34
|
+
typeof selected.processStart === "string" && selected.processStart.length > 0 &&
|
|
35
|
+
selected.processStart === current.processStart
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
export function resolveClaudeDesktopSession({ target, expectedCwd, expectedTaskId, sessions, account, readContext }) {
|
|
27
40
|
if (account?.status !== "verified") {
|
|
28
41
|
throw preflightFailure("CLAUDE_ACCOUNT_UNVERIFIED", account?.reason ?? "The current Claude Desktop account is not confirmed.");
|
|
@@ -53,6 +66,9 @@ export function resolveClaudeDesktopSession({ target, expectedCwd, expectedTaskI
|
|
|
53
66
|
if (matches.length > 1) {
|
|
54
67
|
throw preflightFailure("CLAUDE_SESSION_AMBIGUOUS", "The exact target identifies multiple live sessions; use an unambiguous sessionId or pid.");
|
|
55
68
|
}
|
|
69
|
+
if (!matches.length) {
|
|
70
|
+
throw preflightFailure("CLAUDE_SESSION_NOT_FOUND", "No live Claude Desktop session matches the exact target. Reinspect the existing task and use its current sessionId or pid.");
|
|
71
|
+
}
|
|
56
72
|
if (matches.length === 1) {
|
|
57
73
|
const found = matches[0];
|
|
58
74
|
if (found.entrypoint !== "claude-desktop") {
|