mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
Merge upstream/main: keep local workflows
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use codex_execpolicy::NegativeExamplePassedCheck;
|
||||
use codex_execpolicy::get_default_policy;
|
||||
use codex_execpolicy_legacy::NegativeExamplePassedCheck;
|
||||
use codex_execpolicy_legacy::get_default_policy;
|
||||
|
||||
#[test]
|
||||
fn verify_everything_in_bad_list_is_rejected() {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
extern crate codex_execpolicy;
|
||||
extern crate codex_execpolicy_legacy;
|
||||
|
||||
use codex_execpolicy::ArgMatcher;
|
||||
use codex_execpolicy::ArgType;
|
||||
use codex_execpolicy::Error;
|
||||
use codex_execpolicy::ExecCall;
|
||||
use codex_execpolicy::MatchedArg;
|
||||
use codex_execpolicy::MatchedExec;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_execpolicy::Result;
|
||||
use codex_execpolicy::ValidExec;
|
||||
use codex_execpolicy::get_default_policy;
|
||||
use codex_execpolicy_legacy::ArgMatcher;
|
||||
use codex_execpolicy_legacy::ArgType;
|
||||
use codex_execpolicy_legacy::Error;
|
||||
use codex_execpolicy_legacy::ExecCall;
|
||||
use codex_execpolicy_legacy::MatchedArg;
|
||||
use codex_execpolicy_legacy::MatchedExec;
|
||||
use codex_execpolicy_legacy::Policy;
|
||||
use codex_execpolicy_legacy::Result;
|
||||
use codex_execpolicy_legacy::ValidExec;
|
||||
use codex_execpolicy_legacy::get_default_policy;
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
fn setup() -> Policy {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use codex_execpolicy::PositiveExampleFailedCheck;
|
||||
use codex_execpolicy::get_default_policy;
|
||||
use codex_execpolicy_legacy::PositiveExampleFailedCheck;
|
||||
use codex_execpolicy_legacy::get_default_policy;
|
||||
|
||||
#[test]
|
||||
fn verify_everything_in_good_list_is_allowed() {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use codex_execpolicy::ArgMatcher;
|
||||
use codex_execpolicy::ArgType;
|
||||
use codex_execpolicy::Error;
|
||||
use codex_execpolicy::ExecCall;
|
||||
use codex_execpolicy::MatchedArg;
|
||||
use codex_execpolicy::MatchedExec;
|
||||
use codex_execpolicy::MatchedOpt;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_execpolicy::Result;
|
||||
use codex_execpolicy::ValidExec;
|
||||
use codex_execpolicy::get_default_policy;
|
||||
use codex_execpolicy_legacy::ArgMatcher;
|
||||
use codex_execpolicy_legacy::ArgType;
|
||||
use codex_execpolicy_legacy::Error;
|
||||
use codex_execpolicy_legacy::ExecCall;
|
||||
use codex_execpolicy_legacy::MatchedArg;
|
||||
use codex_execpolicy_legacy::MatchedExec;
|
||||
use codex_execpolicy_legacy::MatchedOpt;
|
||||
use codex_execpolicy_legacy::Policy;
|
||||
use codex_execpolicy_legacy::Result;
|
||||
use codex_execpolicy_legacy::ValidExec;
|
||||
use codex_execpolicy_legacy::get_default_policy;
|
||||
|
||||
extern crate codex_execpolicy;
|
||||
extern crate codex_execpolicy_legacy;
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
fn setup() -> Policy {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use codex_execpolicy::ArgType;
|
||||
use codex_execpolicy::Error;
|
||||
use codex_execpolicy::ExecCall;
|
||||
use codex_execpolicy::MatchedArg;
|
||||
use codex_execpolicy::MatchedExec;
|
||||
use codex_execpolicy::PolicyParser;
|
||||
use codex_execpolicy::Result;
|
||||
use codex_execpolicy::ValidExec;
|
||||
use codex_execpolicy_legacy::ArgType;
|
||||
use codex_execpolicy_legacy::Error;
|
||||
use codex_execpolicy_legacy::ExecCall;
|
||||
use codex_execpolicy_legacy::MatchedArg;
|
||||
use codex_execpolicy_legacy::MatchedExec;
|
||||
use codex_execpolicy_legacy::PolicyParser;
|
||||
use codex_execpolicy_legacy::Result;
|
||||
use codex_execpolicy_legacy::ValidExec;
|
||||
|
||||
extern crate codex_execpolicy;
|
||||
extern crate codex_execpolicy_legacy;
|
||||
|
||||
#[test]
|
||||
fn test_invalid_subcommand() -> Result<()> {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
extern crate codex_execpolicy;
|
||||
extern crate codex_execpolicy_legacy;
|
||||
|
||||
use codex_execpolicy::ArgType;
|
||||
use codex_execpolicy::Error;
|
||||
use codex_execpolicy::ExecCall;
|
||||
use codex_execpolicy::MatchedArg;
|
||||
use codex_execpolicy::MatchedExec;
|
||||
use codex_execpolicy::MatchedFlag;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_execpolicy::Result;
|
||||
use codex_execpolicy::ValidExec;
|
||||
use codex_execpolicy::get_default_policy;
|
||||
use codex_execpolicy_legacy::ArgType;
|
||||
use codex_execpolicy_legacy::Error;
|
||||
use codex_execpolicy_legacy::ExecCall;
|
||||
use codex_execpolicy_legacy::MatchedArg;
|
||||
use codex_execpolicy_legacy::MatchedExec;
|
||||
use codex_execpolicy_legacy::MatchedFlag;
|
||||
use codex_execpolicy_legacy::Policy;
|
||||
use codex_execpolicy_legacy::Result;
|
||||
use codex_execpolicy_legacy::ValidExec;
|
||||
use codex_execpolicy_legacy::get_default_policy;
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
fn setup() -> Policy {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use codex_execpolicy::Error;
|
||||
use codex_execpolicy::parse_sed_command;
|
||||
use codex_execpolicy_legacy::Error;
|
||||
use codex_execpolicy_legacy::parse_sed_command;
|
||||
|
||||
#[test]
|
||||
fn parses_simple_print_command() {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
extern crate codex_execpolicy;
|
||||
extern crate codex_execpolicy_legacy;
|
||||
|
||||
use std::vec;
|
||||
|
||||
use codex_execpolicy::Error;
|
||||
use codex_execpolicy::ExecCall;
|
||||
use codex_execpolicy::MatchedExec;
|
||||
use codex_execpolicy::MatchedFlag;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_execpolicy::PositionalArg;
|
||||
use codex_execpolicy::ValidExec;
|
||||
use codex_execpolicy::get_default_policy;
|
||||
use codex_execpolicy_legacy::Error;
|
||||
use codex_execpolicy_legacy::ExecCall;
|
||||
use codex_execpolicy_legacy::MatchedExec;
|
||||
use codex_execpolicy_legacy::MatchedFlag;
|
||||
use codex_execpolicy_legacy::Policy;
|
||||
use codex_execpolicy_legacy::PositionalArg;
|
||||
use codex_execpolicy_legacy::ValidExec;
|
||||
use codex_execpolicy_legacy::get_default_policy;
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
fn setup() -> Policy {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
extern crate codex_execpolicy;
|
||||
extern crate codex_execpolicy_legacy;
|
||||
|
||||
use codex_execpolicy::ArgType;
|
||||
use codex_execpolicy::Error;
|
||||
use codex_execpolicy::ExecCall;
|
||||
use codex_execpolicy::MatchedArg;
|
||||
use codex_execpolicy::MatchedExec;
|
||||
use codex_execpolicy::MatchedFlag;
|
||||
use codex_execpolicy::MatchedOpt;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_execpolicy::Result;
|
||||
use codex_execpolicy::ValidExec;
|
||||
use codex_execpolicy::get_default_policy;
|
||||
use codex_execpolicy_legacy::ArgType;
|
||||
use codex_execpolicy_legacy::Error;
|
||||
use codex_execpolicy_legacy::ExecCall;
|
||||
use codex_execpolicy_legacy::MatchedArg;
|
||||
use codex_execpolicy_legacy::MatchedExec;
|
||||
use codex_execpolicy_legacy::MatchedFlag;
|
||||
use codex_execpolicy_legacy::MatchedOpt;
|
||||
use codex_execpolicy_legacy::Policy;
|
||||
use codex_execpolicy_legacy::Result;
|
||||
use codex_execpolicy_legacy::ValidExec;
|
||||
use codex_execpolicy_legacy::get_default_policy;
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
fn setup() -> Policy {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Execpolicy quickstart
|
||||
|
||||
Codex can enforce your own rules-based execution policy before it runs shell commands. Policies live in Starlark `.codexpolicy` files under `~/.codex/policy`.
|
||||
|
||||
## Create a policy
|
||||
|
||||
1. Create a policy directory: `mkdir -p ~/.codex/policy`.
|
||||
2. Add one or more `.codexpolicy` files in that folder. Codex automatically loads every `.codexpolicy` file in there on startup.
|
||||
3. Write `prefix_rule` entries to describe the commands you want to allow, prompt, or block:
|
||||
|
||||
```starlark
|
||||
prefix_rule(
|
||||
pattern = ["git", ["push", "fetch"]],
|
||||
decision = "prompt", # allow | prompt | forbidden
|
||||
match = [["git", "push", "origin", "main"]], # examples that must match
|
||||
not_match = [["git", "status"]], # examples that must not match
|
||||
)
|
||||
```
|
||||
|
||||
- `pattern` is a list of shell tokens, evaluated from left to right; wrap tokens in a nested list to express alternatives (for example, match both `push` and `fetch`).
|
||||
- `decision` sets the severity; Codex picks the strictest decision when multiple rules match (forbidden > prompt > allow).
|
||||
- `match` and `not_match` act as optional unit tests. Codex validates them when it loads your policy, so you get feedback if an example has unexpected behavior.
|
||||
|
||||
In this example rule, if Codex wants to run commands with the prefix `git push` or `git fetch`, it will first ask for user approval.
|
||||
|
||||
## Preview decisions
|
||||
|
||||
Use the `codex execpolicy check` subcommand to preview decisions before you save a rule (see the [`codex-execpolicy` README](../codex-rs/execpolicy/README.md) for syntax details):
|
||||
|
||||
```shell
|
||||
codex execpolicy check --policy ~/.codex/policy/default.codexpolicy git push origin main
|
||||
```
|
||||
|
||||
Pass multiple `--policy` flags to test how several files combine, and use `--pretty` for formatted JSON output. See the [`codex-rs/execpolicy` README](../codex-rs/execpolicy/README.md) for a more detailed walkthrough of the available syntax.
|
||||
|
||||
## Status
|
||||
|
||||
`execpolicy` commands are still in preview. The API may have breaking changes in the future.
|
||||
@@ -8,7 +8,7 @@ When commands run via `codex sandbox windows …` (or when the CLI/TUI calls int
|
||||
|
||||
## Known Security Limitations
|
||||
|
||||
Running `python windows-sandbox-rs/sandbox_smoketests.py` with full filesystem and network access currently results in **37/42** passing cases. The list below focuses on the four high-value failures numbered #32 and higher in the smoketests (earlier tests are less security-focused).
|
||||
Running `python windows-sandbox-rs/sandbox_smoketests.py` with full filesystem and network access currently results in **37/41** passing cases. The list below focuses on the four high-value failures numbered #32 and higher in the smoketests (earlier tests are less security-focused).
|
||||
|
||||
| Test | Purpose |
|
||||
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
@@ -19,4 +19,4 @@ Running `python windows-sandbox-rs/sandbox_smoketests.py` with full filesystem a
|
||||
|
||||
## Want to Help?
|
||||
|
||||
If you are a security-minded Windows user, help us get these tests passing! Improved implementations that make these smoke tests pass meaningfully reduce Codex's escape surface. After iterating, rerun `python windows-sandbox-rs/sandbox_smoketests.py` to validate the fixes and help us drive the suite toward 42/42.
|
||||
If you are a security-minded Windows user, help us get these tests passing! Improved implementations that make these smoke tests pass meaningfully reduce Codex's escape surface. After iterating, rerun `python windows-sandbox-rs/sandbox_smoketests.py` to validate the fixes and help us drive the suite toward 41/41.
|
||||
|
||||
Generated
+24
@@ -59,6 +59,30 @@ importers:
|
||||
specifier: ^8.45.0
|
||||
version: 8.45.0([email protected])([email protected])
|
||||
|
||||
shell-tool-mcp:
|
||||
devDependencies:
|
||||
'@types/jest':
|
||||
specifier: ^29.5.14
|
||||
version: 29.5.14
|
||||
'@types/node':
|
||||
specifier: ^20.19.18
|
||||
version: 20.19.18
|
||||
jest:
|
||||
specifier: ^29.7.0
|
||||
version: 29.7.0(@types/[email protected])([email protected](@types/[email protected])([email protected]))
|
||||
prettier:
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.2
|
||||
ts-jest:
|
||||
specifier: ^29.3.4
|
||||
version: 29.4.4(@babel/[email protected])(@jest/[email protected])(@jest/[email protected])([email protected](@babel/[email protected]))([email protected])([email protected])([email protected](@types/[email protected])([email protected](@types/[email protected])([email protected])))([email protected])
|
||||
tsup:
|
||||
specifier: ^8.5.0
|
||||
version: 8.5.0([email protected])([email protected])([email protected])
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.2
|
||||
|
||||
packages:
|
||||
|
||||
'@babel/[email protected]':
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
packages:
|
||||
- docs
|
||||
- sdk/typescript
|
||||
- shell-tool-mcp
|
||||
|
||||
ignoredBuiltDependencies:
|
||||
- esbuild
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/bin/
|
||||
/node_modules/
|
||||
@@ -0,0 +1,31 @@
|
||||
# @openai/codex-shell-tool-mcp
|
||||
|
||||
This package wraps the `codex-exec-mcp-server` binary and its helpers so that the shell MCP can be invoked via `npx @openai/codex-shell-tool-mcp`. It bundles:
|
||||
|
||||
- `codex-exec-mcp-server` and `codex-execve-wrapper` built for macOS (arm64, x64) and Linux (musl arm64, musl x64).
|
||||
- A patched Bash that honors `BASH_EXEC_WRAPPER`, built for multiple glibc baselines (Ubuntu 24.04/22.04/20.04, Debian 12/11, CentOS-like 9) and macOS (15/14/13).
|
||||
- A launcher (`bin/mcp-server.js`) that picks the correct binaries for the current `process.platform` / `process.arch`, specifying `--execve` and `--bash` for the MCP, as appropriate.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
npx @openai/codex-shell-tool-mcp --help
|
||||
```
|
||||
|
||||
The launcher selects a Rust target triple based on the host and chooses the closest Bash variant by inspecting `/etc/os-release` on Linux or the Darwin major version on macOS.
|
||||
|
||||
## Patched Bash
|
||||
|
||||
We carry a small patch to `execute_cmd.c` (see `patches/bash-exec-wrapper.patch`) that adds support for `BASH_EXEC_WRAPPER`. The original commit message is “add support for BASH_EXEC_WRAPPER” and the patch applies cleanly to `a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b` from https://github.com/bminor/bash. To rebuild manually:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/bminor/bash
|
||||
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
|
||||
git apply /path/to/patches/bash-exec-wrapper.patch
|
||||
./configure --without-bash-malloc
|
||||
make -j"$(nproc)"
|
||||
```
|
||||
|
||||
## Release workflow
|
||||
|
||||
`.github/workflows/shell-tool-mcp.yml` builds the Rust binaries, compiles the patched Bash variants, assembles the `vendor/` tree, and creates `codex-shell-tool-mcp-npm-<version>.tgz` for inclusion in the Rust GitHub Release. When the version is a stable or alpha tag, the workflow also publishes the tarball to npm using OIDC. The workflow is invoked from `rust-release.yml` so the package ships alongside other Codex artifacts.
|
||||
@@ -0,0 +1,6 @@
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
roots: ["<rootDir>/tests"],
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@openai/codex-shell-tool-mcp",
|
||||
"version": "0.0.0-dev",
|
||||
"description": "Codex MCP server for the shell tool with patched Bash and exec wrappers.",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"codex-shell-tool-mcp": "bin/mcp-server.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"vendor",
|
||||
"README.md"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/openai/codex.git",
|
||||
"directory": "shell-tool-mcp"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf bin",
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"format": "prettier --check .",
|
||||
"format:fix": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.19.18",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.6.2",
|
||||
"ts-jest": "^29.3.4",
|
||||
"tsup": "^8.5.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
diff --git a/execute_cmd.c b/execute_cmd.c
|
||||
index 070f5119..d20ad2b9 100644
|
||||
--- a/execute_cmd.c
|
||||
+++ b/execute_cmd.c
|
||||
@@ -6129,6 +6129,19 @@ shell_execve (char *command, char **args, char **env)
|
||||
char sample[HASH_BANG_BUFSIZ];
|
||||
size_t larray;
|
||||
|
||||
+ char* exec_wrapper = getenv("BASH_EXEC_WRAPPER");
|
||||
+ if (exec_wrapper && *exec_wrapper && !whitespace (*exec_wrapper))
|
||||
+ {
|
||||
+ char *orig_command = command;
|
||||
+
|
||||
+ larray = strvec_len (args);
|
||||
+
|
||||
+ memmove (args + 2, args, (++larray) * sizeof (char *));
|
||||
+ args[0] = exec_wrapper;
|
||||
+ args[1] = orig_command;
|
||||
+ command = exec_wrapper;
|
||||
+ }
|
||||
+
|
||||
SETOSTYPE (0); /* Some systems use for USG/POSIX semantics */
|
||||
execve (command, args, env);
|
||||
i = errno; /* error from execve() */
|
||||
@@ -0,0 +1,115 @@
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import { DARWIN_BASH_VARIANTS, LINUX_BASH_VARIANTS } from "./constants";
|
||||
import { BashSelection, OsReleaseInfo } from "./types";
|
||||
|
||||
function supportedDetail(variants: ReadonlyArray<{ 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";
|
||||
|
||||
export const LINUX_BASH_VARIANTS: ReadonlyArray<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: ReadonlyArray<DarwinBashVariant> = [
|
||||
{ name: "macos-15", minDarwin: 24 },
|
||||
{ name: "macos-14", minDarwin: 23 },
|
||||
{ name: "macos-13", minDarwin: 22 },
|
||||
];
|
||||
@@ -0,0 +1,101 @@
|
||||
// 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 { resolveBashPath } from "./bashSelection";
|
||||
import { readOsRelease } from "./osRelease";
|
||||
import { resolveTargetTriple } from "./platform";
|
||||
|
||||
const scriptPath = process.argv[1]
|
||||
? path.resolve(process.argv[1])
|
||||
: process.cwd();
|
||||
const __dirname = path.dirname(scriptPath);
|
||||
|
||||
async function main(): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void main().catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { OsReleaseInfo } from "./types";
|
||||
|
||||
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: Array<string>;
|
||||
versions: Array<string>;
|
||||
};
|
||||
|
||||
export type DarwinBashVariant = {
|
||||
name: string;
|
||||
minDarwin: number;
|
||||
};
|
||||
|
||||
export type OsReleaseInfo = {
|
||||
id: string;
|
||||
idLike: Array<string>;
|
||||
versionId: string;
|
||||
};
|
||||
|
||||
export type BashSelection = {
|
||||
path: string;
|
||||
variant: string;
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { selectDarwinBash, selectLinuxBash } from "../src/bashSelection";
|
||||
import { DARWIN_BASH_VARIANTS, LINUX_BASH_VARIANTS } from "../src/constants";
|
||||
import { OsReleaseInfo } from "../src/types";
|
||||
import path from "node:path";
|
||||
|
||||
describe("selectLinuxBash", () => {
|
||||
const bashRoot = "/vendor/bash";
|
||||
|
||||
it("prefers exact version match when id is present", () => {
|
||||
const info: OsReleaseInfo = {
|
||||
id: "ubuntu",
|
||||
idLike: ["debian"],
|
||||
versionId: "24.04.1",
|
||||
};
|
||||
const selection = selectLinuxBash(bashRoot, info);
|
||||
expect(selection.variant).toBe("ubuntu-24.04");
|
||||
expect(selection.path).toBe(path.join(bashRoot, "ubuntu-24.04", "bash"));
|
||||
});
|
||||
|
||||
it("falls back to first supported variant when no matches", () => {
|
||||
const info: OsReleaseInfo = { id: "unknown", idLike: [], versionId: "1.0" };
|
||||
const selection = selectLinuxBash(bashRoot, info);
|
||||
expect(selection.variant).toBe(LINUX_BASH_VARIANTS[0].name);
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectDarwinBash", () => {
|
||||
const bashRoot = "/vendor/bash";
|
||||
|
||||
it("selects compatible darwin version", () => {
|
||||
const darwinRelease = "24.0.0";
|
||||
const selection = selectDarwinBash(bashRoot, darwinRelease);
|
||||
expect(selection.variant).toBe("macos-15");
|
||||
});
|
||||
|
||||
it("falls back to first darwin variant when release too old", () => {
|
||||
const darwinRelease = "20.0.0";
|
||||
const selection = selectDarwinBash(bashRoot, darwinRelease);
|
||||
expect(selection.variant).toBe(DARWIN_BASH_VARIANTS[0].name);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { parseOsRelease } from "../src/osRelease";
|
||||
|
||||
describe("parseOsRelease", () => {
|
||||
it("parses basic fields", () => {
|
||||
const contents = `ID="ubuntu"
|
||||
ID_LIKE="debian"
|
||||
VERSION_ID=24.04
|
||||
OTHER=ignored`;
|
||||
|
||||
const info = parseOsRelease(contents);
|
||||
expect(info).toEqual({
|
||||
id: "ubuntu",
|
||||
idLike: ["debian"],
|
||||
versionId: "24.04",
|
||||
});
|
||||
});
|
||||
|
||||
it("handles missing fields", () => {
|
||||
const contents = "SOMETHING=else";
|
||||
const info = parseOsRelease(contents);
|
||||
expect(info).toEqual({ id: "", idLike: [], versionId: "" });
|
||||
});
|
||||
|
||||
it("normalizes id_like entries", () => {
|
||||
const contents = `ID="rhel"
|
||||
ID_LIKE="CentOS Rocky"`;
|
||||
const info = parseOsRelease(contents);
|
||||
expect(info.idLike).toEqual(["centos", "rocky"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src", "tests"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
"mcp-server": "src/index.ts",
|
||||
},
|
||||
outDir: "bin",
|
||||
format: ["cjs"],
|
||||
target: "node18",
|
||||
clean: true,
|
||||
sourcemap: false,
|
||||
banner: {
|
||||
js: "#!/usr/bin/env node",
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user