Files
boss/local-agent/codex-remote-control-daemon.mjs
2026-06-04 15:25:18 +08:00

150 lines
4.8 KiB
JavaScript

import { spawn } from "node:child_process";
function nonEmpty(value) {
const text = String(value ?? "").trim();
return text || undefined;
}
function parseBoolean(value, fallback = false) {
if (value === undefined || value === null || value === "") return fallback;
if (value === false || value === "false" || value === "0" || value === 0) return false;
return true;
}
function normalizeArgs(value, fallback = []) {
if (!Array.isArray(value)) return [...fallback];
return value.map((item) => nonEmpty(item)).filter(Boolean);
}
function redactSensitiveText(value) {
return String(value ?? "")
.replace(/sk-[A-Za-z0-9_-]{8,}/g, "[REDACTED]")
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, "Bearer [REDACTED]")
.replace(/token["']?\s*[:=]\s*["']?[^"',\s}]+/gi, "token:[REDACTED]")
.slice(0, 1200);
}
function parseEnvArgs(value) {
const text = nonEmpty(value);
if (!text) return undefined;
try {
const parsed = JSON.parse(text);
if (Array.isArray(parsed)) {
return parsed.map((item) => nonEmpty(item)).filter(Boolean);
}
} catch {
// Fall back to whitespace parsing for simple local overrides.
}
return text.split(/\s+/).map((item) => nonEmpty(item)).filter(Boolean);
}
export function getCodexRemoteControlDaemonConfig(env = process.env, config = {}) {
const appServerEnabled = parseBoolean(config.codexAppServerEnabled, false);
const enabled = parseBoolean(
env.BOSS_CODEX_REMOTE_CONTROL_ENABLED ?? config.codexRemoteControlEnabled,
appServerEnabled,
);
const command =
nonEmpty(env.BOSS_CODEX_REMOTE_CONTROL_COMMAND) ??
nonEmpty(config.codexRemoteControlCommand) ??
nonEmpty(config.codexAppServerCommand) ??
"codex";
const startArgs = normalizeArgs(
parseEnvArgs(env.BOSS_CODEX_REMOTE_CONTROL_START_ARGS) ?? config.codexRemoteControlStartArgs ?? config.codexRemoteControlArgs,
["remote-control", "start", "--json"],
);
const stopArgs = normalizeArgs(
parseEnvArgs(env.BOSS_CODEX_REMOTE_CONTROL_STOP_ARGS) ?? config.codexRemoteControlStopArgs,
["remote-control", "stop", "--json"],
);
const timeoutMs = Number(env.BOSS_CODEX_REMOTE_CONTROL_TIMEOUT_MS ?? config.codexRemoteControlTimeoutMs ?? 15_000);
return {
enabled,
command,
startArgs,
stopArgs,
timeoutMs: Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 15_000,
};
}
function runCommand(command, args, options = {}) {
return new Promise((resolve) => {
const child = spawn(command, args, {
cwd: options.cwd,
env: options.env,
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
child.kill("SIGKILL");
}, options.timeoutMs ?? 15_000);
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk) => {
stdout += chunk;
});
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
child.on("error", (error) => {
clearTimeout(timer);
resolve({ ok: false, exitCode: null, stdout, stderr: error.message, timedOut });
});
child.on("close", (code) => {
clearTimeout(timer);
resolve({ ok: code === 0 && !timedOut, exitCode: code, stdout, stderr, timedOut });
});
});
}
function parseJsonOutput(stdout) {
const text = String(stdout ?? "").trim();
if (!text) return null;
try {
return JSON.parse(text);
} catch {
return null;
}
}
export async function runCodexRemoteControlDaemonAction(action, config = {}, options = {}) {
if (action !== "start" && action !== "stop") {
throw new Error(`Unsupported codex remote-control action: ${action}`);
}
const daemonConfig = getCodexRemoteControlDaemonConfig(options.env ?? process.env, config);
if (!daemonConfig.enabled) {
return {
status: "failed",
action,
commandLabel: "",
outputSummary: "Codex Remote Control daemon is disabled.",
error: "CODEX_REMOTE_CONTROL_DISABLED",
};
}
const args = action === "start" ? daemonConfig.startArgs : daemonConfig.stopArgs;
const commandResult = await runCommand(daemonConfig.command, args, {
cwd: nonEmpty(config.codexRemoteControlWorkdir) ?? nonEmpty(config.codexAppServerWorkdir),
env: options.env ?? process.env,
timeoutMs: daemonConfig.timeoutMs,
});
const outputSummary = redactSensitiveText(commandResult.stdout || commandResult.stderr);
const parsedOutput = parseJsonOutput(outputSummary);
return {
status: commandResult.ok ? "completed" : "failed",
action,
exitCode: commandResult.exitCode,
timedOut: commandResult.timedOut,
commandLabel: [daemonConfig.command, ...args].join(" "),
outputSummary,
...(parsedOutput && typeof parsedOutput === "object" ? { parsedOutput } : {}),
};
}