fix: harden v0.2 code agent transport

This commit is contained in:
Codex
2026-05-31 22:54:50 +08:00
parent 88225c16ae
commit 9a14ed06ae
8 changed files with 553 additions and 10 deletions
+91 -6
View File
@@ -1420,10 +1420,60 @@ function tomlString(value) {
return JSON.stringify(String(value ?? ""));
}
function spawnCodexAppServerProcess(command, args, options = {}) {
const native = resolveCodexNativeSpawn(command, options.env ?? process.env);
return spawn(native?.command ?? command, args, {
...options,
env: native?.env ?? options.env
});
}
function resolveCodexNativeSpawn(command, env = process.env) {
const resolved = commandPathSync(command, env);
if (!resolved) return null;
let real = resolved;
try {
real = realpathSync(resolved);
} catch {
// Keep the commandPathSync result; spawn will surface any executable error.
}
const packageRoot = findPackageRoot(real);
if (!packageRoot) return null;
const candidates = codexNativeBinaryCandidates(packageRoot);
const nativeCommand = candidates.find((candidate) => existsSync(candidate) && accessSyncBoolean(candidate, fsConstants.X_OK));
if (!nativeCommand) return null;
const pathDir = path.resolve(nativeCommand, "..", "..", "path");
const envPath = String(env.PATH ?? process.env.PATH ?? "");
return {
command: nativeCommand,
env: {
...env,
PATH: existsSync(pathDir) ? [pathDir, envPath].filter(Boolean).join(path.delimiter) : envPath,
CODEX_MANAGED_BY_NPM: env.CODEX_MANAGED_BY_NPM ?? "1"
}
};
}
function codexNativeBinaryCandidates(packageRoot) {
if (process.platform === "linux" && process.arch === "x64") {
return [
path.join(packageRoot, "..", "codex-linux-x64", "vendor", "x86_64-unknown-linux-musl", "codex", "codex"),
path.join(packageRoot, "vendor", "x86_64-unknown-linux-musl", "codex", "codex")
];
}
if (process.platform === "linux" && process.arch === "arm64") {
return [
path.join(packageRoot, "..", "codex-linux-arm64", "vendor", "aarch64-unknown-linux-musl", "codex", "codex"),
path.join(packageRoot, "vendor", "aarch64-unknown-linux-musl", "codex", "codex")
];
}
return [];
}
export function createCodexAppServerJsonLineClient({ command = DEFAULT_CODEX_STDIO_COMMAND, args = null, env = process.env, cwd = repoRoot, onNotification = null } = {}) {
const spawnArgs = Array.isArray(args) ? args : codexAppServerArgs(env);
const childDetached = process.platform !== "win32";
const child = spawn(command, spawnArgs, {
const child = spawnCodexAppServerProcess(command, spawnArgs, {
cwd,
env,
detached: childDetached,
@@ -1464,7 +1514,7 @@ export function createCodexAppServerJsonLineClient({ command = DEFAULT_CODEX_STD
} catch {
return;
}
const id = typeof payload.id === "number" ? payload.id : null;
const id = typeof payload.id === "number" || typeof payload.id === "string" ? payload.id : null;
const method = typeof payload.method === "string" ? payload.method : null;
if (id !== null && method === null) {
const request = pending.get(id);
@@ -1479,7 +1529,7 @@ export function createCodexAppServerJsonLineClient({ command = DEFAULT_CODEX_STD
return;
}
if (id !== null && method !== null) {
handleServerRequest(id, method);
handleServerRequest(id, method, extractAppServerRecord(payload.params));
return;
}
if (method !== null && notificationHandler) {
@@ -1490,6 +1540,7 @@ export function createCodexAppServerJsonLineClient({ command = DEFAULT_CODEX_STD
function closeWith(message, code, signal) {
if (closed) return;
closed = true;
terminateChildProcessGroup(child, { detached: childDetached });
for (const [id, request] of pending.entries()) {
pending.delete(id);
clearTimeout(request.timer);
@@ -1547,14 +1598,44 @@ export function createCodexAppServerJsonLineClient({ command = DEFAULT_CODEX_STD
child.stdin.write(`${JSON.stringify({ method, params })}\n`);
}
function handleServerRequest(id, method) {
if (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval") {
child.stdin.write(`${JSON.stringify({ id, result: { decision: "decline" } })}\n`);
function handleServerRequest(id, method, params = {}) {
if (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval" || method === "item/permissions/requestApproval") {
const approval = approvalResponseForServerRequest(params);
notifyClientRequest(method, params, approval.decision);
child.stdin.write(`${JSON.stringify({ id, result: approval })}\n`);
return;
}
if (method === "item/tool/requestUserInput" || method === "mcpServer/elicitation/request") {
notifyClientRequest(method, params, "denied");
child.stdin.write(`${JSON.stringify({ id, result: { canceled: true, decision: "denied", message: "Non-interactive HWLAB Code Agent cannot request user input during a running turn." } })}\n`);
return;
}
notifyClientRequest(method, params, "unsupported");
child.stdin.write(`${JSON.stringify({ id, error: { code: -32601, message: `Unsupported client-side request: ${method}` } })}\n`);
}
function approvalResponseForServerRequest(params = {}) {
const available = Array.isArray(params?.availableDecisions) ? params.availableDecisions.map(String) : [];
const decision = available.find((item) => item === "approved")
?? available.find((item) => item === "approved_for_session")
?? available.find((item) => item.startsWith("approved"))
?? "approved";
return { decision };
}
function notifyClientRequest(method, params, decision) {
notificationHandler?.({
method: "client/request/handled",
params: {
method,
decision,
itemId: optionalId(params?.itemId ?? params?.item?.id),
approvalId: optionalId(params?.approvalId ?? params?.id),
targetItemId: optionalId(params?.targetItemId)
}
});
}
async function startThread({ model, cwd: threadCwd, sandbox, approvalPolicy = "never", serviceName = "hwlab-cloud-api" } = {}, timeoutMs = 30000) {
const result = await request("thread/start", dropEmpty({
model,
@@ -1616,6 +1697,10 @@ export function createCodexAppServerJsonLineClient({ command = DEFAULT_CODEX_STD
}
function terminateChildProcess(child, { detached = false } = {}) {
return terminateChildProcessGroup(child, { detached });
}
function terminateChildProcessGroup(child, { detached = false } = {}) {
if (!child || child.killed) return;
try {
if (detached && Number.isInteger(child.pid)) {