@minhspark/codex-mcp-bridge 1.25.3 → 1.26.0
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/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +31 -0
- package/README.md +45 -20
- package/docs/new-macos-setup.md +47 -21
- package/docs/new-windows-setup.md +34 -9
- package/docs/project-onboarding.md +21 -8
- package/package.json +2 -1
- package/plugin.json +1 -1
- package/scripts/background-relay-hidden.vbs +11 -0
- package/scripts/install-antigravity-bridge.mjs +26 -14
- package/scripts/install-background-relay.mjs +428 -0
- package/scripts/install-native-relay.mjs +1 -1
- package/scripts/setup-background-relay.mjs +58 -0
- package/scripts/worker-relay.mjs +41 -0
- package/src/antigravity-bridge.mjs +18 -3
- package/src/antigravity-cli.mjs +13 -2
- package/src/antigravity-outbound-bridge.mjs +27 -6
- package/src/antigravity-outbound-targets.mjs +59 -4
- package/src/background-antigravity-worker.mjs +192 -0
- package/src/background-claude-worker.mjs +240 -0
- package/src/background-codex-worker.mjs +344 -0
- package/src/background-relay-client.mjs +107 -0
- package/src/background-relay-config.mjs +98 -0
- package/src/background-relay-daemon.mjs +60 -0
- package/src/background-relay-mcp.mjs +45 -0
- package/src/background-relay-protocol.mjs +54 -0
- package/src/background-relay-registration.mjs +198 -0
- package/src/background-relay-service.mjs +311 -0
- package/src/background-relay-storage.mjs +203 -0
- package/src/background-relay-storage.ps1 +64 -0
- package/src/background-relay.mjs +373 -0
- package/src/claude-bridge.mjs +6 -3
- package/src/index.mjs +7 -2
- package/src/mcp-supervisor.mjs +17 -11
- package/src/native-relay-companion.mjs +1 -1
- package/src/reload-control.mjs +2 -2
- package/src/setup.mjs +26 -9
- package/src/windows-pipe-acl.ps1 +1 -1
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { execFile, spawn as spawnProcess } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { StringDecoder } from "node:string_decoder";
|
|
7
|
+
|
|
8
|
+
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}$/i;
|
|
9
|
+
const FRAME_LIMIT = 2 * 1024 * 1024;
|
|
10
|
+
const OUTPUT_LIMIT = 16 * 1024 * 1024;
|
|
11
|
+
const PROBE_LIMIT = 256 * 1024;
|
|
12
|
+
const TOOLS = ["worker_status", "list_workers", "send_to_worker", "read_worker_delivery"];
|
|
13
|
+
const API_CODES = new Set(["authentication_failed", "oauth_org_not_allowed", "account_on_hold", "billing_error", "rate_limit", "overloaded", "invalid_request", "model_not_found", "server_error", "max_output_tokens", "cloud_credential_error", "unknown"]);
|
|
14
|
+
const STARTUP_CODES = new Set(["org_pin_api_key_conflict", "provider_not_allowed", "org_verify_failed", "org_pin_mismatch", "managed_settings_invalid", "remote_settings_required_unavailable", "gateway_signin_required", "gateway_access_denied", "proxy_invalid", "temp_dir_unusable", "cwd_unavailable", "shell_tool_missing", "session_held_by_background", "worktree_resume_refused", "worktree_unverified", "cli_version_too_old", "bypass_root", "org_config_required_unavailable", "org_config_refused"]);
|
|
15
|
+
|
|
16
|
+
function failure(code, message, phase, deliveryState = "not_sent") {
|
|
17
|
+
return Object.assign(new Error(message), { code, phase, deliveryState, sent: deliveryState === "unknown" ? null : deliveryState === "sent" });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function executablePath(executable, env, platform = process.platform) {
|
|
21
|
+
const selected = executable ?? env.CLAUDE_CLI_BIN;
|
|
22
|
+
const windows = platform === "win32";
|
|
23
|
+
const home = env.USERPROFILE ?? env.HOME ?? os.homedir();
|
|
24
|
+
const name = windows ? "claude.exe" : "claude";
|
|
25
|
+
const candidates = selected === undefined ? [
|
|
26
|
+
path.join(home, ".local", "bin", name),
|
|
27
|
+
...(windows ? [path.join(env.APPDATA ?? path.join(home, "AppData", "Roaming"), "npm", "node_modules", "@anthropic-ai", "claude-code", "bin", name)] : []),
|
|
28
|
+
...(env.PATH ?? env.Path ?? "").split(path.delimiter).filter((entry) => path.isAbsolute(entry)).map((entry) => path.join(entry, name)),
|
|
29
|
+
] : [selected];
|
|
30
|
+
for (const candidate of candidates) {
|
|
31
|
+
if (typeof candidate !== "string" || !path.isAbsolute(candidate) || /(?:^|[\\/])(?:AnthropicClaude|Claude\.app|Claude_[^\\/]*)(?:[\\/]|$)/i.test(candidate)) continue;
|
|
32
|
+
try {
|
|
33
|
+
const resolved = fs.realpathSync.native(candidate);
|
|
34
|
+
if (/(?:^|[\\/])(?:AnthropicClaude|Claude\.app|Claude_[^\\/]*)(?:[\\/]|$)/i.test(resolved)
|
|
35
|
+
|| windows && !/\.exe$/i.test(resolved) || !fs.statSync(resolved).isFile()) continue;
|
|
36
|
+
fs.accessSync(resolved, windows ? fs.constants.R_OK : fs.constants.X_OK);
|
|
37
|
+
return resolved;
|
|
38
|
+
} catch {}
|
|
39
|
+
}
|
|
40
|
+
throw failure("CLAUDE_CLI_UNAVAILABLE", "Claude Code CLI executable was not found; configure its absolute native executable path.", "preflight");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function runFile(executable, args, options) {
|
|
44
|
+
return new Promise((resolve, reject) => execFile(executable, args, options, (error, stdout, stderr) => {
|
|
45
|
+
if (error) reject(Object.assign(error, { stdout, stderr }));
|
|
46
|
+
else resolve({ stdout, stderr });
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function probe(run, executable, args, env) {
|
|
51
|
+
let result;
|
|
52
|
+
try { result = await run(executable, args, { env, windowsHide: true, shell: false, timeout: 15000, maxBuffer: PROBE_LIMIT, encoding: "utf8" }); }
|
|
53
|
+
catch (error) { result = { stdout: error.stdout, status: typeof error.code === "number" ? error.code : 1 }; }
|
|
54
|
+
const stdout = String(result?.stdout ?? "");
|
|
55
|
+
return { stdout: Buffer.byteLength(stdout) <= PROBE_LIMIT ? stdout : "", status: result?.status ?? result?.exitCode ?? (result?.error ? 1 : 0) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function inspectClaudeWorker({ env = process.env, executable, platform = process.platform, run = runFile } = {}) {
|
|
59
|
+
const state = { agent: "claude", available: false, authenticated: null, ready: false };
|
|
60
|
+
try { state.executable = executablePath(executable, env, platform); }
|
|
61
|
+
catch (error) { return { ...state, code: error.code, reason: error.message }; }
|
|
62
|
+
const version = await probe(run, state.executable, ["--version"], env);
|
|
63
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-[\w.-]+)?\s+\(Claude Code\)\s*$/.exec(version.stdout.trim());
|
|
64
|
+
if (version.status !== 0 || !match) return { ...state, code: "CLAUDE_CLI_PROBE_FAILED", reason: "The executable did not report a supported Claude Code CLI identity." };
|
|
65
|
+
state.available = true;
|
|
66
|
+
state.version = `${match[1]}.${match[2]}.${match[3]}`;
|
|
67
|
+
if (Number(match[1]) < 2 || Number(match[1]) === 2 && (Number(match[2]) < 1 || Number(match[2]) === 1 && Number(match[3]) < 259)) {
|
|
68
|
+
return { ...state, code: "CLAUDE_CLI_UNSUPPORTED_VERSION", reason: "Claude Code CLI 2.1.259 or later is required for unattended permission handling." };
|
|
69
|
+
}
|
|
70
|
+
const auth = await probe(run, state.executable, ["auth", "status", "--json"], env);
|
|
71
|
+
let parsed;
|
|
72
|
+
try { parsed = JSON.parse(auth.stdout); } catch {}
|
|
73
|
+
if (parsed?.loggedIn === false) return { ...state, authenticated: false, code: "CLAUDE_CLI_AUTH_REQUIRED", reason: "Claude Code CLI is not signed in. Complete its official CLI login; Desktop sign-in is a separate readiness check." };
|
|
74
|
+
if (auth.status !== 0 || parsed?.loggedIn !== true) return { ...state, code: "CLAUDE_CLI_AUTH_CHECK_FAILED", reason: "Claude Code CLI authentication status could not be verified." };
|
|
75
|
+
return { ...state, authenticated: true, ready: true };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function workerInput({ executable, cwd, prompt, requestId, sessionId, env, relayMcp, timeoutMs, onEvent }) {
|
|
79
|
+
const binary = executablePath(executable, env);
|
|
80
|
+
if (typeof cwd !== "string" || !path.isAbsolute(cwd)) throw failure("CLAUDE_WORKER_INVALID_CWD", "The worker requires an absolute authorized working directory.", "preflight");
|
|
81
|
+
let directory;
|
|
82
|
+
try { directory = fs.realpathSync.native(cwd); if (!fs.statSync(directory).isDirectory()) throw new Error(); }
|
|
83
|
+
catch { throw failure("CLAUDE_WORKER_INVALID_CWD", "The worker working directory is unavailable.", "preflight"); }
|
|
84
|
+
if (typeof prompt !== "string" || !prompt.trim() || Buffer.byteLength(prompt) > FRAME_LIMIT / 2) throw failure("CLAUDE_WORKER_INVALID_PROMPT", "The worker prompt must be non-empty text within its size limit.", "preflight");
|
|
85
|
+
if (!UUID.test(requestId ?? "") || !UUID.test(sessionId ?? "")) throw failure("CLAUDE_WORKER_INVALID_ID", "The worker requires independent UUID request and session identities.", "preflight");
|
|
86
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 86400000 || typeof onEvent !== "function") throw failure("CLAUDE_WORKER_INVALID_OPTIONS", "Worker lifecycle options are invalid.", "preflight");
|
|
87
|
+
if (!relayMcp || typeof relayMcp.command !== "string" || !path.isAbsolute(relayMcp.command) || relayMcp.command.includes("\0")
|
|
88
|
+
|| !Array.isArray(relayMcp.args) || relayMcp.args.some((value) => typeof value !== "string" || value.includes("\0"))
|
|
89
|
+
|| !relayMcp.env || typeof relayMcp.env !== "object" || Array.isArray(relayMcp.env)
|
|
90
|
+
|| Object.entries(relayMcp.env).some(([key, value]) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || typeof value !== "string" || value.includes("\0"))) {
|
|
91
|
+
throw failure("CLAUDE_WORKER_INVALID_MCP", "The worker requires a valid relay-only MCP registration.", "preflight");
|
|
92
|
+
}
|
|
93
|
+
const childEnv = { ...env, ...relayMcp.env };
|
|
94
|
+
for (const key of ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "CLAUDE_CODE_SESSION_ID", "CLAUDE_CODE_PARENT_SESSION_ID"]) delete childEnv[key];
|
|
95
|
+
childEnv.CLAUDE_CODE_STARTUP_FAILURE_RESULTS = "1";
|
|
96
|
+
const args = ["-p", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose", "--replay-user-messages", "--session-id", sessionId,
|
|
97
|
+
"--no-session-persistence", "--strict-mcp-config", "--mcp-config", JSON.stringify({ mcpServers: { "worker-relay": { command: relayMcp.command, args: relayMcp.args } } }),
|
|
98
|
+
"--permission-prompts", "none", "--no-chrome", "--settings", JSON.stringify({ disableAllHooks: true }),
|
|
99
|
+
"--allowedTools", TOOLS.map((name) => `mcp__worker-relay__${name}`).join(",")];
|
|
100
|
+
const frame = `${JSON.stringify({ type: "user", uuid: requestId, session_id: sessionId, message: { role: "user", content: prompt }, parent_tool_use_id: null, client_composed: true, priority: "later" })}\n`;
|
|
101
|
+
if (Buffer.byteLength(frame) > FRAME_LIMIT) throw failure("CLAUDE_WORKER_INVALID_PROMPT", "The encoded worker prompt exceeds its size limit.", "preflight");
|
|
102
|
+
return { binary, directory, childEnv, args, frame };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function runClaudeWorker({ executable, cwd, prompt, requestId, sessionId = randomUUID(), env = process.env, relayMcp, onEvent = () => {}, signal, timeoutMs = 600000, spawn = spawnProcess } = {}) {
|
|
106
|
+
const input = workerInput({ executable, cwd, prompt, requestId, sessionId, env, relayMcp, timeoutMs, onEvent });
|
|
107
|
+
if (signal?.aborted) throw failure("CLAUDE_WORKER_ABORTED", "The worker was cancelled before dispatch.", "preflight");
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
let child;
|
|
110
|
+
let settled = false;
|
|
111
|
+
let wrote = false;
|
|
112
|
+
let initialized = false;
|
|
113
|
+
let accepted = false;
|
|
114
|
+
let totalBytes = 0;
|
|
115
|
+
let stderrBytes = 0;
|
|
116
|
+
let buffer = "";
|
|
117
|
+
let result = null;
|
|
118
|
+
let terminalError = null;
|
|
119
|
+
let apiError = null;
|
|
120
|
+
const pendingEvents = new Set();
|
|
121
|
+
const decoder = new StringDecoder("utf8");
|
|
122
|
+
const state = () => wrote ? "unknown" : "not_sent";
|
|
123
|
+
const cleanup = () => { clearTimeout(timer); signal?.removeEventListener("abort", abort); };
|
|
124
|
+
const fail = (error) => {
|
|
125
|
+
if (settled) return;
|
|
126
|
+
settled = true;
|
|
127
|
+
cleanup();
|
|
128
|
+
try { child?.kill(); } catch {}
|
|
129
|
+
reject(error);
|
|
130
|
+
};
|
|
131
|
+
const abort = () => fail(failure("CLAUDE_WORKER_ABORTED", "The worker was cancelled; dispatched work is never replayed automatically.", "execution", state()));
|
|
132
|
+
const timer = setTimeout(() => fail(failure("CLAUDE_WORKER_TIMEOUT", "The worker exceeded its deadline; dispatched work may have started and must not be replayed automatically.", wrote ? "execution" : "startup", state())), timeoutMs);
|
|
133
|
+
const emit = (event) => {
|
|
134
|
+
try {
|
|
135
|
+
const pending = Promise.resolve(onEvent(event));
|
|
136
|
+
pendingEvents.add(pending);
|
|
137
|
+
pending.then(() => pendingEvents.delete(pending), () => {
|
|
138
|
+
pendingEvents.delete(pending);
|
|
139
|
+
fail(failure("CLAUDE_WORKER_EVENT_FAILED", "The worker receipt callback failed.", "receipt", state()));
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch { fail(failure("CLAUDE_WORKER_EVENT_FAILED", "The worker receipt callback failed.", "receipt", state())); }
|
|
143
|
+
};
|
|
144
|
+
const accept = () => { if (!accepted) { accepted = true; emit({ type: "accepted", sessionId }); } };
|
|
145
|
+
const protocolError = () => fail(failure("CLAUDE_WORKER_INVALID_PROTOCOL", "Claude Code CLI returned invalid or mismatched worker protocol data.", "response", state()));
|
|
146
|
+
const sameId = (value, expected) => typeof value === "string" && value.toLowerCase() === expected.toLowerCase();
|
|
147
|
+
const correlated = (event) => {
|
|
148
|
+
if (event.user_message_uuids !== undefined && !Array.isArray(event.user_message_uuids)) return false;
|
|
149
|
+
const ids = [...(event.user_message_uuid === undefined ? [] : [event.user_message_uuid]), ...(Array.isArray(event.user_message_uuids) ? event.user_message_uuids : [])];
|
|
150
|
+
return ids.length > 0 && ids.every((id) => sameId(id, requestId));
|
|
151
|
+
};
|
|
152
|
+
const event = (value) => {
|
|
153
|
+
if (settled) return;
|
|
154
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.type !== "string") return protocolError();
|
|
155
|
+
if (value.type === "system" && value.subtype === "init") {
|
|
156
|
+
if (initialized || !sameId(value.session_id, sessionId) || typeof value.cwd !== "string") return protocolError();
|
|
157
|
+
try { if (fs.realpathSync.native(value.cwd) !== input.directory) return protocolError(); } catch { return protocolError(); }
|
|
158
|
+
initialized = true;
|
|
159
|
+
emit({ type: "session", sessionId });
|
|
160
|
+
} else if (value.type === "user" && value.isReplay === true && value.parent_tool_use_id == null) {
|
|
161
|
+
if (!initialized || !sameId(value.session_id, sessionId) || !sameId(value.uuid, requestId)) return protocolError();
|
|
162
|
+
accept();
|
|
163
|
+
} else if (value.type === "assistant" && value.parent_tool_use_id == null && sameId(value.session_id, sessionId)) {
|
|
164
|
+
if (API_CODES.has(value.error)) apiError = value.error;
|
|
165
|
+
} else if (value.type === "result") {
|
|
166
|
+
if (!sameId(value.session_id, sessionId) || result || terminalError) return protocolError();
|
|
167
|
+
const matched = correlated(value);
|
|
168
|
+
const failed = value.is_error === true || value.subtype !== "success";
|
|
169
|
+
if (value.permission_denials !== undefined && !Array.isArray(value.permission_denials)) return protocolError();
|
|
170
|
+
const denials = Array.isArray(value.permission_denials) ? value.permission_denials.length : 0;
|
|
171
|
+
if (!failed && (!initialized || !matched || typeof value.result !== "string" || value.is_error !== false)) {
|
|
172
|
+
return fail(failure("CLAUDE_WORKER_RESULT_UNCORRELATED", "Claude Code CLI returned a result without the worker's request correlation.", "response", state()));
|
|
173
|
+
}
|
|
174
|
+
if (matched) accept();
|
|
175
|
+
const delivered = matched || accepted ? "sent" : state();
|
|
176
|
+
if (STARTUP_CODES.has(value.startup_failure_reason)) {
|
|
177
|
+
const reason = value.startup_failure_reason.toUpperCase();
|
|
178
|
+
terminalError = failure(value.startup_failure_reason === "gateway_signin_required" ? "CLAUDE_CLI_AUTH_REQUIRED" : `CLAUDE_CLI_STARTUP_${reason}`, "Claude Code CLI refused to start the worker under its current configuration or policy.", "startup", delivered);
|
|
179
|
+
terminalError.reasonCode = reason;
|
|
180
|
+
} else if (denials) terminalError = failure("CLAUDE_WORKER_PERMISSION_DENIED", "Claude Code CLI denied one or more required tool permissions.", "execution", delivered);
|
|
181
|
+
else if (!failed && value.num_turns === 0) terminalError = failure("CLAUDE_WORKER_TURN_NOT_STARTED", "Claude Code CLI ended the request without starting an agent turn.", "execution", delivered);
|
|
182
|
+
else if (failed || apiError) {
|
|
183
|
+
const code = apiError === "authentication_failed" ? "CLAUDE_CLI_AUTH_REQUIRED" : apiError ? `CLAUDE_CLI_${apiError.toUpperCase()}` : "CLAUDE_WORKER_EXECUTION_FAILED";
|
|
184
|
+
terminalError = failure(code, "Claude Code CLI did not complete the requested work successfully.", "execution", delivered);
|
|
185
|
+
if (typeof value.subtype === "string" && /^error_[a-z_]{1,80}$/.test(value.subtype)) terminalError.reasonCode = value.subtype.toUpperCase();
|
|
186
|
+
} else result = { output: value.result, sessionId, deliveryState: "sent", sent: true, requestId };
|
|
187
|
+
if (!settled) { try { child.stdin.end(); } catch { fail(failure("CLAUDE_WORKER_STDIN_FAILED", "Claude Code CLI input could not close cleanly.", "response", state())); } }
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
const consume = (chunk, final = false) => {
|
|
191
|
+
if (settled) return;
|
|
192
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
193
|
+
totalBytes += bytes.length;
|
|
194
|
+
if (totalBytes > OUTPUT_LIMIT) return fail(failure("CLAUDE_WORKER_OUTPUT_LIMIT", "Claude Code CLI output exceeded its size limit.", "response", state()));
|
|
195
|
+
buffer += decoder.write(bytes);
|
|
196
|
+
if (final) buffer += decoder.end();
|
|
197
|
+
let newline;
|
|
198
|
+
while ((newline = buffer.indexOf("\n")) >= 0 || final && buffer.length) {
|
|
199
|
+
const line = newline >= 0 ? buffer.slice(0, newline).replace(/\r$/, "") : buffer;
|
|
200
|
+
buffer = newline >= 0 ? buffer.slice(newline + 1) : "";
|
|
201
|
+
if (Buffer.byteLength(line) > FRAME_LIMIT) return fail(failure("CLAUDE_WORKER_OUTPUT_LIMIT", "Claude Code CLI frame exceeded its size limit.", "response", state()));
|
|
202
|
+
if (line.trim()) { try { event(JSON.parse(line)); } catch { protocolError(); } }
|
|
203
|
+
if (settled) return;
|
|
204
|
+
}
|
|
205
|
+
if (Buffer.byteLength(buffer) > FRAME_LIMIT) fail(failure("CLAUDE_WORKER_OUTPUT_LIMIT", "Claude Code CLI frame exceeded its size limit.", "response", state()));
|
|
206
|
+
};
|
|
207
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
208
|
+
try {
|
|
209
|
+
child = spawn(input.binary, input.args, { cwd: input.directory, env: input.childEnv, windowsHide: true, shell: false, stdio: ["pipe", "pipe", "pipe"] });
|
|
210
|
+
child.on("error", () => fail(failure("CLAUDE_WORKER_PROCESS_FAILED", "Claude Code CLI process could not run.", wrote ? "execution" : "startup", state())));
|
|
211
|
+
child.stdin.on("error", () => fail(failure("CLAUDE_WORKER_STDIN_FAILED", "Claude Code CLI input failed; do not replay dispatched work.", "dispatch", state())));
|
|
212
|
+
child.stdout.on("error", () => fail(failure("CLAUDE_WORKER_STDOUT_FAILED", "Claude Code CLI output failed.", "response", state())));
|
|
213
|
+
child.stderr.on("error", () => fail(failure("CLAUDE_WORKER_STDERR_FAILED", "Claude Code CLI diagnostics failed.", "response", state())));
|
|
214
|
+
child.stdout.on("data", consume);
|
|
215
|
+
child.stderr.on("data", (chunk) => {
|
|
216
|
+
if (!settled && (stderrBytes += Buffer.byteLength(chunk)) > PROBE_LIMIT) fail(failure("CLAUDE_WORKER_OUTPUT_LIMIT", "Claude Code CLI diagnostics exceeded their size limit.", "response", state()));
|
|
217
|
+
});
|
|
218
|
+
child.once("spawn", () => {
|
|
219
|
+
if (settled) return;
|
|
220
|
+
wrote = true;
|
|
221
|
+
try { child.stdin.write(input.frame, (error) => { if (error) fail(failure("CLAUDE_WORKER_STDIN_FAILED", "Claude Code CLI input write failed; delivery is uncertain.", "dispatch", "unknown")); }); }
|
|
222
|
+
catch { fail(failure("CLAUDE_WORKER_STDIN_FAILED", "Claude Code CLI input write failed; delivery is uncertain.", "dispatch", "unknown")); }
|
|
223
|
+
});
|
|
224
|
+
child.once("close", async (code, exitSignal) => {
|
|
225
|
+
if (settled) return;
|
|
226
|
+
consume(Buffer.alloc(0), true);
|
|
227
|
+
if (settled) return;
|
|
228
|
+
try { await Promise.all([...pendingEvents]); }
|
|
229
|
+
catch { return fail(failure("CLAUDE_WORKER_EVENT_FAILED", "The worker receipt callback failed.", "receipt", state())); }
|
|
230
|
+
if (settled) return;
|
|
231
|
+
settled = true;
|
|
232
|
+
cleanup();
|
|
233
|
+
if (terminalError && !exitSignal) reject(terminalError);
|
|
234
|
+
else if (code !== 0 || exitSignal) reject(failure("CLAUDE_WORKER_EXIT_FAILED", "Claude Code CLI exited abnormally; dispatched work is not replayed.", "execution", state()));
|
|
235
|
+
else if (!result) reject(failure("CLAUDE_WORKER_RESULT_MISSING", "Claude Code CLI exited without a correlated result.", "response", state()));
|
|
236
|
+
else resolve(result);
|
|
237
|
+
});
|
|
238
|
+
} catch { fail(failure("CLAUDE_WORKER_SPAWN_FAILED", "Claude Code CLI could not be launched.", "startup", state())); }
|
|
239
|
+
});
|
|
240
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { execFile, spawn as spawnProcess } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { StringDecoder } from "node:string_decoder";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
import { resolveCodexBin } from "./platform.mjs";
|
|
9
|
+
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
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}$/i;
|
|
12
|
+
const MCP_NAME = /^[A-Za-z0-9_-]{1,128}$/;
|
|
13
|
+
const MAX_CAPTURE = 2 * 1024 * 1024;
|
|
14
|
+
const MAX_FRAME = 2 * 1024 * 1024;
|
|
15
|
+
const MAX_OUTPUT = 16 * 1024 * 1024;
|
|
16
|
+
const MAX_STDERR = 256 * 1024;
|
|
17
|
+
const MCP_TOOLS = ["worker_status", "list_workers", "send_to_worker", "read_worker_delivery"];
|
|
18
|
+
const ISOLATION_FLAGS = ["--disable", "plugins", "--disable", "apps"];
|
|
19
|
+
const ERRORS = {
|
|
20
|
+
INVALID_INPUT: "Codex worker input is invalid",
|
|
21
|
+
CLI_UNAVAILABLE: "Codex CLI executable is unavailable",
|
|
22
|
+
CLI_UNSUPPORTED: "Codex CLI does not support the required background worker options",
|
|
23
|
+
AUTH_REQUIRED: "Codex CLI authentication is required or has expired",
|
|
24
|
+
AUTH_UNVERIFIED: "Codex CLI authentication could not be verified",
|
|
25
|
+
PERMISSION_DENIED: "Codex CLI permission policy denied the operation",
|
|
26
|
+
CONFIG_INVALID: "Codex CLI configuration could not be loaded",
|
|
27
|
+
MCP_CONFLICT: "A worker-relay MCP registration already exists; isolated worker configuration was not applied",
|
|
28
|
+
MCP_ISOLATION_FAILED: "Codex worker MCP isolation could not be verified; no prompt was sent",
|
|
29
|
+
INVALID_PROTOCOL: "Codex CLI emitted invalid or incomplete protocol data",
|
|
30
|
+
OUTPUT_LIMIT: "Codex CLI output exceeded the worker limit",
|
|
31
|
+
SPAWN_FAILED: "Codex CLI process could not be started",
|
|
32
|
+
PROCESS_ERROR: "Codex CLI process or output pipe failed",
|
|
33
|
+
PROCESS_EXIT: "Codex CLI exited before completing the operation",
|
|
34
|
+
STDIN_ERROR: "Codex CLI input pipe failed",
|
|
35
|
+
TURN_FAILED: "Codex CLI reported a failed turn",
|
|
36
|
+
ABORTED: "Codex worker operation was cancelled",
|
|
37
|
+
TIMEOUT: "Codex worker operation exceeded its deadline",
|
|
38
|
+
EVENT_HANDLER_FAILED: "Codex worker event could not be recorded",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function failure(code, phase, deliveryState = "not-sent", sent = false) {
|
|
42
|
+
return Object.assign(new Error(ERRORS[code]), { code, phase, deliveryState, sent });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function object(value) {
|
|
46
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function diagnosticCode(value, fallback) {
|
|
50
|
+
const text = String(value ?? "").slice(0, MAX_STDERR);
|
|
51
|
+
if (/\b(?:not logged in|not authenticated|authentication (?:is )?required|authentication failed|login required|please run codex login|unauthenticated|unauthorized|invalid_api_key|invalid authentication|token expired|token has expired|refresh_token_reused)\b|\b401\b/i.test(text)) return "AUTH_REQUIRED";
|
|
52
|
+
if (/\b(?:permission denied|access denied|approval required|approval denied|sandbox denied|not allowed by|disallowed by|violates.*requirements|allowed_sandbox_modes|allowed_approval_policies)\b/i.test(text)) return "PERMISSION_DENIED";
|
|
53
|
+
if (/error loading config|failed to load config|invalid.*(?:config|toml)|unknown feature flag|unexpected argument/i.test(text)) return "CONFIG_INVALID";
|
|
54
|
+
return fallback;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function workerEnv(env, relayEnv = {}) {
|
|
58
|
+
const clean = Object.fromEntries(Object.entries(env).filter(([name, value]) => value !== undefined
|
|
59
|
+
&& !/^(?:CODEX_BRIDGE_|CLAUDE_BRIDGE_|CODEX_NATIVE_RELAY_|CODEX_RELAY_ID$|CODEX_THREAD_ID$|CODEX_SESSION_ID$|CODEX_APP_TOOLS_PIPE_PATH$|CODEX_APP_SERVER_URL$|CODEX_INTERNAL_ORIGINATOR_OVERRIDE$|AGENT_RELAY_)/i.test(name)));
|
|
60
|
+
return { ...clean, ...relayEnv };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function binary(executable, env, platform = process.platform) {
|
|
64
|
+
const selected = executable ?? resolveCodexBin(undefined, { env, platform });
|
|
65
|
+
try {
|
|
66
|
+
if (typeof selected !== "string" || !path.isAbsolute(selected) || /[\u0000-\u001f\u007f]/.test(selected)) throw new Error();
|
|
67
|
+
const resolved = fs.realpathSync.native(selected);
|
|
68
|
+
if (!fs.statSync(resolved).isFile() || platform === "win32" && !/\.(?:exe|com)$/i.test(resolved)) throw new Error();
|
|
69
|
+
fs.accessSync(resolved, platform === "win32" ? fs.constants.R_OK : fs.constants.X_OK);
|
|
70
|
+
return resolved;
|
|
71
|
+
} catch { throw failure("CLI_UNAVAILABLE", "inspect"); }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function inspectCodexWorker({ env = process.env, executable, platform = process.platform, run = execFileAsync } = {}) {
|
|
75
|
+
const result = { agent: "codex", available: false, authenticated: null, ready: false };
|
|
76
|
+
if (!object(env) || typeof run !== "function") return { ...result, code: "INVALID_INPUT", reason: ERRORS.INVALID_INPUT };
|
|
77
|
+
let selected;
|
|
78
|
+
try { selected = binary(executable, env, platform); }
|
|
79
|
+
catch (error) { return { ...result, code: error.code, reason: error.message }; }
|
|
80
|
+
const options = { env: workerEnv(env), windowsHide: true, shell: false, timeout: 10000, maxBuffer: MAX_CAPTURE, encoding: "utf8" };
|
|
81
|
+
try {
|
|
82
|
+
const version = await run(selected, ["--version"], options);
|
|
83
|
+
const match = /^codex-cli ([A-Za-z0-9][A-Za-z0-9.+_-]{0,95})\s*$/m.exec(String(version.stdout ?? ""));
|
|
84
|
+
if (!match) return { ...result, code: "CLI_UNSUPPORTED", reason: ERRORS.CLI_UNSUPPORTED };
|
|
85
|
+
Object.assign(result, { available: true, executable: selected, version: match[1] });
|
|
86
|
+
const help = await run(selected, ["exec", "--help"], options);
|
|
87
|
+
if (!["--json", "--ephemeral", "--color", "--config"].every((flag) => String(help.stdout ?? "").includes(flag))) {
|
|
88
|
+
return { ...result, code: "CLI_UNSUPPORTED", reason: ERRORS.CLI_UNSUPPORTED };
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const code = diagnosticCode(error.stderr, result.available ? "CLI_UNSUPPORTED" : "CLI_UNAVAILABLE");
|
|
92
|
+
return { ...result, code, reason: ERRORS[code] };
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const login = await run(selected, ["login", "status"], options);
|
|
96
|
+
const text = `${login.stdout ?? ""}\n${login.stderr ?? ""}`;
|
|
97
|
+
if (/^Logged in using (?:ChatGPT|workload identity|an API key)\b|^API key configured\b/m.test(text)) {
|
|
98
|
+
return { ...result, authenticated: true, ready: true, reason: "Codex CLI and stored authentication are available; model access is not verified" };
|
|
99
|
+
}
|
|
100
|
+
const code = diagnosticCode(text, "AUTH_UNVERIFIED");
|
|
101
|
+
return { ...result, authenticated: code === "AUTH_REQUIRED" ? false : null, code, reason: ERRORS[code] };
|
|
102
|
+
} catch (error) {
|
|
103
|
+
const code = diagnosticCode(`${error.stdout ?? ""}\n${error.stderr ?? ""}`, "AUTH_UNVERIFIED");
|
|
104
|
+
return { ...result, authenticated: code === "AUTH_REQUIRED" ? false : null, code, reason: ERRORS[code] };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function capture(executable, args, { cwd, env, spawn, signal, timeoutMs }) {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
if (signal?.aborted) return reject(failure("ABORTED", "configure"));
|
|
111
|
+
let child;
|
|
112
|
+
let settled = false;
|
|
113
|
+
let size = 0;
|
|
114
|
+
const stdout = [];
|
|
115
|
+
const stderr = [];
|
|
116
|
+
const finish = (error, value) => {
|
|
117
|
+
if (settled) return;
|
|
118
|
+
settled = true;
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
signal?.removeEventListener("abort", abort);
|
|
121
|
+
if (error) {
|
|
122
|
+
try { child?.kill(); } catch {}
|
|
123
|
+
reject(error);
|
|
124
|
+
} else resolve(value);
|
|
125
|
+
};
|
|
126
|
+
const abort = () => finish(failure("ABORTED", "configure"));
|
|
127
|
+
const timer = setTimeout(() => finish(failure("TIMEOUT", "configure")), timeoutMs);
|
|
128
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
129
|
+
try {
|
|
130
|
+
child = spawn(executable, args, { cwd, env, shell: false, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
131
|
+
for (const [stream, chunks] of [[child.stdout, stdout], [child.stderr, stderr]]) {
|
|
132
|
+
stream.on("error", () => finish(failure("PROCESS_ERROR", "configure")));
|
|
133
|
+
stream.on("data", (chunk) => {
|
|
134
|
+
if (settled) return;
|
|
135
|
+
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
136
|
+
size += data.length;
|
|
137
|
+
if (size > MAX_CAPTURE) return finish(failure("OUTPUT_LIMIT", "configure"));
|
|
138
|
+
chunks.push(data);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
child.on("error", () => finish(failure("SPAWN_FAILED", "configure")));
|
|
142
|
+
child.on("close", (code) => {
|
|
143
|
+
const diagnostic = Buffer.concat(stderr).toString("utf8");
|
|
144
|
+
if (code !== 0) return finish(failure(diagnosticCode(diagnostic, "MCP_ISOLATION_FAILED"), "configure"));
|
|
145
|
+
finish(null, Buffer.concat(stdout).toString("utf8"));
|
|
146
|
+
});
|
|
147
|
+
} catch { finish(failure("SPAWN_FAILED", "configure")); }
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function inventory(text) {
|
|
152
|
+
let entries;
|
|
153
|
+
try { entries = JSON.parse(text); }
|
|
154
|
+
catch { throw failure("MCP_ISOLATION_FAILED", "configure"); }
|
|
155
|
+
if (!Array.isArray(entries) || entries.length > 512) throw failure("MCP_ISOLATION_FAILED", "configure");
|
|
156
|
+
const names = new Set();
|
|
157
|
+
for (const entry of entries) {
|
|
158
|
+
if (!object(entry) || !MCP_NAME.test(entry.name ?? "") || typeof entry.enabled !== "boolean" || names.has(entry.name)) {
|
|
159
|
+
throw failure("MCP_ISOLATION_FAILED", "configure");
|
|
160
|
+
}
|
|
161
|
+
names.add(entry.name);
|
|
162
|
+
}
|
|
163
|
+
return entries;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function isolatedArgs(executable, settings, relayMcp) {
|
|
167
|
+
const initial = inventory(await capture(executable, [...ISOLATION_FLAGS, "mcp", "list", "--json"], settings));
|
|
168
|
+
if (initial.some((entry) => entry.name === "worker-relay")) throw failure("MCP_CONFLICT", "configure");
|
|
169
|
+
const overrides = initial.flatMap(({ name }) => ["-c", `mcp_servers.${name}.enabled=false`]);
|
|
170
|
+
const config = {
|
|
171
|
+
command: relayMcp.command,
|
|
172
|
+
args: relayMcp.args,
|
|
173
|
+
env_vars: Object.keys(relayMcp.env).sort(),
|
|
174
|
+
enabled: true,
|
|
175
|
+
required: true,
|
|
176
|
+
enabled_tools: MCP_TOOLS,
|
|
177
|
+
startup_timeout_sec: 30,
|
|
178
|
+
tool_timeout_sec: 90,
|
|
179
|
+
};
|
|
180
|
+
for (const [name, value] of Object.entries(config)) overrides.push("-c", `mcp_servers.worker-relay.${name}=${JSON.stringify(value)}`);
|
|
181
|
+
const args = [...ISOLATION_FLAGS, ...overrides];
|
|
182
|
+
const checked = inventory(await capture(executable, [...args, "mcp", "list", "--json"], settings));
|
|
183
|
+
const enabled = checked.filter((entry) => entry.enabled);
|
|
184
|
+
const transport = enabled[0]?.transport;
|
|
185
|
+
const expectedNames = [...initial.map((entry) => entry.name), "worker-relay"].sort();
|
|
186
|
+
if (JSON.stringify(checked.map((entry) => entry.name).sort()) !== JSON.stringify(expectedNames)
|
|
187
|
+
|| enabled.length !== 1 || enabled[0].name !== "worker-relay" || !object(transport)
|
|
188
|
+
|| transport.type !== "stdio" || transport.command !== config.command
|
|
189
|
+
|| JSON.stringify(transport.args) !== JSON.stringify(config.args)
|
|
190
|
+
|| JSON.stringify(transport.env_vars) !== JSON.stringify(config.env_vars)
|
|
191
|
+
|| transport.env !== null && (!object(transport.env) || Object.keys(transport.env).length !== 0)
|
|
192
|
+
|| transport.cwd !== null && transport.cwd !== undefined) throw failure("MCP_ISOLATION_FAILED", "configure");
|
|
193
|
+
return args;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function runCodexWorker({ executable, cwd, prompt, requestId, sessionId, env = process.env, relayMcp, onEvent = () => {}, signal, timeoutMs = 600000, spawn = spawnProcess } = {}) {
|
|
197
|
+
let directory;
|
|
198
|
+
try {
|
|
199
|
+
if (!object(env) || typeof spawn !== "function" || typeof cwd !== "string" || !path.isAbsolute(cwd) || typeof prompt !== "string" || !prompt.trim()
|
|
200
|
+
|| Buffer.byteLength(prompt) > MAX_FRAME || typeof requestId !== "string" || !requestId || requestId.length > 128
|
|
201
|
+
|| /[\u0000-\u001f\u007f]/.test(requestId) || sessionId !== undefined && (typeof sessionId !== "string" || !UUID.test(sessionId))
|
|
202
|
+
|| !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647 || typeof onEvent !== "function"
|
|
203
|
+
|| !object(relayMcp) || typeof relayMcp.command !== "string" || !relayMcp.command || relayMcp.command.length > 32768
|
|
204
|
+
|| /[\u0000-\u001f\u007f]/.test(relayMcp.command) || !Array.isArray(relayMcp.args) || relayMcp.args.length > 128
|
|
205
|
+
|| relayMcp.args.some((arg) => typeof arg !== "string" || arg.length > 32768 || arg.includes("\0"))
|
|
206
|
+
|| !object(relayMcp.env) || Object.keys(relayMcp.env).length > 32
|
|
207
|
+
|| Object.entries(relayMcp.env).some(([name, value]) => !/^AGENT_RELAY_[A-Z0-9_]+$/.test(name)
|
|
208
|
+
|| typeof value !== "string" || !value || value.length > 65536 || value.includes("\0"))) throw new Error();
|
|
209
|
+
if (relayMcp.env.AGENT_RELAY_TOKEN && [relayMcp.command, ...relayMcp.args].some((arg) => arg.includes(relayMcp.env.AGENT_RELAY_TOKEN))) throw new Error();
|
|
210
|
+
directory = fs.realpathSync.native(cwd);
|
|
211
|
+
if (!fs.statSync(directory).isDirectory()) throw new Error();
|
|
212
|
+
} catch { throw failure("INVALID_INPUT", "validate"); }
|
|
213
|
+
const selected = binary(executable, env);
|
|
214
|
+
const workerSessionId = sessionId ?? randomUUID();
|
|
215
|
+
const childEnv = workerEnv(env, relayMcp.env);
|
|
216
|
+
const startedAt = Date.now();
|
|
217
|
+
const settings = { cwd: directory, env: childEnv, spawn, signal, get timeoutMs() { return Math.max(1, Math.min(20000, timeoutMs - (Date.now() - startedAt))); } };
|
|
218
|
+
const overrides = await isolatedArgs(selected, settings, relayMcp);
|
|
219
|
+
if (signal?.aborted) throw failure("ABORTED", "launch");
|
|
220
|
+
const remaining = timeoutMs - (Date.now() - startedAt);
|
|
221
|
+
if (remaining <= 0) throw failure("TIMEOUT", "launch");
|
|
222
|
+
return new Promise((resolve, reject) => {
|
|
223
|
+
let child;
|
|
224
|
+
let settled = false;
|
|
225
|
+
let wrote = false;
|
|
226
|
+
let accepted = false;
|
|
227
|
+
let completed = false;
|
|
228
|
+
let providerSessionId = null;
|
|
229
|
+
let stdoutBytes = 0;
|
|
230
|
+
let stderrBytes = 0;
|
|
231
|
+
let buffer = "";
|
|
232
|
+
let diagnostic = "";
|
|
233
|
+
let output = "";
|
|
234
|
+
let usage;
|
|
235
|
+
let pendingEvents = Promise.resolve();
|
|
236
|
+
const decoder = new StringDecoder("utf8");
|
|
237
|
+
const stderrDecoder = new StringDecoder("utf8");
|
|
238
|
+
const state = () => wrote ? "unknown" : "not-sent";
|
|
239
|
+
const finish = (error) => {
|
|
240
|
+
if (settled) return;
|
|
241
|
+
settled = true;
|
|
242
|
+
clearTimeout(timer);
|
|
243
|
+
signal?.removeEventListener("abort", abort);
|
|
244
|
+
if (error) {
|
|
245
|
+
try { child?.kill(); } catch {}
|
|
246
|
+
reject(error);
|
|
247
|
+
} else resolve({ agent: "codex", output, sessionId: workerSessionId, requestId, deliveryState: "sent", sent: true, ...(usage ? { usage } : {}) });
|
|
248
|
+
};
|
|
249
|
+
const fail = (code, phase = "run") => finish(failure(code, phase, state(), accepted));
|
|
250
|
+
const abort = () => fail("ABORTED");
|
|
251
|
+
const timer = setTimeout(() => fail("TIMEOUT"), remaining);
|
|
252
|
+
const notify = (event) => {
|
|
253
|
+
try {
|
|
254
|
+
pendingEvents = Promise.all([pendingEvents, onEvent(event)]).then(() => undefined);
|
|
255
|
+
pendingEvents.catch(() => fail("EVENT_HANDLER_FAILED"));
|
|
256
|
+
}
|
|
257
|
+
catch { fail("EVENT_HANDLER_FAILED"); }
|
|
258
|
+
};
|
|
259
|
+
const event = (value) => {
|
|
260
|
+
if (!object(value) || typeof value.type !== "string") return fail("INVALID_PROTOCOL");
|
|
261
|
+
if (value.type === "thread.started") {
|
|
262
|
+
if (providerSessionId || accepted || completed || !UUID.test(value.thread_id ?? "")) return fail("INVALID_PROTOCOL");
|
|
263
|
+
providerSessionId = value.thread_id;
|
|
264
|
+
notify({ type: "session", sessionId: workerSessionId });
|
|
265
|
+
} else if (value.type === "turn.started") {
|
|
266
|
+
if (!providerSessionId || accepted || completed) return fail("INVALID_PROTOCOL");
|
|
267
|
+
accepted = true;
|
|
268
|
+
notify({ type: "accepted", sessionId: workerSessionId });
|
|
269
|
+
} else if (value.type === "turn.completed") {
|
|
270
|
+
if (!accepted || completed || !object(value.usage)) return fail("INVALID_PROTOCOL");
|
|
271
|
+
const entries = Object.entries(value.usage);
|
|
272
|
+
if (entries.some(([name, count]) => !/^[a-z_]{1,64}$/.test(name) || !Number.isSafeInteger(count) || count < 0)) return fail("INVALID_PROTOCOL");
|
|
273
|
+
usage = Object.fromEntries(entries);
|
|
274
|
+
completed = true;
|
|
275
|
+
} else if (value.type === "turn.failed" || value.type === "error") {
|
|
276
|
+
const message = value.type === "turn.failed" ? value.error?.message : value.message;
|
|
277
|
+
if (typeof message !== "string") return fail("INVALID_PROTOCOL");
|
|
278
|
+
fail(diagnosticCode(message, "TURN_FAILED"));
|
|
279
|
+
} else if (/^item\.(?:started|updated|completed)$/.test(value.type)) {
|
|
280
|
+
if (!accepted || completed || !object(value.item) || typeof value.item.type !== "string") return fail("INVALID_PROTOCOL");
|
|
281
|
+
if (value.type === "item.completed" && value.item.type === "agent_message") {
|
|
282
|
+
if (typeof value.item.text !== "string") return fail("INVALID_PROTOCOL");
|
|
283
|
+
output = value.item.text;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
const consume = (chunk) => {
|
|
288
|
+
if (settled) return;
|
|
289
|
+
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
290
|
+
stdoutBytes += data.length;
|
|
291
|
+
if (stdoutBytes > MAX_OUTPUT) return fail("OUTPUT_LIMIT");
|
|
292
|
+
buffer += decoder.write(data);
|
|
293
|
+
let newline;
|
|
294
|
+
while (!settled && (newline = buffer.indexOf("\n")) >= 0) {
|
|
295
|
+
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
|
296
|
+
buffer = buffer.slice(newline + 1);
|
|
297
|
+
if (Buffer.byteLength(line) > MAX_FRAME) return fail("OUTPUT_LIMIT");
|
|
298
|
+
if (!line.trim()) continue;
|
|
299
|
+
let value;
|
|
300
|
+
try { value = JSON.parse(line); }
|
|
301
|
+
catch { return fail("INVALID_PROTOCOL"); }
|
|
302
|
+
event(value);
|
|
303
|
+
}
|
|
304
|
+
if (Buffer.byteLength(buffer) > MAX_FRAME) fail("OUTPUT_LIMIT");
|
|
305
|
+
};
|
|
306
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
307
|
+
try {
|
|
308
|
+
child = spawn(selected, [...overrides, "exec", "--json", "--ephemeral", "--color", "never", "-C", directory, "-"], {
|
|
309
|
+
cwd: directory, env: childEnv, shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"],
|
|
310
|
+
});
|
|
311
|
+
child.on("error", () => fail(wrote ? "PROCESS_ERROR" : "SPAWN_FAILED", wrote ? "run" : "launch"));
|
|
312
|
+
child.stdin.on("error", () => fail("STDIN_ERROR", "send"));
|
|
313
|
+
child.stdout.on("error", () => fail("PROCESS_ERROR"));
|
|
314
|
+
child.stderr.on("error", () => fail("PROCESS_ERROR"));
|
|
315
|
+
child.stdout.on("data", consume);
|
|
316
|
+
child.stderr.on("data", (chunk) => {
|
|
317
|
+
if (settled) return;
|
|
318
|
+
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
319
|
+
stderrBytes += data.length;
|
|
320
|
+
if (stderrBytes > MAX_STDERR) return fail("OUTPUT_LIMIT");
|
|
321
|
+
diagnostic += stderrDecoder.write(data);
|
|
322
|
+
});
|
|
323
|
+
child.on("close", (code) => {
|
|
324
|
+
if (settled) return;
|
|
325
|
+
buffer += decoder.end();
|
|
326
|
+
diagnostic += stderrDecoder.end();
|
|
327
|
+
if (buffer.trim()) {
|
|
328
|
+
consume(Buffer.from("\n"));
|
|
329
|
+
if (settled) return;
|
|
330
|
+
}
|
|
331
|
+
if (code !== 0) return fail(diagnosticCode(diagnostic, "PROCESS_EXIT"), "result");
|
|
332
|
+
if (!completed || !accepted) return fail("INVALID_PROTOCOL", "result");
|
|
333
|
+
pendingEvents.then(() => finish(), () => fail("EVENT_HANDLER_FAILED"));
|
|
334
|
+
});
|
|
335
|
+
child.on("spawn", () => {
|
|
336
|
+
if (settled) return;
|
|
337
|
+
if (signal?.aborted) return abort();
|
|
338
|
+
wrote = true;
|
|
339
|
+
try { child.stdin.end(prompt, "utf8", (error) => { if (error) fail("STDIN_ERROR", "send"); }); }
|
|
340
|
+
catch { fail("STDIN_ERROR", "send"); }
|
|
341
|
+
});
|
|
342
|
+
} catch { fail("SPAWN_FAILED", "launch"); }
|
|
343
|
+
});
|
|
344
|
+
}
|