mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
feat: codex-shell-tool-mcp
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import { DARWIN_BASH_VARIANTS, LINUX_BASH_VARIANTS } from "./constants.js";
|
||||
import { BashSelection, OsReleaseInfo } from "./types.js";
|
||||
|
||||
function supportedDetail(variants: { name: string }[]): string {
|
||||
return `Supported variants: ${variants.map((variant) => variant.name).join(", ")}`;
|
||||
}
|
||||
|
||||
export function selectLinuxBash(
|
||||
bashRoot: string,
|
||||
info: OsReleaseInfo,
|
||||
): BashSelection {
|
||||
const versionId = info.versionId;
|
||||
const candidates: Array<{
|
||||
variant: (typeof LINUX_BASH_VARIANTS)[number];
|
||||
matchesVersion: boolean;
|
||||
}> = [];
|
||||
for (const variant of LINUX_BASH_VARIANTS) {
|
||||
const matchesId =
|
||||
variant.ids.includes(info.id) ||
|
||||
variant.ids.some((id) => info.idLike.includes(id));
|
||||
if (!matchesId) {
|
||||
continue;
|
||||
}
|
||||
const matchesVersion = Boolean(
|
||||
versionId &&
|
||||
variant.versions.some((prefix) => versionId.startsWith(prefix)),
|
||||
);
|
||||
candidates.push({ variant, matchesVersion });
|
||||
}
|
||||
|
||||
const pickVariant = (list: typeof candidates) =>
|
||||
list.find((item) => item.variant)?.variant;
|
||||
|
||||
const preferred = pickVariant(
|
||||
candidates.filter((item) => item.matchesVersion),
|
||||
);
|
||||
if (preferred) {
|
||||
return {
|
||||
path: path.join(bashRoot, preferred.name, "bash"),
|
||||
variant: preferred.name,
|
||||
};
|
||||
}
|
||||
|
||||
const fallbackMatch = pickVariant(candidates);
|
||||
if (fallbackMatch) {
|
||||
return {
|
||||
path: path.join(bashRoot, fallbackMatch.name, "bash"),
|
||||
variant: fallbackMatch.name,
|
||||
};
|
||||
}
|
||||
|
||||
const fallback = LINUX_BASH_VARIANTS[0];
|
||||
if (fallback) {
|
||||
return {
|
||||
path: path.join(bashRoot, fallback.name, "bash"),
|
||||
variant: fallback.name,
|
||||
};
|
||||
}
|
||||
|
||||
const detail = supportedDetail(LINUX_BASH_VARIANTS);
|
||||
throw new Error(
|
||||
`Unable to select a Bash variant for ${info.id || "unknown"} ${versionId || ""}. ${detail}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function selectDarwinBash(
|
||||
bashRoot: string,
|
||||
darwinRelease: string,
|
||||
): BashSelection {
|
||||
const darwinMajor = Number.parseInt(darwinRelease.split(".")[0] || "0", 10);
|
||||
const preferred = DARWIN_BASH_VARIANTS.find(
|
||||
(variant) => darwinMajor >= variant.minDarwin,
|
||||
);
|
||||
if (preferred) {
|
||||
return {
|
||||
path: path.join(bashRoot, preferred.name, "bash"),
|
||||
variant: preferred.name,
|
||||
};
|
||||
}
|
||||
|
||||
const fallback = DARWIN_BASH_VARIANTS[0];
|
||||
if (fallback) {
|
||||
return {
|
||||
path: path.join(bashRoot, fallback.name, "bash"),
|
||||
variant: fallback.name,
|
||||
};
|
||||
}
|
||||
|
||||
const detail = supportedDetail(DARWIN_BASH_VARIANTS);
|
||||
throw new Error(
|
||||
`Unable to select a macOS Bash build (darwin ${darwinMajor}). ${detail}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveBashPath(
|
||||
targetRoot: string,
|
||||
platform: NodeJS.Platform,
|
||||
darwinRelease = os.release(),
|
||||
osInfo: OsReleaseInfo | null = null,
|
||||
): BashSelection {
|
||||
const bashRoot = path.join(targetRoot, "bash");
|
||||
|
||||
if (platform === "linux") {
|
||||
if (!osInfo) {
|
||||
throw new Error("Linux OS info is required to select a Bash variant.");
|
||||
}
|
||||
return selectLinuxBash(bashRoot, osInfo);
|
||||
}
|
||||
if (platform === "darwin") {
|
||||
return selectDarwinBash(bashRoot, darwinRelease);
|
||||
}
|
||||
throw new Error(`Unsupported platform for Bash selection: ${platform}`);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { DarwinBashVariant, LinuxBashVariant } from "./types.js";
|
||||
|
||||
export const LINUX_BASH_VARIANTS: LinuxBashVariant[] = [
|
||||
{ name: "ubuntu-24.04", ids: ["ubuntu"], versions: ["24.04"] },
|
||||
{ name: "ubuntu-22.04", ids: ["ubuntu"], versions: ["22.04"] },
|
||||
{ name: "ubuntu-20.04", ids: ["ubuntu"], versions: ["20.04"] },
|
||||
{ name: "debian-12", ids: ["debian"], versions: ["12"] },
|
||||
{ name: "debian-11", ids: ["debian"], versions: ["11"] },
|
||||
{
|
||||
name: "centos-9",
|
||||
ids: ["centos", "rhel", "rocky", "almalinux"],
|
||||
versions: ["9"],
|
||||
},
|
||||
];
|
||||
|
||||
export const DARWIN_BASH_VARIANTS: DarwinBashVariant[] = [
|
||||
{ name: "macos-15", minDarwin: 24 },
|
||||
{ name: "macos-14", minDarwin: 23 },
|
||||
{ name: "macos-13", minDarwin: 22 },
|
||||
];
|
||||
@@ -0,0 +1,92 @@
|
||||
// Launches the codex-exec-mcp-server binary bundled in this package.
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { accessSync, constants } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolveBashPath } from "./bashSelection.js";
|
||||
import { readOsRelease } from "./osRelease.js";
|
||||
import { resolveTargetTriple } from "./platform.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const targetTriple = resolveTargetTriple(process.platform, process.arch);
|
||||
const vendorRoot = path.join(__dirname, "..", "vendor");
|
||||
const targetRoot = path.join(vendorRoot, targetTriple);
|
||||
const execveWrapperPath = path.join(targetRoot, "codex-execve-wrapper");
|
||||
const serverPath = path.join(targetRoot, "codex-exec-mcp-server");
|
||||
|
||||
const osInfo = process.platform === "linux" ? readOsRelease() : null;
|
||||
const { path: bashPath } = resolveBashPath(
|
||||
targetRoot,
|
||||
process.platform,
|
||||
os.release(),
|
||||
osInfo,
|
||||
);
|
||||
|
||||
[execveWrapperPath, serverPath, bashPath].forEach((checkPath) => {
|
||||
try {
|
||||
accessSync(checkPath, constants.F_OK);
|
||||
} catch {
|
||||
throw new Error(`Required binary missing: ${checkPath}`);
|
||||
}
|
||||
});
|
||||
|
||||
const args = [
|
||||
"--execve",
|
||||
execveWrapperPath,
|
||||
"--bash",
|
||||
bashPath,
|
||||
...process.argv.slice(2),
|
||||
];
|
||||
const child = spawn(serverPath, args, {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
const forwardSignal = (signal: NodeJS.Signals) => {
|
||||
if (child.killed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
child.kill(signal);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
(["SIGINT", "SIGTERM", "SIGHUP"] as const).forEach((sig) => {
|
||||
process.on(sig, () => forwardSignal(sig));
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
const childResult = await new Promise<
|
||||
| { type: "signal"; signal: NodeJS.Signals }
|
||||
| { type: "code"; exitCode: number }
|
||||
>((resolve) => {
|
||||
child.on("exit", (code, signal) => {
|
||||
if (signal) {
|
||||
resolve({ type: "signal", signal });
|
||||
} else {
|
||||
resolve({ type: "code", exitCode: code ?? 1 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (childResult.type === "signal") {
|
||||
// This environment running under `node --test` may not allow rethrowing a signal.
|
||||
// Wrap in a try to avoid masking the original termination reason.
|
||||
try {
|
||||
process.kill(process.pid, childResult.signal);
|
||||
} catch {
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
process.exit(childResult.exitCode);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { OsReleaseInfo } from "./types.js";
|
||||
|
||||
export function parseOsRelease(contents: string): OsReleaseInfo {
|
||||
const lines = contents.split("\n").filter(Boolean);
|
||||
const info: Record<string, string> = {};
|
||||
for (const line of lines) {
|
||||
const [rawKey, rawValue] = line.split("=", 2);
|
||||
if (!rawKey || rawValue === undefined) {
|
||||
continue;
|
||||
}
|
||||
const key = rawKey.toLowerCase();
|
||||
const value = rawValue.replace(/^"/, "").replace(/"$/, "");
|
||||
info[key] = value;
|
||||
}
|
||||
const idLike = (info.id_like || "")
|
||||
.split(/\s+/)
|
||||
.map((item) => item.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
return {
|
||||
id: (info.id || "").toLowerCase(),
|
||||
idLike,
|
||||
versionId: info.version_id || "",
|
||||
};
|
||||
}
|
||||
|
||||
export function readOsRelease(pathname = "/etc/os-release"): OsReleaseInfo {
|
||||
try {
|
||||
const contents = readFileSync(pathname, "utf8");
|
||||
return parseOsRelease(contents);
|
||||
} catch {
|
||||
return { id: "", idLike: [], versionId: "" };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export function resolveTargetTriple(
|
||||
platform: NodeJS.Platform,
|
||||
arch: NodeJS.Architecture,
|
||||
): string {
|
||||
if (platform === "linux") {
|
||||
if (arch === "x64") {
|
||||
return "x86_64-unknown-linux-musl";
|
||||
}
|
||||
if (arch === "arm64") {
|
||||
return "aarch64-unknown-linux-musl";
|
||||
}
|
||||
} else if (platform === "darwin") {
|
||||
if (arch === "x64") {
|
||||
return "x86_64-apple-darwin";
|
||||
}
|
||||
if (arch === "arm64") {
|
||||
return "aarch64-apple-darwin";
|
||||
}
|
||||
}
|
||||
throw new Error(`Unsupported platform: ${platform} (${arch})`);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export type LinuxBashVariant = {
|
||||
name: string;
|
||||
ids: string[];
|
||||
versions: string[];
|
||||
};
|
||||
|
||||
export type DarwinBashVariant = {
|
||||
name: string;
|
||||
minDarwin: number;
|
||||
};
|
||||
|
||||
export type OsReleaseInfo = {
|
||||
id: string;
|
||||
idLike: string[];
|
||||
versionId: string;
|
||||
};
|
||||
|
||||
export type BashSelection = {
|
||||
path: string;
|
||||
variant: string;
|
||||
};
|
||||
Reference in New Issue
Block a user