mirror of
https://github.com/openai/codex.git
synced 2026-11-03 19:53:25 +00:00
fix: delete shell-tool-mcp
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"outputs": {
|
||||
"codex-zsh": {
|
||||
"platforms": {
|
||||
"macos-aarch64": {
|
||||
"name": "codex-zsh-aarch64-apple-darwin.tar.gz",
|
||||
"format": "tar.gz",
|
||||
"path": "codex-zsh/bin/zsh"
|
||||
},
|
||||
"linux-x86_64": {
|
||||
"name": "codex-zsh-x86_64-unknown-linux-musl.tar.gz",
|
||||
"format": "tar.gz",
|
||||
"path": "codex-zsh/bin/zsh"
|
||||
},
|
||||
"linux-aarch64": {
|
||||
"name": "codex-zsh-aarch64-unknown-linux-musl.tar.gz",
|
||||
"format": "tar.gz",
|
||||
"path": "codex-zsh/bin/zsh"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$#" -ne 1 ]]; then
|
||||
echo "usage: $0 <archive-path>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
archive_path="$1"
|
||||
workspace="${GITHUB_WORKSPACE:?missing GITHUB_WORKSPACE}"
|
||||
zsh_commit="${ZSH_COMMIT:?missing ZSH_COMMIT}"
|
||||
zsh_patch="${ZSH_PATCH:?missing ZSH_PATCH}"
|
||||
temp_root="${RUNNER_TEMP:-/tmp}"
|
||||
work_root="$(mktemp -d "${temp_root%/}/codex-zsh-release.XXXXXX")"
|
||||
trap 'rm -rf "$work_root"' EXIT
|
||||
|
||||
source_root="${work_root}/zsh"
|
||||
package_root="${work_root}/codex-zsh"
|
||||
wrapper_path="${work_root}/exec-wrapper"
|
||||
stdout_path="${work_root}/stdout.txt"
|
||||
wrapper_log_path="${work_root}/wrapper.log"
|
||||
|
||||
git clone https://git.code.sf.net/p/zsh/code "$source_root"
|
||||
cd "$source_root"
|
||||
git checkout "$zsh_commit"
|
||||
git apply "${workspace}/${zsh_patch}"
|
||||
./Util/preconfig
|
||||
./configure
|
||||
|
||||
cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)"
|
||||
make -j"${cores}"
|
||||
|
||||
cat > "$wrapper_path" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
: "${CODEX_WRAPPER_LOG:?missing CODEX_WRAPPER_LOG}"
|
||||
printf '%s\n' "$@" > "$CODEX_WRAPPER_LOG"
|
||||
file="$1"
|
||||
shift
|
||||
if [[ "$#" -eq 0 ]]; then
|
||||
exec "$file"
|
||||
fi
|
||||
arg0="$1"
|
||||
shift
|
||||
exec -a "$arg0" "$file" "$@"
|
||||
EOF
|
||||
chmod +x "$wrapper_path"
|
||||
|
||||
CODEX_WRAPPER_LOG="$wrapper_log_path" \
|
||||
EXEC_WRAPPER="$wrapper_path" \
|
||||
"${source_root}/Src/zsh" -fc '/bin/echo smoke-zsh' > "$stdout_path"
|
||||
|
||||
grep -Fx "smoke-zsh" "$stdout_path"
|
||||
grep -Fx "/bin/echo" "$wrapper_log_path"
|
||||
|
||||
mkdir -p "$package_root/bin" "$(dirname "${workspace}/${archive_path}")"
|
||||
cp "${source_root}/Src/zsh" "$package_root/bin/zsh"
|
||||
chmod +x "$package_root/bin/zsh"
|
||||
|
||||
(cd "$work_root" && tar -czf "${workspace}/${archive_path}" codex-zsh)
|
||||
@@ -0,0 +1,95 @@
|
||||
name: rust-release-zsh
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
ZSH_COMMIT: 77045ef899e53b9598bebc5a41db93a548a40ca6
|
||||
ZSH_PATCH: codex-rs/shell-escalation/patches/zsh-exec-wrapper.patch
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Build zsh (Linux) - ${{ matrix.variant }} - ${{ matrix.target }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 30
|
||||
container:
|
||||
image: ${{ matrix.image }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
variant: ubuntu-24.04
|
||||
image: ubuntu:24.04
|
||||
archive_name: codex-zsh-x86_64-unknown-linux-musl.tar.gz
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
variant: ubuntu-24.04
|
||||
image: arm64v8/ubuntu:24.04
|
||||
archive_name: codex-zsh-aarch64-unknown-linux-musl.tar.gz
|
||||
|
||||
steps:
|
||||
- name: Install build prerequisites
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
autoconf \
|
||||
bison \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
gettext \
|
||||
git \
|
||||
libncursesw5-dev
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Build, smoke-test, and stage zsh artifact
|
||||
shell: bash
|
||||
run: |
|
||||
bash "${GITHUB_WORKSPACE}/.github/scripts/build-zsh-release-artifact.sh" \
|
||||
"dist/zsh/${{ matrix.target }}/${{ matrix.archive_name }}"
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: codex-zsh-${{ matrix.target }}
|
||||
path: dist/zsh/${{ matrix.target }}/*
|
||||
|
||||
darwin:
|
||||
name: Build zsh (macOS) - ${{ matrix.variant }} - ${{ matrix.target }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 30
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-xlarge
|
||||
target: aarch64-apple-darwin
|
||||
variant: macos-15
|
||||
archive_name: codex-zsh-aarch64-apple-darwin.tar.gz
|
||||
|
||||
steps:
|
||||
- name: Install build prerequisites
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! command -v autoconf >/dev/null 2>&1; then
|
||||
brew install autoconf
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Build, smoke-test, and stage zsh artifact
|
||||
shell: bash
|
||||
run: |
|
||||
bash "${GITHUB_WORKSPACE}/.github/scripts/build-zsh-release-artifact.sh" \
|
||||
"dist/zsh/${{ matrix.target }}/${{ matrix.archive_name }}"
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: codex-zsh-${{ matrix.target }}
|
||||
path: dist/zsh/${{ matrix.target }}/*
|
||||
@@ -389,15 +389,6 @@ jobs:
|
||||
release-lto: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'fat' }}
|
||||
secrets: inherit
|
||||
|
||||
shell-tool-mcp:
|
||||
name: shell-tool-mcp
|
||||
needs: tag-check
|
||||
uses: ./.github/workflows/shell-tool-mcp.yml
|
||||
with:
|
||||
release-tag: ${{ github.ref_name }}
|
||||
publish: true
|
||||
secrets: inherit
|
||||
|
||||
argument-comment-lint-release-assets:
|
||||
name: argument-comment-lint release assets
|
||||
needs: tag-check
|
||||
@@ -405,12 +396,17 @@ jobs:
|
||||
with:
|
||||
publish: true
|
||||
|
||||
zsh-release-assets:
|
||||
name: zsh release assets
|
||||
needs: tag-check
|
||||
uses: ./.github/workflows/rust-release-zsh.yml
|
||||
|
||||
release:
|
||||
needs:
|
||||
- build
|
||||
- build-windows
|
||||
- shell-tool-mcp
|
||||
- argument-comment-lint-release-assets
|
||||
- zsh-release-assets
|
||||
name: release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -453,11 +449,8 @@ jobs:
|
||||
- name: List
|
||||
run: ls -R dist/
|
||||
|
||||
# This is a temporary fix: we should modify shell-tool-mcp.yml so these
|
||||
# files do not end up in dist/ in the first place.
|
||||
- name: Delete entries from dist/ that should not go in the release
|
||||
run: |
|
||||
rm -rf dist/shell-tool-mcp*
|
||||
rm -rf dist/windows-binaries*
|
||||
# cargo-timing.html appears under multiple target-specific directories.
|
||||
# If included in files: dist/**, release upload races on duplicate
|
||||
@@ -547,6 +540,13 @@ jobs:
|
||||
tag: ${{ github.ref_name }}
|
||||
config: .github/dotslash-config.json
|
||||
|
||||
- uses: facebook/dotslash-publish-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag: ${{ github.ref_name }}
|
||||
config: .github/dotslash-zsh-config.json
|
||||
|
||||
- uses: facebook/dotslash-publish-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
#!/usr/bin/env dotslash
|
||||
|
||||
// This is an instance of the fork of Bash that we bundle with
|
||||
// https://www.npmjs.com/package/@openai/codex-shell-tool-mcp.
|
||||
// Fetching the prebuilt version via DotSlash makes it easier to write
|
||||
// integration tests for shell execution flows.
|
||||
//
|
||||
// TODO(mbolin): Currently, we use a .tgz artifact that includes binaries for
|
||||
// multiple platforms, but we could save a bit of space by making arch-specific
|
||||
// artifacts available in the GitHub releases and referencing those here.
|
||||
{
|
||||
"name": "codex-bash",
|
||||
"platforms": {
|
||||
// macOS 13 builds (and therefore x86_64) were dropped in
|
||||
// https://github.com/openai/codex/pull/7295, so we only provide an
|
||||
// Apple Silicon build for now.
|
||||
"macos-aarch64": {
|
||||
"size": 37003612,
|
||||
"hash": "blake3",
|
||||
"digest": "d9cd5928c993b65c340507931c61c02bd6e9179933f8bf26a548482bb5fa53bb",
|
||||
"format": "tar.gz",
|
||||
"path": "package/vendor/aarch64-apple-darwin/bash/macos-15/bash",
|
||||
"providers": [
|
||||
{
|
||||
"url": "https://github.com/openai/codex/releases/download/rust-v0.65.0/codex-shell-tool-mcp-npm-0.65.0.tgz"
|
||||
},
|
||||
{
|
||||
"type": "github-release",
|
||||
"repo": "openai/codex",
|
||||
"tag": "rust-v0.65.0",
|
||||
"name": "codex-shell-tool-mcp-npm-0.65.0.tgz"
|
||||
}
|
||||
]
|
||||
},
|
||||
// Note the `musl` parts of the Linux paths are misleading: the Bash
|
||||
// binaries are actually linked against `glibc`, but the
|
||||
// `codex-execve-wrapper` that invokes them is linked against `musl`.
|
||||
"linux-x86_64": {
|
||||
"size": 37003612,
|
||||
"hash": "blake3",
|
||||
"digest": "d9cd5928c993b65c340507931c61c02bd6e9179933f8bf26a548482bb5fa53bb",
|
||||
"format": "tar.gz",
|
||||
"path": "package/vendor/x86_64-unknown-linux-musl/bash/ubuntu-24.04/bash",
|
||||
"providers": [
|
||||
{
|
||||
"url": "https://github.com/openai/codex/releases/download/rust-v0.65.0/codex-shell-tool-mcp-npm-0.65.0.tgz"
|
||||
},
|
||||
{
|
||||
"type": "github-release",
|
||||
"repo": "openai/codex",
|
||||
"tag": "rust-v0.65.0",
|
||||
"name": "codex-shell-tool-mcp-npm-0.65.0.tgz"
|
||||
}
|
||||
]
|
||||
},
|
||||
"linux-aarch64": {
|
||||
"size": 37003612,
|
||||
"hash": "blake3",
|
||||
"digest": "d9cd5928c993b65c340507931c61c02bd6e9179933f8bf26a548482bb5fa53bb",
|
||||
"format": "tar.gz",
|
||||
"path": "package/vendor/aarch64-unknown-linux-musl/bash/ubuntu-24.04/bash",
|
||||
"providers": [
|
||||
{
|
||||
"url": "https://github.com/openai/codex/releases/download/rust-v0.65.0/codex-shell-tool-mcp-npm-0.65.0.tgz"
|
||||
},
|
||||
{
|
||||
"type": "github-release",
|
||||
"repo": "openai/codex",
|
||||
"tag": "rust-v0.65.0",
|
||||
"name": "codex-shell-tool-mcp-npm-0.65.0.tgz"
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
#!/usr/bin/env dotslash
|
||||
|
||||
// This is the patched zsh fork built by
|
||||
// `.github/workflows/shell-tool-mcp.yml` for the shell-tool-mcp package.
|
||||
// This is the patched zsh fork corresponding to
|
||||
// `codex-rs/shell-escalation/patches/zsh-exec-wrapper.patch`.
|
||||
// Fetching the prebuilt version via DotSlash makes it easier to write
|
||||
// integration tests that exercise the zsh fork behavior in app-server tests.
|
||||
//
|
||||
// TODO(mbolin): Currently, we use a .tgz artifact that includes binaries for
|
||||
// multiple platforms, but we could save a bit of space by making arch-specific
|
||||
// artifacts available in the GitHub releases and referencing those here.
|
||||
// This checked-in fixture is still pinned to the latest released bundle that
|
||||
// contains this binary. New releases publish standalone `codex-zsh-*.tar.gz`
|
||||
// assets plus a generated `codex-zsh` DotSlash release asset, so this file can
|
||||
// be retargeted when a newer fork build needs to be exercised in tests.
|
||||
{
|
||||
"name": "codex-zsh",
|
||||
"platforms": {
|
||||
|
||||
@@ -899,7 +899,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
|
||||
let mut exec_env = self.env.clone();
|
||||
// `env_overlay` comes from `EscalationSession::env()`, so merge only the
|
||||
// wrapper/socket variables into the base shell environment.
|
||||
for var in ["CODEX_ESCALATE_SOCKET", "EXEC_WRAPPER", "BASH_EXEC_WRAPPER"] {
|
||||
for var in ["CODEX_ESCALATE_SOCKET", "EXEC_WRAPPER"] {
|
||||
if let Some(value) = env_overlay.get(var) {
|
||||
exec_env.insert(var.to_string(), value.clone());
|
||||
}
|
||||
|
||||
@@ -15,14 +15,15 @@ decision to the shell-escalation protocol over a shared file descriptor (specifi
|
||||
- `Deny`: the server has declared the proposed command to be forbidden, so
|
||||
`codex-execve-wrapper` prints an error to `stderr` and exits with `1`.
|
||||
|
||||
## Patched Bash
|
||||
## Patched zsh
|
||||
|
||||
We carry a small patch to `execute_cmd.c` (see `patches/bash-exec-wrapper.patch`) that adds support for `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:
|
||||
We carry a small patch to `Src/exec.c` (see `patches/zsh-exec-wrapper.patch`) that adds support for `EXEC_WRAPPER`. The patch applies to `77045ef899e53b9598bebc5a41db93a548a40ca6` from https://sourceforge.net/p/zsh/code/ci/master/tree/. To rebuild manually:
|
||||
|
||||
```bash
|
||||
git clone https://git.savannah.gnu.org/git/bash
|
||||
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
|
||||
git apply /path/to/patches/bash-exec-wrapper.patch
|
||||
./configure --without-bash-malloc
|
||||
git clone https://git.code.sf.net/p/zsh/code
|
||||
git checkout 77045ef899e53b9598bebc5a41db93a548a40ca6
|
||||
git apply /path/to/patches/zsh-exec-wrapper.patch
|
||||
./Util/preconfig
|
||||
./configure
|
||||
make -j"$(nproc)"
|
||||
```
|
||||
|
||||
@@ -11,7 +11,6 @@ use crate::unix::escalate_protocol::EXEC_WRAPPER_ENV_VAR;
|
||||
use crate::unix::escalate_protocol::EscalateAction;
|
||||
use crate::unix::escalate_protocol::EscalateRequest;
|
||||
use crate::unix::escalate_protocol::EscalateResponse;
|
||||
use crate::unix::escalate_protocol::LEGACY_BASH_EXEC_WRAPPER_ENV_VAR;
|
||||
use crate::unix::escalate_protocol::SuperExecMessage;
|
||||
use crate::unix::escalate_protocol::SuperExecResult;
|
||||
use crate::unix::socket::AsyncDatagramSocket;
|
||||
@@ -46,12 +45,7 @@ pub async fn run_shell_escalation_execve_wrapper(
|
||||
.await
|
||||
.context("failed to send handshake datagram")?;
|
||||
let env = std::env::vars()
|
||||
.filter(|(k, _)| {
|
||||
!matches!(
|
||||
k.as_str(),
|
||||
ESCALATE_SOCKET_ENV_VAR | EXEC_WRAPPER_ENV_VAR | LEGACY_BASH_EXEC_WRAPPER_ENV_VAR
|
||||
)
|
||||
})
|
||||
.filter(|(k, _)| !matches!(k.as_str(), ESCALATE_SOCKET_ENV_VAR | EXEC_WRAPPER_ENV_VAR))
|
||||
.collect();
|
||||
client
|
||||
.send(EscalateRequest {
|
||||
|
||||
@@ -13,9 +13,6 @@ pub const ESCALATE_SOCKET_ENV_VAR: &str = "CODEX_ESCALATE_SOCKET";
|
||||
/// Patched shells use this to wrap exec() calls.
|
||||
pub const EXEC_WRAPPER_ENV_VAR: &str = "EXEC_WRAPPER";
|
||||
|
||||
/// Compatibility alias for older patched bash builds.
|
||||
pub const LEGACY_BASH_EXEC_WRAPPER_ENV_VAR: &str = "BASH_EXEC_WRAPPER";
|
||||
|
||||
/// The client sends this to the server to request an exec() call.
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
pub struct EscalateRequest {
|
||||
|
||||
@@ -20,7 +20,6 @@ use crate::unix::escalate_protocol::EscalateRequest;
|
||||
use crate::unix::escalate_protocol::EscalateResponse;
|
||||
use crate::unix::escalate_protocol::EscalationDecision;
|
||||
use crate::unix::escalate_protocol::EscalationExecution;
|
||||
use crate::unix::escalate_protocol::LEGACY_BASH_EXEC_WRAPPER_ENV_VAR;
|
||||
use crate::unix::escalate_protocol::SuperExecMessage;
|
||||
use crate::unix::escalate_protocol::SuperExecResult;
|
||||
use crate::unix::escalation_policy::EscalationPolicy;
|
||||
@@ -64,13 +63,13 @@ pub trait ShellCommandExecutor: Send + Sync {
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct ExecParams {
|
||||
/// The the string of Zsh/shell to execute.
|
||||
/// The string of shell code to execute.
|
||||
pub command: String,
|
||||
/// The working directory to execute the command in. Must be an absolute path.
|
||||
pub workdir: String,
|
||||
/// The timeout for the command in milliseconds.
|
||||
pub timeout_ms: Option<u64>,
|
||||
/// Launch Bash with -lc instead of -c: defaults to true.
|
||||
/// Launch the shell with -lc instead of -c: defaults to true.
|
||||
pub login: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -126,18 +125,18 @@ impl Drop for EscalationSession {
|
||||
}
|
||||
|
||||
pub struct EscalateServer {
|
||||
bash_path: PathBuf,
|
||||
shell_path: PathBuf,
|
||||
execve_wrapper: PathBuf,
|
||||
policy: Arc<dyn EscalationPolicy>,
|
||||
}
|
||||
|
||||
impl EscalateServer {
|
||||
pub fn new<Policy>(bash_path: PathBuf, execve_wrapper: PathBuf, policy: Policy) -> Self
|
||||
pub fn new<Policy>(shell_path: PathBuf, execve_wrapper: PathBuf, policy: Policy) -> Self
|
||||
where
|
||||
Policy: EscalationPolicy + Send + Sync + 'static,
|
||||
{
|
||||
Self {
|
||||
bash_path,
|
||||
shell_path,
|
||||
execve_wrapper,
|
||||
policy: Arc::new(policy),
|
||||
}
|
||||
@@ -153,7 +152,7 @@ impl EscalateServer {
|
||||
let env_overlay = session.env().clone();
|
||||
let client_socket = Arc::clone(&session.client_socket);
|
||||
let command = vec calls). For each call, it makes a request back to the MCP server to determine whether to allow the proposed command to execute. It also has the option of _escalating_ the command to run unprivileged outside of the sandbox governing the Bash process.
|
||||
|
||||
The user can use [Codex `.rules`](https://developers.openai.com/codex/local-config#rules-preview) files to define how a command should be handled. The action to take is determined by the `decision` parameter of a matching rule as follows:
|
||||
|
||||
- `allow`: the command will be _escalated_ and run outside the sandbox
|
||||
- `prompt`: the command will be subject to human approval via an [MCP elicitation](https://modelcontextprotocol.io/specification/draft/client/elicitation) (it will run _escalated_ if approved)
|
||||
- `forbidden`: the command will fail with exit code `1` and an error message will be written to `stderr`
|
||||
|
||||
Commands that do not match an explicit rule in `.rules` will be allowed to run as-is, though they will still be subject to the sandbox applied to the parent Bash process.
|
||||
|
||||
## Motivation
|
||||
|
||||
When a software agent asks if it is safe to run a command like `ls`, without more context, it is unclear whether it will result in executing `/bin/ls`. Consider:
|
||||
|
||||
- There could be another executable named `ls` that appears before `/bin/ls` on the `$PATH`.
|
||||
- `ls` could be mapped to a shell alias or function.
|
||||
|
||||
Because `@openai/codex-shell-tool-mcp` intercepts `execve(2)` calls directly, it _always_ knows the full path to the program being executed. In turn, this makes it possible to provide stronger guarantees on how [Codex `.rules`](https://developers.openai.com/codex/local-config#rules-preview) are enforced.
|
||||
|
||||
## Usage
|
||||
|
||||
First, verify that you can download and run the MCP executable:
|
||||
|
||||
```bash
|
||||
npx -y @openai/codex-shell-tool-mcp --version
|
||||
```
|
||||
|
||||
To test out the MCP with a one-off invocation of Codex CLI, it is important to _disable_ the default shell tool in addition to enabling the MCP so Codex has exactly one shell-like tool available to it:
|
||||
|
||||
```bash
|
||||
codex --disable shell_tool \
|
||||
--config 'mcp_servers.bash={command = "npx", args = ["-y", "@openai/codex-shell-tool-mcp"]}'
|
||||
```
|
||||
|
||||
To configure this permanently so you can use the MCP while running `codex` without additional command-line flags, add the following to your `~/.codex/config.toml`:
|
||||
|
||||
```toml
|
||||
[features]
|
||||
shell_tool = false
|
||||
|
||||
[mcp_servers.shell-tool]
|
||||
command = "npx"
|
||||
args = ["-y", "@openai/codex-shell-tool-mcp"]
|
||||
```
|
||||
|
||||
Note when the `@openai/codex-shell-tool-mcp` launcher runs, it selects the appropriate native binary to run based on the host OS/architecture. For the Bash wrapper, it inspects `/etc/os-release` on Linux or the Darwin major version on macOS to try to find the best match it has available. See [`bashSelection.ts`](https://github.com/openai/codex/blob/main/shell-tool-mcp/src/bashSelection.ts) for details.
|
||||
|
||||
## MCP Client Requirements
|
||||
|
||||
This MCP server is designed to be used with [Codex](https://developers.openai.com/codex/cli), as it declares the following `capability` that Codex supports when acting as an MCP client:
|
||||
|
||||
```json
|
||||
{
|
||||
"capabilities": {
|
||||
"experimental": {
|
||||
"codex/sandbox-state": {
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This capability means the MCP server honors requests like the following to update the sandbox policy the MCP server uses when spawning Bash:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "req-42",
|
||||
"method": "codex/sandbox-state/update",
|
||||
"params": {
|
||||
"sandboxPolicy": {
|
||||
"type": "workspace-write",
|
||||
"writable_roots": ["/home/user/code/codex"],
|
||||
"network_access": false,
|
||||
"exclude_tmpdir_env_var": false,
|
||||
"exclude_slash_tmp": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Once the server has processed the update, it sends an empty response to acknowledge the request:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "req-42",
|
||||
"result": {}
|
||||
}
|
||||
```
|
||||
|
||||
The Codex harness (used by the CLI and the VS Code extension) sends such requests to MCP servers that declare the `codex/sandbox-state` capability.
|
||||
|
||||
## Package Contents
|
||||
|
||||
This package currently publishes shell binaries only. It bundles:
|
||||
|
||||
- A patched Bash that honors `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 patched zsh with `EXEC_WRAPPER` support for the same supported target triples.
|
||||
|
||||
It does not currently include the Rust MCP server binaries.
|
||||
|
||||
See [the README in the Codex repo](https://github.com/openai/codex/blob/main/codex-rs/shell-escalation/README.md) for details.
|
||||
@@ -1,6 +0,0 @@
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
roots: ["<rootDir>/tests"],
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"name": "@openai/codex-shell-tool-mcp",
|
||||
"version": "0.0.0-dev",
|
||||
"description": "Patched Bash and Zsh binaries for Codex shell execution.",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"files": [
|
||||
"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"
|
||||
},
|
||||
"packageManager": "pnpm@10.29.3+sha512.498e1fb4cca5aa06c1dcf2611e6fafc50972ffe7189998c409e90de74566444298ffe43e6cd2acdc775ba1aa7cc5e092a8b7054c811ba8c5770f84693d33d2dc"
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
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("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() */
|
||||
@@ -1,115 +0,0 @@
|
||||
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}`);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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 },
|
||||
];
|
||||
@@ -1,29 +0,0 @@
|
||||
// Reports the path to the appropriate Bash binary bundled in this package.
|
||||
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { resolveBashPath } from "./bashSelection";
|
||||
import { readOsRelease } from "./osRelease";
|
||||
import { resolveTargetTriple } from "./platform";
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const targetTriple = resolveTargetTriple(process.platform, process.arch);
|
||||
const vendorRoot = path.resolve(__dirname, "..", "vendor");
|
||||
const targetRoot = path.join(vendorRoot, targetTriple);
|
||||
|
||||
const osInfo = process.platform === "linux" ? readOsRelease() : null;
|
||||
const { path: bashPath } = resolveBashPath(
|
||||
targetRoot,
|
||||
process.platform,
|
||||
os.release(),
|
||||
osInfo,
|
||||
);
|
||||
|
||||
console.log(`Platform Bash is: ${bashPath}`);
|
||||
}
|
||||
|
||||
void main().catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
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: "" };
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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})`);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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;
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
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"]);
|
||||
});
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src", "tests"]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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