@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
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const REQUEST_ID = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-f]{32,128})$/i;
|
|
5
|
+
const PENDING = new Set(["awaiting_user", "launch_uncertain", "ambiguous", "awaiting_project_confirmation"]);
|
|
6
|
+
const STATUSES = new Set([...PENDING, "created", "abandoned"]);
|
|
7
|
+
const MAX_REQUESTS = 32;
|
|
8
|
+
const MAX_TASKS = 8192;
|
|
9
|
+
|
|
10
|
+
function fail(code, message) {
|
|
11
|
+
throw Object.assign(new Error(message), { code });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function string(value, max = 1024) {
|
|
15
|
+
return typeof value === "string" && value.length > 0 && value.length <= max && !value.includes("\0");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function accountIdentity(account) {
|
|
19
|
+
if (account?.status !== "verified" || !string(account.fingerprint) || !string(account.root, 8192)) {
|
|
20
|
+
fail("CLAUDE_ACCOUNT_UNVERIFIED", "The current Claude Desktop account must be verified.");
|
|
21
|
+
}
|
|
22
|
+
return { fingerprint: account.fingerprint, root: account.root };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sameAccount(left, right) {
|
|
26
|
+
return left.fingerprint === right.fingerprint && left.root === right.root;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function augmentedPrompt(prompt, requestId) {
|
|
30
|
+
if (!string(prompt, 14000) || !prompt.trim()) fail("CLAUDE_CREATION_PROMPT_INVALID", "Provide a nonempty prompt of at most 14000 characters.");
|
|
31
|
+
const submitted = `${prompt.trim()}\n\n[Codex creation request: ${requestId}]`;
|
|
32
|
+
if (submitted.length > 14000) fail("CLAUDE_CREATION_PROMPT_TOO_LONG", "The prompt including its correlation marker exceeds 14000 characters.");
|
|
33
|
+
return submitted;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function matchesInitialMessage(text, expected) {
|
|
37
|
+
if (text === expected) return true;
|
|
38
|
+
if (typeof text !== "string" || !/^<system-reminder>\r?\nThe user started this session without choosing a project folder,/.test(text)) return false;
|
|
39
|
+
const closing = text.indexOf("</system-reminder>");
|
|
40
|
+
if (closing < 0 || closing > 32768) return false;
|
|
41
|
+
const body = text.slice("<system-reminder>".length, closing);
|
|
42
|
+
if (body.includes("<system-reminder") || body.includes("</system-reminder")) return false;
|
|
43
|
+
const suffix = text.slice(closing + "</system-reminder>".length);
|
|
44
|
+
const separator = /^\r?\n\r?\n/.exec(suffix);
|
|
45
|
+
return separator !== null && suffix.slice(separator[0].length) === expected;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function receipt(request) {
|
|
49
|
+
return structuredClone({ requestId: request.requestId, status: request.status, cwd: request.cwd, promptSubmitted: request.status === "created" || request.submissionObserved === true, reason: request.reason, ...(request.observedCwd ? { observedCwd: request.observedCwd } : {}), ...(request.sessionId ? { sessionId: request.sessionId, taskId: request.taskId, title: request.title } : {}) });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class ClaudeSessionCreation {
|
|
53
|
+
constructor({ open, listTasks, listSessions, readContext, readTranscript, readAccount, assertProcess, realpath = (directory) => {
|
|
54
|
+
const canonical = fs.realpathSync.native(directory);
|
|
55
|
+
if (!fs.statSync(canonical).isDirectory()) throw new Error("Not a directory");
|
|
56
|
+
return canonical;
|
|
57
|
+
}, platform = process.platform }) {
|
|
58
|
+
for (const [name, dependency] of Object.entries({ open, listTasks, listSessions, readContext, readTranscript, readAccount, assertProcess, realpath })) {
|
|
59
|
+
if (typeof dependency !== "function") throw new TypeError(`${name} must be a function`);
|
|
60
|
+
}
|
|
61
|
+
Object.assign(this, { open, listTasks, listSessions, readContext, readTranscript, readAccount, assertProcess, realpath, platform });
|
|
62
|
+
this.requests = new Map();
|
|
63
|
+
this.queue = Promise.resolve();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async canonical(directory) {
|
|
67
|
+
const paths = this.platform === "win32" ? path.win32 : path.posix;
|
|
68
|
+
if (!string(directory, 8192) || !paths.isAbsolute(directory)) fail("CLAUDE_CREATION_CWD_INVALID", "The project directory must be an existing absolute directory.");
|
|
69
|
+
let canonical;
|
|
70
|
+
try { canonical = await this.realpath(directory); } catch { fail("CLAUDE_CREATION_CWD_INVALID", "The project directory does not exist or cannot be resolved."); }
|
|
71
|
+
if (!string(canonical, 8192) || !paths.isAbsolute(canonical)) fail("CLAUDE_CREATION_CWD_INVALID", "The canonical project directory is invalid.");
|
|
72
|
+
return canonical;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
sameCwd(left, right) {
|
|
76
|
+
return this.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async checkAccount(account, expected) {
|
|
80
|
+
const provided = accountIdentity(account);
|
|
81
|
+
const current = accountIdentity(await this.readAccount());
|
|
82
|
+
if (!sameAccount(provided, current) || (expected && !sameAccount(provided, expected))) fail("CLAUDE_CREATION_ACCOUNT_CHANGED", "Claude Desktop account changed; this creation request cannot be used in another account.");
|
|
83
|
+
return provided;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
start(args) {
|
|
87
|
+
const pending = this.queue.then(() => this.startLocked(args));
|
|
88
|
+
this.queue = pending.catch(() => {});
|
|
89
|
+
return pending;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async startLocked({ requestId, cwd, prompt, account, senderThreadId, beforeOpen }) {
|
|
93
|
+
if (!REQUEST_ID.test(requestId ?? "") || typeof requestId !== "string") fail("CLAUDE_CREATION_REQUEST_INVALID", "requestId must be a UUID or 32–128 hexadecimal characters.");
|
|
94
|
+
if (!string(senderThreadId, 256)) fail("CLAUDE_CREATION_SENDER_INVALID", "A verified sender thread identity is required.");
|
|
95
|
+
const identity = await this.checkAccount(account);
|
|
96
|
+
const canonical = await this.canonical(cwd);
|
|
97
|
+
const initialMessage = augmentedPrompt(prompt, requestId);
|
|
98
|
+
const prior = this.requests.get(requestId);
|
|
99
|
+
if (prior) {
|
|
100
|
+
if (!sameAccount(identity, prior.account) || prior.senderThreadId !== senderThreadId || !this.sameCwd(prior.cwd, canonical) || prior.prompt !== prompt) {
|
|
101
|
+
fail("CLAUDE_CREATION_REQUEST_CONFLICT", "This requestId is already bound to a different creation payload.");
|
|
102
|
+
}
|
|
103
|
+
return receipt(prior);
|
|
104
|
+
}
|
|
105
|
+
if (this.requests.size >= MAX_REQUESTS) fail("CLAUDE_CREATION_LIMIT", "The creation receipt limit has been reached; existing requests remain available for inspection.");
|
|
106
|
+
for (const existing of this.requests.values()) {
|
|
107
|
+
if (sameAccount(existing.account, identity) && PENDING.has(existing.status)) fail("CLAUDE_CREATION_PENDING", "A creation request is already pending for this account; inspect or explicitly abandon that request before opening another project in the shared composer.");
|
|
108
|
+
}
|
|
109
|
+
const tasks = await this.listTasks(account);
|
|
110
|
+
if (!Array.isArray(tasks) || tasks.length > MAX_TASKS || tasks.some((task) => !string(task?.taskId, 256) || (task.fileTaskId !== undefined && task.fileTaskId !== task.taskId))) fail("CLAUDE_CREATION_TASKS_INVALID", "The existing native task baseline cannot be verified.");
|
|
111
|
+
let existingProject = false;
|
|
112
|
+
for (const task of tasks) {
|
|
113
|
+
if (task.isArchived !== false) continue;
|
|
114
|
+
try { if (this.sameCwd(await this.canonical(task.cwd), canonical)) existingProject = true; } catch {}
|
|
115
|
+
}
|
|
116
|
+
if (!existingProject) fail("CLAUDE_CREATION_PROJECT_NOT_FOUND", "The canonical directory must already belong to a nonarchived Claude Desktop task.");
|
|
117
|
+
const url = `claude://code/new?q=${encodeURIComponent(initialMessage)}&folder=${encodeURIComponent(canonical)}`;
|
|
118
|
+
if (url.length > 30000) fail("CLAUDE_CREATION_URL_TOO_LONG", "The encoded Claude Desktop link exceeds 30000 characters.");
|
|
119
|
+
await this.checkAccount(account, identity);
|
|
120
|
+
if (beforeOpen !== undefined) {
|
|
121
|
+
if (typeof beforeOpen !== "function") fail("CLAUDE_CREATION_PREFLIGHT_INVALID", "beforeOpen must be a function.");
|
|
122
|
+
await beforeOpen();
|
|
123
|
+
}
|
|
124
|
+
const request = { requestId, cwd: canonical, prompt, initialMessage, account: identity, senderThreadId, baseline: [...new Set(tasks.map((task) => task.taskId))], status: "launch_uncertain", reason: "The Desktop launch outcome is uncertain; inspect this request before taking further action." };
|
|
125
|
+
this.requests.set(requestId, request);
|
|
126
|
+
try {
|
|
127
|
+
await this.open(url);
|
|
128
|
+
request.status = "awaiting_user";
|
|
129
|
+
request.reason = "Claude Desktop was asked to prefill a new session. Confirm the folder if prompted and press Send; no prompt has been submitted by the bridge.";
|
|
130
|
+
} catch {
|
|
131
|
+
request.reason = "The Desktop launcher failed or returned an uncertain outcome. This request will not be launched again; inspect it to check whether the session was created.";
|
|
132
|
+
}
|
|
133
|
+
return receipt(request);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async inspect(requestId, { account, senderThreadId }) {
|
|
137
|
+
const request = this.requests.get(requestId);
|
|
138
|
+
if (!request) fail("CLAUDE_CREATION_REQUEST_NOT_FOUND", "No retained creation request has this requestId.");
|
|
139
|
+
if (request.senderThreadId !== senderThreadId) fail("CLAUDE_CREATION_SENDER_MISMATCH", "This creation request belongs to another sender thread.");
|
|
140
|
+
await this.checkAccount(account, request.account);
|
|
141
|
+
if (request.status === "created") return receipt(request);
|
|
142
|
+
const sessions = await this.listSessions(account);
|
|
143
|
+
if (!Array.isArray(sessions) || sessions.length > MAX_TASKS) fail("CLAUDE_CREATION_SESSIONS_INVALID", "Live native sessions could not be inspected within the limit.");
|
|
144
|
+
const matches = new Map();
|
|
145
|
+
for (const session of sessions) {
|
|
146
|
+
if (!session?.alive || session.entrypoint !== "claude-desktop" || !string(session.sessionId, 256)) continue;
|
|
147
|
+
try {
|
|
148
|
+
const observedCwd = await this.canonical(session.cwd);
|
|
149
|
+
if (await this.assertProcess(session) === false) continue;
|
|
150
|
+
const context = await this.readContext(session, account);
|
|
151
|
+
if (context?.status !== "matched" || context.isArchived || !string(context.taskId, 256) || request.baseline.includes(context.taskId)) continue;
|
|
152
|
+
if (context.fileTaskId !== undefined && context.fileTaskId !== context.taskId) continue;
|
|
153
|
+
if (context.accountFingerprint && context.accountFingerprint !== request.account.fingerprint) continue;
|
|
154
|
+
if (!this.sameCwd(await this.canonical(context.cwd), observedCwd)) continue;
|
|
155
|
+
const transcript = await this.readTranscript(session.sessionId, observedCwd, 1000);
|
|
156
|
+
if (transcript?.truncated || transcript?.incomplete) continue;
|
|
157
|
+
const firstUser = transcript?.messages?.find((message) => message.role === "user");
|
|
158
|
+
if (!matchesInitialMessage(firstUser?.text, request.initialMessage)) continue;
|
|
159
|
+
if (await this.assertProcess(session) === false) continue;
|
|
160
|
+
matches.set(JSON.stringify([context.taskId, session.sessionId]), { sessionId: session.sessionId, taskId: context.taskId, title: string(context.title, 65536) && context.title.trim() ? context.title.trim().slice(0, 4096) : null, observedCwd });
|
|
161
|
+
} catch {}
|
|
162
|
+
}
|
|
163
|
+
await this.checkAccount(account, request.account);
|
|
164
|
+
if (matches.size > 1) {
|
|
165
|
+
request.submissionObserved = true;
|
|
166
|
+
request.ambiguitySeen = true;
|
|
167
|
+
request.status = "ambiguous";
|
|
168
|
+
for (const key of ["sessionId", "taskId", "title", "observedCwd"]) delete request[key];
|
|
169
|
+
request.reason = "Multiple new native sessions contain the exact creation message; no session was selected.";
|
|
170
|
+
} else if (matches.size === 1 && request.status !== "ambiguous" && !request.ambiguitySeen) {
|
|
171
|
+
const match = [...matches.values()][0];
|
|
172
|
+
const projectConfirmed = this.sameCwd(match.observedCwd, request.cwd);
|
|
173
|
+
Object.assign(request, match, { submissionObserved: true, status: projectConfirmed ? "created" : "awaiting_project_confirmation", reason: projectConfirmed ? "A new live native Claude Desktop task has the exact initial user message, verified project, account, and process identity." : "The prompt was submitted in a new native Claude Desktop task, but its actual directory differs from the requested project. Select or adopt the requested project folder in Claude Desktop and inspect this request again; project creation is not confirmed." });
|
|
174
|
+
}
|
|
175
|
+
return receipt(request);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
abandon(requestId, args) {
|
|
179
|
+
const pending = this.queue.then(() => this.abandonLocked(requestId, args));
|
|
180
|
+
this.queue = pending.catch(() => {});
|
|
181
|
+
return pending;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async abandonLocked(requestId, { account, senderThreadId }) {
|
|
185
|
+
const request = this.requests.get(requestId);
|
|
186
|
+
if (!request) fail("CLAUDE_CREATION_REQUEST_NOT_FOUND", "No retained creation request has this requestId.");
|
|
187
|
+
if (request.senderThreadId !== senderThreadId) fail("CLAUDE_CREATION_SENDER_MISMATCH", "This creation request belongs to another sender thread.");
|
|
188
|
+
await this.checkAccount(account, request.account);
|
|
189
|
+
if (request.status === "created") return receipt(request);
|
|
190
|
+
if (request.status === "ambiguous") request.ambiguitySeen = true;
|
|
191
|
+
request.status = "abandoned";
|
|
192
|
+
request.reason = "The creation request was abandoned and its composer lock released. Claude Desktop and its composer may still be open, and the prompt could still be submitted later. The receipt is retained for inspection and will never relaunch.";
|
|
193
|
+
return receipt(request);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
exportState() {
|
|
197
|
+
return structuredClone({ version: 1, requests: [...this.requests.values()] });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
restoreState(state) {
|
|
201
|
+
if (this.requests.size || state?.version !== 1 || !Array.isArray(state.requests) || state.requests.length > MAX_REQUESTS) fail("CLAUDE_CREATION_STATE_INVALID", "Creation state cannot be restored.");
|
|
202
|
+
const restored = new Map();
|
|
203
|
+
const paths = this.platform === "win32" ? path.win32 : path.posix;
|
|
204
|
+
for (const request of state.requests) {
|
|
205
|
+
if (!request || typeof request.requestId !== "string" || !REQUEST_ID.test(request.requestId) || restored.has(request.requestId) || !string(request.cwd, 8192) || !paths.isAbsolute(request.cwd) || !string(request.senderThreadId, 256) || !string(request.account?.fingerprint) || !string(request.account?.root, 8192) || !STATUSES.has(request.status) || !string(request.reason, 4096) || !Array.isArray(request.baseline) || request.baseline.length > MAX_TASKS || request.baseline.some((id) => !string(id, 256)) || request.initialMessage !== augmentedPrompt(request.prompt, request.requestId)) fail("CLAUDE_CREATION_STATE_INVALID", "A retained creation receipt is invalid.");
|
|
206
|
+
const hasSession = ["created", "awaiting_project_confirmation"].includes(request.status) || request.sessionId !== undefined;
|
|
207
|
+
if (hasSession && (!string(request.sessionId, 256) || !string(request.taskId, 256) || request.baseline.includes(request.taskId) || (request.title !== null && !string(request.title, 4096)))) fail("CLAUDE_CREATION_STATE_INVALID", "A submitted creation receipt is invalid.");
|
|
208
|
+
if (request.submissionObserved !== undefined && typeof request.submissionObserved !== "boolean") fail("CLAUDE_CREATION_STATE_INVALID", "A retained submission marker is invalid.");
|
|
209
|
+
if ((request.status === "awaiting_project_confirmation" || request.observedCwd !== undefined) && (!string(request.observedCwd, 8192) || !paths.isAbsolute(request.observedCwd))) fail("CLAUDE_CREATION_STATE_INVALID", "A retained observed directory is invalid.");
|
|
210
|
+
if (request.status === "awaiting_project_confirmation" && (request.submissionObserved !== true || this.sameCwd(request.cwd, request.observedCwd))) fail("CLAUDE_CREATION_STATE_INVALID", "A pending project confirmation is invalid.");
|
|
211
|
+
if (request.ambiguitySeen !== undefined && typeof request.ambiguitySeen !== "boolean") fail("CLAUDE_CREATION_STATE_INVALID", "A retained ambiguity marker is invalid.");
|
|
212
|
+
restored.set(request.requestId, structuredClone({ requestId: request.requestId, cwd: request.cwd, prompt: request.prompt, initialMessage: request.initialMessage, account: { fingerprint: request.account.fingerprint, root: request.account.root }, senderThreadId: request.senderThreadId, baseline: request.baseline, status: request.status, reason: request.reason, ...(request.ambiguitySeen || request.status === "ambiguous" ? { ambiguitySeen: true } : {}), ...(request.submissionObserved ? { submissionObserved: true } : {}), ...(request.observedCwd ? { observedCwd: request.observedCwd } : {}), ...(hasSession ? { sessionId: request.sessionId, taskId: request.taskId, title: request.title } : {}) }));
|
|
213
|
+
}
|
|
214
|
+
this.requests = restored;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -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") {
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
7
|
+
const MAX_ROLLOUT_BYTES = 16 * 1024 * 1024;
|
|
8
|
+
const MAX_ENTRIES = 100000;
|
|
9
|
+
const MAX_DIRECTORIES = 4096;
|
|
10
|
+
const MAX_REPLY_BYTES = 1024 * 1024;
|
|
11
|
+
const COMPLETED = new Set(["task_complete", "task_completed", "turn_complete", "turn_completed"]);
|
|
12
|
+
const STARTED = new Set(["task_started", "turn_started"]);
|
|
13
|
+
const FAILED = new Set(["task_aborted", "turn_aborted", "task_failed", "turn_failed"]);
|
|
14
|
+
|
|
15
|
+
function object(value) {
|
|
16
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function unavailable(reason) {
|
|
20
|
+
return { status: "unavailable", reason: reason?.message ?? String(reason) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function configuredSessions(env) {
|
|
24
|
+
const configuredHome = env.CODEX_HOME || path.join(env.HOME || env.USERPROFILE || os.homedir(), ".codex");
|
|
25
|
+
if (!path.isAbsolute(configuredHome)) throw new Error("The configured Codex home must be absolute");
|
|
26
|
+
const homeStatus = fs.lstatSync(configuredHome);
|
|
27
|
+
if (!homeStatus.isDirectory() || homeStatus.isSymbolicLink()) throw new Error("The configured Codex home is not a regular directory");
|
|
28
|
+
const sessions = path.join(configuredHome, "sessions");
|
|
29
|
+
const status = fs.lstatSync(sessions);
|
|
30
|
+
if (!status.isDirectory() || status.isSymbolicLink()) throw new Error("The Codex sessions path is not a regular directory");
|
|
31
|
+
const canonicalHome = fs.realpathSync.native(configuredHome);
|
|
32
|
+
const canonicalSessions = fs.realpathSync.native(sessions);
|
|
33
|
+
const relative = path.relative(canonicalHome, canonicalSessions);
|
|
34
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error("The Codex sessions path escapes the configured home");
|
|
35
|
+
return canonicalSessions;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function findRollout(threadId, env) {
|
|
39
|
+
if (!UUID.test(threadId)) throw new Error("The Codex task identity is invalid");
|
|
40
|
+
const sessions = configuredSessions(env);
|
|
41
|
+
const queue = [{ directory: sessions, depth: 0 }];
|
|
42
|
+
const matches = [];
|
|
43
|
+
let entries = 0;
|
|
44
|
+
let directories = 0;
|
|
45
|
+
while (queue.length) {
|
|
46
|
+
const { directory, depth } = queue.pop();
|
|
47
|
+
if (++directories > MAX_DIRECTORIES) throw new Error("The bounded Codex sessions scan exceeded its directory limit");
|
|
48
|
+
const children = fs.readdirSync(directory, { withFileTypes: true });
|
|
49
|
+
entries += children.length;
|
|
50
|
+
if (entries > MAX_ENTRIES) throw new Error("The bounded Codex sessions scan exceeded its entry limit");
|
|
51
|
+
for (const child of children) {
|
|
52
|
+
const candidate = path.join(directory, child.name);
|
|
53
|
+
if (depth < 3 && (depth === 0 ? /^\d{4}$/ : /^\d{2}$/).test(child.name)) {
|
|
54
|
+
if (child.isSymbolicLink()) throw new Error("The Codex sessions scan encountered a linked date directory");
|
|
55
|
+
if (child.isDirectory()) {
|
|
56
|
+
const resolved = fs.realpathSync.native(candidate);
|
|
57
|
+
const relative = path.relative(sessions, resolved);
|
|
58
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error("A Codex date directory escapes the sessions path");
|
|
59
|
+
queue.push({ directory: resolved, depth: depth + 1 });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (depth === 3 && child.name.startsWith("rollout-") && child.name.endsWith(`-${threadId}.jsonl`)) matches.push(candidate);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (matches.length !== 1) throw new Error(matches.length ? "Multiple rollouts match the Codex task" : "No rollout matches the Codex task");
|
|
66
|
+
const status = fs.lstatSync(matches[0]);
|
|
67
|
+
if (!status.isFile() || status.isSymbolicLink()) throw new Error("The Codex rollout is not a regular file");
|
|
68
|
+
return { file: matches[0], sessions };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function pathIdentity(candidate, kind) {
|
|
72
|
+
const status = fs.lstatSync(candidate);
|
|
73
|
+
if ((kind === "file" ? !status.isFile() : !status.isDirectory()) || status.isSymbolicLink()) throw new Error(`The Codex rollout ${kind === "file" ? "file" : "ancestor"} is not regular`);
|
|
74
|
+
return { path: candidate, dev: status.dev, ino: status.ino };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function capturePathBoundary(file, sessions) {
|
|
78
|
+
const resolvedFile = fs.realpathSync.native(file);
|
|
79
|
+
const relative = path.relative(sessions, resolvedFile);
|
|
80
|
+
if (relative.startsWith("..") || path.isAbsolute(relative) || path.relative(file, resolvedFile)) throw new Error("The Codex rollout path escapes its sessions directory");
|
|
81
|
+
const ancestors = [];
|
|
82
|
+
let current = path.dirname(file);
|
|
83
|
+
for (;;) {
|
|
84
|
+
const resolved = fs.realpathSync.native(current);
|
|
85
|
+
const within = path.relative(sessions, resolved);
|
|
86
|
+
if (within.startsWith("..") || path.isAbsolute(within) || path.relative(current, resolved)) throw new Error("A Codex rollout ancestor redirects outside its sessions directory");
|
|
87
|
+
ancestors.push(pathIdentity(current, "directory"));
|
|
88
|
+
if (!path.relative(sessions, current)) break;
|
|
89
|
+
const parent = path.dirname(current);
|
|
90
|
+
if (parent === current) throw new Error("The Codex rollout path has no sessions ancestor");
|
|
91
|
+
current = parent;
|
|
92
|
+
}
|
|
93
|
+
return { file: pathIdentity(file, "file"), ancestors };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function samePathBoundary(expected, current) {
|
|
97
|
+
return expected.file.dev === current.file.dev && expected.file.ino === current.file.ino &&
|
|
98
|
+
expected.ancestors.length === current.ancestors.length && expected.ancestors.every((entry, index) =>
|
|
99
|
+
entry.path === current.ancestors[index].path && entry.dev === current.ancestors[index].dev && entry.ino === current.ancestors[index].ino);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function readStable(found, maxBytes) {
|
|
103
|
+
const { file, sessions } = found;
|
|
104
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > MAX_ROLLOUT_BYTES) throw new Error("The Codex rollout read limit is invalid");
|
|
105
|
+
const beforeBoundary = capturePathBoundary(file, sessions);
|
|
106
|
+
const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
107
|
+
try {
|
|
108
|
+
const before = fs.fstatSync(descriptor);
|
|
109
|
+
if (before.dev !== beforeBoundary.file.dev || before.ino !== beforeBoundary.file.ino) throw new Error("The Codex rollout changed while opening");
|
|
110
|
+
if (!before.isFile() || before.size === 0 || before.size > maxBytes) throw new Error("The Codex rollout is empty or exceeds the bounded read limit");
|
|
111
|
+
const data = Buffer.alloc(before.size);
|
|
112
|
+
let offset = 0;
|
|
113
|
+
while (offset < data.length) {
|
|
114
|
+
const count = fs.readSync(descriptor, data, offset, data.length - offset, offset);
|
|
115
|
+
if (!count) throw new Error("The Codex rollout changed while reading");
|
|
116
|
+
offset += count;
|
|
117
|
+
}
|
|
118
|
+
const after = fs.fstatSync(descriptor);
|
|
119
|
+
const afterBoundary = capturePathBoundary(file, sessions);
|
|
120
|
+
if (!samePathBoundary(beforeBoundary, afterBoundary) || before.size !== after.size || before.mtimeMs !== after.mtimeMs ||
|
|
121
|
+
before.ctimeMs !== after.ctimeMs || before.ino !== after.ino || before.dev !== after.dev ||
|
|
122
|
+
before.ino !== afterBoundary.file.ino || before.dev !== afterBoundary.file.dev) throw new Error("The Codex rollout changed while reading");
|
|
123
|
+
if (data.at(-1) !== 0x0a) throw new Error("The Codex rollout has an incomplete final record");
|
|
124
|
+
return { data, identity: { dev: before.dev, ino: before.ino } };
|
|
125
|
+
} finally {
|
|
126
|
+
fs.closeSync(descriptor);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseRecords(data) {
|
|
131
|
+
const records = [];
|
|
132
|
+
let offset = 0;
|
|
133
|
+
while (offset < data.length) {
|
|
134
|
+
const newline = data.indexOf(0x0a, offset);
|
|
135
|
+
if (newline < 0) throw new Error("The Codex rollout has an incomplete final record");
|
|
136
|
+
const raw = data.subarray(offset, newline).toString("utf8");
|
|
137
|
+
const start = offset;
|
|
138
|
+
offset = newline + 1;
|
|
139
|
+
if (!raw) continue;
|
|
140
|
+
const record = JSON.parse(raw);
|
|
141
|
+
if (!object(record) || !object(record.payload)) throw new Error("The Codex rollout contains an invalid record");
|
|
142
|
+
records.push({ record, start });
|
|
143
|
+
}
|
|
144
|
+
return records;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function canonicalDirectory(value, label) {
|
|
148
|
+
if (typeof value !== "string" || !path.isAbsolute(value)) throw new Error(`${label} is missing or invalid`);
|
|
149
|
+
const status = fs.lstatSync(value);
|
|
150
|
+
if (!status.isDirectory() || status.isSymbolicLink()) throw new Error(`${label} is not a regular directory`);
|
|
151
|
+
return fs.realpathSync.native(value);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function validateSession(records, threadId, expectedCwd) {
|
|
155
|
+
const sessions = records.filter(({ record }) => record.type === "session_meta").map(({ record }) => record.payload);
|
|
156
|
+
if (sessions.length !== 1) throw new Error("The Codex rollout has ambiguous session identity");
|
|
157
|
+
const session = sessions[0];
|
|
158
|
+
if (session.id !== threadId || session.originator !== "Codex Desktop" || session.source !== "vscode") throw new Error("The rollout does not confirm the exact native Codex Desktop task");
|
|
159
|
+
if (canonicalDirectory(session.cwd, "The rollout workspace") !== expectedCwd) throw new Error("The rollout workspace does not match the selected native task");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function delegationOutput(executorThreadId, prompt) {
|
|
163
|
+
return `<codex_delegation>\n <source_thread_id>${executorThreadId}</source_thread_id>\n <input>${prompt}</input>\n</codex_delegation>`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function normalizedText(value) {
|
|
167
|
+
return value.replace(/\r\n?/g, "\n").normalize("NFC");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function replyHash(text) {
|
|
171
|
+
return crypto.createHash("sha256").update(normalizedText(text), "utf8").digest("hex");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function recordBelongsToTurn(entry, threadId, turnId) {
|
|
175
|
+
const payload = entry.record.payload;
|
|
176
|
+
const metadata = object(payload.internal_chat_message_metadata_passthrough) ? payload.internal_chat_message_metadata_passthrough : {};
|
|
177
|
+
const turnFields = [payload.turn_id, payload.root_turn_id, metadata.turn_id, metadata.root_turn_id].filter((value) => value !== undefined);
|
|
178
|
+
if (!turnFields.includes(turnId)) return false;
|
|
179
|
+
if (turnFields.some((value) => value !== turnId)) throw new Error("The native response contains contradictory turn identity");
|
|
180
|
+
const threadFields = [payload.thread_id, metadata.thread_id].filter((value) => value !== undefined);
|
|
181
|
+
if (threadFields.some((value) => value !== threadId)) throw new Error("The native response contains contradictory task identity");
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function responseItemText(payload) {
|
|
186
|
+
if (!Array.isArray(payload.content) || payload.content.some((item) => !object(item) || item.type !== "output_text" || typeof item.text !== "string")) {
|
|
187
|
+
throw new Error("The native final response contains unsupported content");
|
|
188
|
+
}
|
|
189
|
+
return payload.content.map((item) => item.text).join("");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function completedEventText(item) {
|
|
193
|
+
if (!object(item) || item.type !== "AgentMessage" || item.phase !== "final_answer") return null;
|
|
194
|
+
if (!Array.isArray(item.content) || item.content.some((part) => !object(part) || part.type !== "Text" || typeof part.text !== "string")) {
|
|
195
|
+
throw new Error("The native completed assistant item contains unsupported content");
|
|
196
|
+
}
|
|
197
|
+
return item.content.map((part) => part.text).join("");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function inspectTurnRecords(records, threadId, turnId, cwd, { requireDispatch } = {}) {
|
|
201
|
+
const turns = records.filter((entry) => recordBelongsToTurn(entry, threadId, turnId));
|
|
202
|
+
const starts = turns.filter(({ record }) => record.type === "event_msg" && STARTED.has(record.payload.type));
|
|
203
|
+
const contexts = turns.filter(({ record }) => record.type === "turn_context");
|
|
204
|
+
const completions = turns.filter(({ record }) => record.type === "event_msg" && COMPLETED.has(record.payload.type));
|
|
205
|
+
const failures = turns.filter(({ record }) => record.type === "event_msg" &&
|
|
206
|
+
(FAILED.has(record.payload.type) || record.payload.error || ["failed", "aborted", "interrupted"].includes(record.payload.status)));
|
|
207
|
+
if (failures.length || starts.length !== 1 || contexts.length !== 1 || completions.length !== 1 ||
|
|
208
|
+
canonicalDirectory(contexts[0].record.payload.cwd, "The observed turn workspace") !== cwd ||
|
|
209
|
+
starts[0].start >= contexts[0].start || contexts[0].start >= completions[0].start) {
|
|
210
|
+
throw new Error("The native turn lifecycle is missing, ambiguous, failed, or out of order");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let dispatch = null;
|
|
214
|
+
if (requireDispatch) {
|
|
215
|
+
const dispatches = turns.filter(({ record }) => record.type === "response_item" && record.payload.type === "function_call_output" &&
|
|
216
|
+
record.payload.namespace === "codex_app" && record.payload.name === "send_message_to_thread");
|
|
217
|
+
if (dispatches.length !== 1 || dispatches[0].record.payload.output !== requireDispatch.output ||
|
|
218
|
+
contexts[0].start >= dispatches[0].start || dispatches[0].start >= completions[0].start) {
|
|
219
|
+
throw new Error("The newly observed turn is not correlated to the exact native dispatch");
|
|
220
|
+
}
|
|
221
|
+
dispatch = dispatches[0];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const allAssistantEntries = turns.filter(({ record }) => record.type === "response_item" && record.payload.type === "message" && record.payload.role === "assistant");
|
|
225
|
+
const finalEntries = allAssistantEntries.filter(({ record }) => record.payload.phase === "final_answer");
|
|
226
|
+
if (!finalEntries.length && allAssistantEntries.length) throw new Error("The completed native turn has assistant items but no final assistant reply");
|
|
227
|
+
const assistantItems = [];
|
|
228
|
+
const ids = new Set();
|
|
229
|
+
for (const entry of finalEntries) {
|
|
230
|
+
const { payload } = entry.record;
|
|
231
|
+
if (typeof payload.id !== "string" || !payload.id.trim() || ids.has(payload.id) ||
|
|
232
|
+
entry.start <= (dispatch?.start ?? contexts[0].start) || entry.start >= completions[0].start) {
|
|
233
|
+
throw new Error("The native final response item identity is missing, duplicated, or out of order");
|
|
234
|
+
}
|
|
235
|
+
ids.add(payload.id);
|
|
236
|
+
const text = responseItemText(payload);
|
|
237
|
+
if (!text.trim()) throw new Error("The native final response item is empty");
|
|
238
|
+
assistantItems.push({ id: payload.id, text });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const eventItems = new Map();
|
|
242
|
+
for (const { record, start } of turns) {
|
|
243
|
+
if (record.type !== "event_msg" || record.payload.type !== "item_completed") continue;
|
|
244
|
+
const eventItem = record.payload.item;
|
|
245
|
+
if (ids.has(eventItem?.id) && (eventItem.type !== "AgentMessage" || eventItem.phase !== "final_answer")) {
|
|
246
|
+
throw new Error("The native assistant item representations conflict in type or phase");
|
|
247
|
+
}
|
|
248
|
+
const text = completedEventText(record.payload.item);
|
|
249
|
+
if (text === null) continue;
|
|
250
|
+
const id = record.payload.item.id;
|
|
251
|
+
if (typeof id !== "string" || !id.trim() || eventItems.has(id) || start <= contexts[0].start || start >= completions[0].start) {
|
|
252
|
+
throw new Error("The native completed assistant item identity is missing, duplicated, or out of order");
|
|
253
|
+
}
|
|
254
|
+
eventItems.set(id, text);
|
|
255
|
+
}
|
|
256
|
+
for (const item of assistantItems) {
|
|
257
|
+
if (eventItems.has(item.id) && eventItems.get(item.id) !== item.text) throw new Error("The native assistant item representations conflict");
|
|
258
|
+
}
|
|
259
|
+
if ([...eventItems.keys()].some((id) => !ids.has(id))) throw new Error("The native completed assistant item has no matching authoritative response item");
|
|
260
|
+
|
|
261
|
+
const text = assistantItems.map((item) => item.text).join("\n\n");
|
|
262
|
+
if (Buffer.byteLength(text, "utf8") > MAX_REPLY_BYTES) throw new Error("The native final response exceeds the bounded reply limit");
|
|
263
|
+
return {
|
|
264
|
+
status: assistantItems.length ? "completed" : "completed_no_reply",
|
|
265
|
+
threadId,
|
|
266
|
+
turnId,
|
|
267
|
+
source: "codex_desktop_rollout",
|
|
268
|
+
assistantItems,
|
|
269
|
+
text,
|
|
270
|
+
replySha256: replyHash(text),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function captureCodexRolloutWatermark({ threadId, expectedCwd }, { env = process.env, maxRolloutBytes = MAX_ROLLOUT_BYTES } = {}) {
|
|
275
|
+
try {
|
|
276
|
+
const cwd = canonicalDirectory(expectedCwd, "The selected native task workspace");
|
|
277
|
+
const found = findRollout(threadId, env);
|
|
278
|
+
const snapshot = readStable(found, maxRolloutBytes);
|
|
279
|
+
const records = parseRecords(snapshot.data);
|
|
280
|
+
validateSession(records, threadId, cwd);
|
|
281
|
+
return {
|
|
282
|
+
status: "available",
|
|
283
|
+
threadId,
|
|
284
|
+
cwd,
|
|
285
|
+
file: found.file,
|
|
286
|
+
size: snapshot.data.length,
|
|
287
|
+
prefixSha256: crypto.createHash("sha256").update(snapshot.data).digest("hex"),
|
|
288
|
+
identity: snapshot.identity,
|
|
289
|
+
};
|
|
290
|
+
} catch (error) {
|
|
291
|
+
return unavailable(error);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function inspectCodexNativeTurn({ threadId, turnId, expectedCwd }, { env = process.env, maxRolloutBytes = MAX_ROLLOUT_BYTES } = {}) {
|
|
296
|
+
try {
|
|
297
|
+
if (!UUID.test(threadId) || !UUID.test(turnId)) throw new Error("The native turn identity is invalid");
|
|
298
|
+
const cwd = canonicalDirectory(expectedCwd, "The selected native task workspace");
|
|
299
|
+
const found = findRollout(threadId, env);
|
|
300
|
+
const snapshot = readStable(found, maxRolloutBytes);
|
|
301
|
+
const records = parseRecords(snapshot.data);
|
|
302
|
+
validateSession(records, threadId, cwd);
|
|
303
|
+
return inspectTurnRecords(records, threadId, turnId, cwd);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
return { ...unavailable(error), threadId, turnId, source: "codex_desktop_rollout", assistantItems: [], text: "", replySha256: null };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function readCodexNativeTurnResponse({ threadId, turnId, previousTurnId, expectedCwd, executorThreadId, prompt, watermark }, { env = process.env, maxRolloutBytes = MAX_ROLLOUT_BYTES } = {}) {
|
|
310
|
+
try {
|
|
311
|
+
if (!UUID.test(threadId) || !UUID.test(turnId) || !UUID.test(executorThreadId)) throw new Error("The native response identity is invalid");
|
|
312
|
+
if (turnId === previousTurnId) throw new Error("The observed turn was already present before dispatch");
|
|
313
|
+
if (typeof prompt !== "string" || !prompt.length) throw new Error("The exact dispatched prompt is unavailable");
|
|
314
|
+
if (!object(watermark) || watermark.status !== "available" || watermark.threadId !== threadId) throw new Error("No trusted pre-send rollout watermark is available");
|
|
315
|
+
const cwd = canonicalDirectory(expectedCwd, "The selected native task workspace");
|
|
316
|
+
if (watermark.cwd !== cwd) throw new Error("The selected native task workspace changed after dispatch");
|
|
317
|
+
const found = findRollout(threadId, env);
|
|
318
|
+
if (found.file !== watermark.file) throw new Error("The selected native task rollout changed after dispatch");
|
|
319
|
+
const snapshot = readStable(found, maxRolloutBytes);
|
|
320
|
+
if (snapshot.identity.dev !== watermark.identity?.dev || snapshot.identity.ino !== watermark.identity?.ino || snapshot.data.length < watermark.size) throw new Error("The selected native task rollout identity changed after dispatch");
|
|
321
|
+
const prefix = snapshot.data.subarray(0, watermark.size);
|
|
322
|
+
if (crypto.createHash("sha256").update(prefix).digest("hex") !== watermark.prefixSha256) throw new Error("The pre-send Codex rollout history changed after dispatch");
|
|
323
|
+
const records = parseRecords(snapshot.data);
|
|
324
|
+
validateSession(records, threadId, cwd);
|
|
325
|
+
const tail = records.filter(({ start }) => start >= watermark.size);
|
|
326
|
+
const inspected = inspectTurnRecords(tail, threadId, turnId, cwd, {
|
|
327
|
+
requireDispatch: { output: delegationOutput(executorThreadId, prompt) },
|
|
328
|
+
});
|
|
329
|
+
return { ...inspected, observationStatus: inspected.status };
|
|
330
|
+
} catch (error) {
|
|
331
|
+
return { ...unavailable(error), threadId, turnId, source: "codex_desktop_rollout", assistantItems: [], text: "", replySha256: null };
|
|
332
|
+
}
|
|
333
|
+
}
|