diff --git a/.github/dotslash-zsh-config.json b/.github/dotslash-zsh-config.json new file mode 100644 index 00000000000..db2c4164015 --- /dev/null +++ b/.github/dotslash-zsh-config.json @@ -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" + } + } + } + } +} diff --git a/.github/scripts/build-zsh-release-artifact.sh b/.github/scripts/build-zsh-release-artifact.sh new file mode 100755 index 00000000000..4fc3db3903e --- /dev/null +++ b/.github/scripts/build-zsh-release-artifact.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ "$#" -ne 1 ]]; then + echo "usage: $0 " >&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) diff --git a/.github/workflows/rust-release-zsh.yml b/.github/workflows/rust-release-zsh.yml new file mode 100644 index 00000000000..a0f71aa736d --- /dev/null +++ b/.github/workflows/rust-release-zsh.yml @@ -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: | + "${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: | + "${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 }}/* diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index f7f650b8c14..1ec9bd28bad 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -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 }} diff --git a/.github/workflows/shell-tool-mcp-ci.yml b/.github/workflows/shell-tool-mcp-ci.yml deleted file mode 100644 index 62a61efb933..00000000000 --- a/.github/workflows/shell-tool-mcp-ci.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: shell-tool-mcp CI - -on: - push: - paths: - - "shell-tool-mcp/**" - - ".github/workflows/shell-tool-mcp-ci.yml" - - "pnpm-lock.yaml" - - "pnpm-workspace.yaml" - pull_request: - paths: - - "shell-tool-mcp/**" - - ".github/workflows/shell-tool-mcp-ci.yml" - - "pnpm-lock.yaml" - - "pnpm-workspace.yaml" - -env: - NODE_VERSION: 22 - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Setup pnpm - uses: pnpm/action-setup@v5 - with: - run_install: false - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: ${{ env.NODE_VERSION }} - cache: "pnpm" - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Format check - run: pnpm --filter @openai/codex-shell-tool-mcp run format - - - name: Run tests - run: pnpm --filter @openai/codex-shell-tool-mcp test - - - name: Build - run: pnpm --filter @openai/codex-shell-tool-mcp run build diff --git a/.github/workflows/shell-tool-mcp.yml b/.github/workflows/shell-tool-mcp.yml deleted file mode 100644 index 71d1725caf9..00000000000 --- a/.github/workflows/shell-tool-mcp.yml +++ /dev/null @@ -1,553 +0,0 @@ -name: shell-tool-mcp - -on: - workflow_call: - inputs: - release-version: - description: Version to publish (x.y.z or x.y.z-alpha.N). Defaults to GITHUB_REF_NAME when it starts with rust-v. - required: false - type: string - release-tag: - description: Tag name to use when downloading release artifacts (defaults to rust-v). - required: false - type: string - publish: - description: Whether to publish to npm when the version is releasable. - required: false - default: true - type: boolean - -env: - NODE_VERSION: 22 - -jobs: - metadata: - runs-on: ubuntu-latest - outputs: - version: ${{ steps.compute.outputs.version }} - release_tag: ${{ steps.compute.outputs.release_tag }} - should_publish: ${{ steps.compute.outputs.should_publish }} - npm_tag: ${{ steps.compute.outputs.npm_tag }} - steps: - - name: Compute version and tags - id: compute - env: - RELEASE_TAG_INPUT: ${{ inputs.release-tag }} - RELEASE_VERSION_INPUT: ${{ inputs.release-version }} - run: | - set -euo pipefail - - version="$RELEASE_VERSION_INPUT" - release_tag="$RELEASE_TAG_INPUT" - - if [[ -z "$version" ]]; then - if [[ -n "$release_tag" && "$release_tag" =~ ^rust-v.+ ]]; then - version="${release_tag#rust-v}" - elif [[ "${GITHUB_REF_NAME:-}" =~ ^rust-v.+ ]]; then - version="${GITHUB_REF_NAME#rust-v}" - release_tag="${GITHUB_REF_NAME}" - else - echo "release-version is required when GITHUB_REF_NAME is not a rust-v tag." - exit 1 - fi - fi - - if [[ -z "$release_tag" ]]; then - release_tag="rust-v${version}" - fi - - npm_tag="" - should_publish="false" - if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - should_publish="true" - elif [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then - should_publish="true" - npm_tag="alpha" - fi - - echo "version=${version}" >> "$GITHUB_OUTPUT" - echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT" - echo "npm_tag=${npm_tag}" >> "$GITHUB_OUTPUT" - echo "should_publish=${should_publish}" >> "$GITHUB_OUTPUT" - - bash-linux: - name: Build Bash (Linux) - ${{ matrix.variant }} - ${{ matrix.target }} - needs: metadata - 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 - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-musl - variant: ubuntu-22.04 - image: ubuntu:22.04 - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-musl - variant: debian-12 - image: debian:12 - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-musl - variant: debian-11 - image: debian:11 - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-musl - variant: centos-9 - image: quay.io/centos/centos:stream9 - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - variant: ubuntu-24.04 - image: arm64v8/ubuntu:24.04 - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - variant: ubuntu-22.04 - image: arm64v8/ubuntu:22.04 - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - variant: ubuntu-20.04 - image: arm64v8/ubuntu:20.04 - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - variant: debian-12 - image: arm64v8/debian:12 - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - variant: debian-11 - image: arm64v8/debian:11 - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - variant: centos-9 - image: quay.io/centos/centos:stream9 - steps: - - name: Install build prerequisites - shell: bash - run: | - set -euo pipefail - if command -v apt-get >/dev/null 2>&1; then - apt-get update - DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential bison autoconf gettext libncursesw5-dev - elif command -v dnf >/dev/null 2>&1; then - dnf install -y git gcc gcc-c++ make bison autoconf gettext ncurses-devel - elif command -v yum >/dev/null 2>&1; then - yum install -y git gcc gcc-c++ make bison autoconf gettext ncurses-devel - else - echo "Unsupported package manager in container" - exit 1 - fi - - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Build patched Bash - shell: bash - run: | - set -euo pipefail - git clone https://git.savannah.gnu.org/git/bash /tmp/bash - cd /tmp/bash - git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b - git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" - ./configure --without-bash-malloc - cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)" - make -j"${cores}" - - dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" - mkdir -p "$dest" - cp bash "$dest/bash" - - - uses: actions/upload-artifact@v7 - with: - name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} - path: artifacts/** - if-no-files-found: error - - bash-darwin: - name: Build Bash (macOS) - ${{ matrix.variant }} - ${{ matrix.target }} - needs: metadata - runs-on: ${{ matrix.runner }} - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - include: - - runner: macos-15-xlarge - target: aarch64-apple-darwin - variant: macos-15 - - runner: macos-14 - target: aarch64-apple-darwin - variant: macos-14 - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Build patched Bash - shell: bash - run: | - set -euo pipefail - git clone https://git.savannah.gnu.org/git/bash /tmp/bash - cd /tmp/bash - git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b - git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch" - ./configure --without-bash-malloc - cores="$(getconf _NPROCESSORS_ONLN)" - make -j"${cores}" - - dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}" - mkdir -p "$dest" - cp bash "$dest/bash" - - - uses: actions/upload-artifact@v7 - with: - name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }} - path: artifacts/** - if-no-files-found: error - - zsh-linux: - name: Build zsh (Linux) - ${{ matrix.variant }} - ${{ matrix.target }} - needs: metadata - 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 - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-musl - variant: ubuntu-22.04 - image: ubuntu:22.04 - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-musl - variant: debian-12 - image: debian:12 - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-musl - variant: debian-11 - image: debian:11 - - runner: ubuntu-24.04 - target: x86_64-unknown-linux-musl - variant: centos-9 - image: quay.io/centos/centos:stream9 - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - variant: ubuntu-24.04 - image: arm64v8/ubuntu:24.04 - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - variant: ubuntu-22.04 - image: arm64v8/ubuntu:22.04 - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - variant: ubuntu-20.04 - image: arm64v8/ubuntu:20.04 - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - variant: debian-12 - image: arm64v8/debian:12 - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - variant: debian-11 - image: arm64v8/debian:11 - - runner: ubuntu-24.04-arm - target: aarch64-unknown-linux-musl - variant: centos-9 - image: quay.io/centos/centos:stream9 - steps: - - name: Install build prerequisites - shell: bash - run: | - set -euo pipefail - if command -v apt-get >/dev/null 2>&1; then - apt-get update - DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential bison autoconf gettext libncursesw5-dev - elif command -v dnf >/dev/null 2>&1; then - dnf install -y git gcc gcc-c++ make bison autoconf gettext ncurses-devel - elif command -v yum >/dev/null 2>&1; then - yum install -y git gcc gcc-c++ make bison autoconf gettext ncurses-devel - else - echo "Unsupported package manager in container" - exit 1 - fi - - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Build patched zsh - shell: bash - run: | - set -euo pipefail - git clone https://git.code.sf.net/p/zsh/code /tmp/zsh - cd /tmp/zsh - git checkout 77045ef899e53b9598bebc5a41db93a548a40ca6 - git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/zsh-exec-wrapper.patch" - ./Util/preconfig - ./configure - cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)" - make -j"${cores}" - - dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/zsh/${{ matrix.variant }}" - mkdir -p "$dest" - cp Src/zsh "$dest/zsh" - - - name: Smoke test zsh exec wrapper - shell: bash - run: | - set -euo pipefail - tmpdir="$(mktemp -d)" - cat > "$tmpdir/exec-wrapper" <<'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 "$tmpdir/exec-wrapper" - - CODEX_WRAPPER_LOG="$tmpdir/wrapper.log" \ - EXEC_WRAPPER="$tmpdir/exec-wrapper" \ - /tmp/zsh/Src/zsh -fc '/bin/echo smoke-zsh' > "$tmpdir/stdout.txt" - - grep -Fx "smoke-zsh" "$tmpdir/stdout.txt" - grep -Fx "/bin/echo" "$tmpdir/wrapper.log" - - - uses: actions/upload-artifact@v7 - with: - name: shell-tool-mcp-zsh-${{ matrix.target }}-${{ matrix.variant }} - path: artifacts/** - if-no-files-found: error - - zsh-darwin: - name: Build zsh (macOS) - ${{ matrix.variant }} - ${{ matrix.target }} - needs: metadata - runs-on: ${{ matrix.runner }} - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - include: - - runner: macos-15-xlarge - target: aarch64-apple-darwin - variant: macos-15 - - runner: macos-14 - target: aarch64-apple-darwin - variant: macos-14 - steps: - - name: Install build prerequisites - shell: bash - run: | - set -euo pipefail - if ! command -v autoconf >/dev/null 2>&1; then - brew install autoconf - fi - - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Build patched zsh - shell: bash - run: | - set -euo pipefail - git clone https://git.code.sf.net/p/zsh/code /tmp/zsh - cd /tmp/zsh - git checkout 77045ef899e53b9598bebc5a41db93a548a40ca6 - git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/zsh-exec-wrapper.patch" - ./Util/preconfig - ./configure - cores="$(getconf _NPROCESSORS_ONLN)" - make -j"${cores}" - - dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/zsh/${{ matrix.variant }}" - mkdir -p "$dest" - cp Src/zsh "$dest/zsh" - - - name: Smoke test zsh exec wrapper - shell: bash - run: | - set -euo pipefail - tmpdir="$(mktemp -d)" - cat > "$tmpdir/exec-wrapper" <<'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 "$tmpdir/exec-wrapper" - - CODEX_WRAPPER_LOG="$tmpdir/wrapper.log" \ - EXEC_WRAPPER="$tmpdir/exec-wrapper" \ - /tmp/zsh/Src/zsh -fc '/bin/echo smoke-zsh' > "$tmpdir/stdout.txt" - - grep -Fx "smoke-zsh" "$tmpdir/stdout.txt" - grep -Fx "/bin/echo" "$tmpdir/wrapper.log" - - - uses: actions/upload-artifact@v7 - with: - name: shell-tool-mcp-zsh-${{ matrix.target }}-${{ matrix.variant }} - path: artifacts/** - if-no-files-found: error - - package: - name: Package npm module - needs: - - metadata - - bash-linux - - bash-darwin - - zsh-linux - - zsh-darwin - runs-on: ubuntu-latest - env: - PACKAGE_VERSION: ${{ needs.metadata.outputs.version }} - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Setup pnpm - uses: pnpm/action-setup@v5 - with: - run_install: false - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Install JavaScript dependencies - run: pnpm install --frozen-lockfile - - - name: Build (shell-tool-mcp) - run: pnpm --filter @openai/codex-shell-tool-mcp run build - - - name: Download build artifacts - uses: actions/download-artifact@v8 - with: - path: artifacts - - - name: Assemble staging directory - id: staging - shell: bash - run: | - set -euo pipefail - staging="${STAGING_DIR}" - mkdir -p "$staging" "$staging/vendor" - cp shell-tool-mcp/README.md "$staging/" - cp shell-tool-mcp/package.json "$staging/" - - found_vendor="false" - shopt -s nullglob - for vendor_dir in artifacts/*/vendor; do - rsync -av "$vendor_dir/" "$staging/vendor/" - found_vendor="true" - done - if [[ "$found_vendor" == "false" ]]; then - echo "No vendor payloads were downloaded." - exit 1 - fi - - node - <<'NODE' - import fs from "node:fs"; - import path from "node:path"; - - const stagingDir = process.env.STAGING_DIR; - const version = process.env.PACKAGE_VERSION; - const pkgPath = path.join(stagingDir, "package.json"); - const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); - pkg.version = version; - fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); - NODE - - echo "dir=$staging" >> "$GITHUB_OUTPUT" - env: - STAGING_DIR: ${{ runner.temp }}/shell-tool-mcp - - - name: Ensure binaries are executable - env: - STAGING_DIR: ${{ steps.staging.outputs.dir }} - run: | - set -euo pipefail - chmod +x \ - "$STAGING_DIR"/vendor/*/bash/*/bash \ - "$STAGING_DIR"/vendor/*/zsh/*/zsh - - - name: Create npm tarball - shell: bash - env: - STAGING_DIR: ${{ steps.staging.outputs.dir }} - run: | - set -euo pipefail - mkdir -p dist/npm - pack_info=$(cd "$STAGING_DIR" && npm pack --ignore-scripts --json --pack-destination "${GITHUB_WORKSPACE}/dist/npm") - filename=$(PACK_INFO="$pack_info" node -e 'const data = JSON.parse(process.env.PACK_INFO); console.log(data[0].filename);') - mv "dist/npm/${filename}" "dist/npm/codex-shell-tool-mcp-npm-${PACKAGE_VERSION}.tgz" - - - uses: actions/upload-artifact@v7 - with: - name: codex-shell-tool-mcp-npm - path: dist/npm/codex-shell-tool-mcp-npm-${{ env.PACKAGE_VERSION }}.tgz - if-no-files-found: error - - publish: - name: Publish npm package - needs: - - metadata - - package - if: ${{ inputs.publish && needs.metadata.outputs.should_publish == 'true' }} - runs-on: ubuntu-latest - permissions: - id-token: write - contents: read - steps: - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: ${{ env.NODE_VERSION }} - registry-url: https://registry.npmjs.org - scope: "@openai" - - # Trusted publishing requires npm CLI version 11.5.1 or later. - - name: Update npm - run: npm install -g npm@latest - - - name: Download npm tarball - uses: actions/download-artifact@v8 - with: - name: codex-shell-tool-mcp-npm - path: dist/npm - - - name: Publish to npm - env: - NPM_TAG: ${{ needs.metadata.outputs.npm_tag }} - VERSION: ${{ needs.metadata.outputs.version }} - shell: bash - run: | - set -euo pipefail - tag_args=() - if [[ -n "${NPM_TAG}" ]]; then - tag_args+=(--tag "${NPM_TAG}") - fi - npm publish "dist/npm/codex-shell-tool-mcp-npm-${VERSION}.tgz" "${tag_args[@]}" diff --git a/codex-rs/app-server/tests/suite/bash b/codex-rs/app-server/tests/suite/bash deleted file mode 100755 index 828d10f2669..00000000000 --- a/codex-rs/app-server/tests/suite/bash +++ /dev/null @@ -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" - } - ] - }, - } -} diff --git a/codex-rs/app-server/tests/suite/zsh b/codex-rs/app-server/tests/suite/zsh index 76cd78ce697..f796fa7201e 100755 --- a/codex-rs/app-server/tests/suite/zsh +++ b/codex-rs/app-server/tests/suite/zsh @@ -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": { diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index a38a53ce0dc..0131616c340 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -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()); } diff --git a/codex-rs/shell-escalation/README.md b/codex-rs/shell-escalation/README.md index cea59601f85..e4d3fecd669 100644 --- a/codex-rs/shell-escalation/README.md +++ b/codex-rs/shell-escalation/README.md @@ -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://git.code.sf.net/p/zsh/code. 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)" ``` diff --git a/shell-tool-mcp/patches/zsh-exec-wrapper.patch b/codex-rs/shell-escalation/patches/zsh-exec-wrapper.patch similarity index 100% rename from shell-tool-mcp/patches/zsh-exec-wrapper.patch rename to codex-rs/shell-escalation/patches/zsh-exec-wrapper.patch diff --git a/codex-rs/shell-escalation/src/unix/escalate_client.rs b/codex-rs/shell-escalation/src/unix/escalate_client.rs index 43ae05624ab..0f988838655 100644 --- a/codex-rs/shell-escalation/src/unix/escalate_client.rs +++ b/codex-rs/shell-escalation/src/unix/escalate_client.rs @@ -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 { diff --git a/codex-rs/shell-escalation/src/unix/escalate_protocol.rs b/codex-rs/shell-escalation/src/unix/escalate_protocol.rs index ea38c7b2a6b..1892f181b98 100644 --- a/codex-rs/shell-escalation/src/unix/escalate_protocol.rs +++ b/codex-rs/shell-escalation/src/unix/escalate_protocol.rs @@ -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 { diff --git a/codex-rs/shell-escalation/src/unix/escalate_server.rs b/codex-rs/shell-escalation/src/unix/escalate_server.rs index 9cd4dbfad11..4e7f09b9ad9 100644 --- a/codex-rs/shell-escalation/src/unix/escalate_server.rs +++ b/codex-rs/shell-escalation/src/unix/escalate_server.rs @@ -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 command string to pass to the shell via `-c` or `-lc`. 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, - /// Launch Bash with -lc instead of -c: defaults to true. + /// Launch the shell with -lc instead of -c: defaults to true. pub login: Option, } @@ -126,18 +125,18 @@ impl Drop for EscalationSession { } pub struct EscalateServer { - bash_path: PathBuf, + shell_path: PathBuf, execve_wrapper: PathBuf, policy: Arc, } impl EscalateServer { - pub fn new(bash_path: PathBuf, execve_wrapper: PathBuf, policy: Policy) -> Self + pub fn new(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![ - self.bash_path.to_string_lossy().to_string(), + self.shell_path.to_string_lossy().to_string(), if params.login == Some(false) { "-c".to_string() } else { @@ -211,10 +210,6 @@ impl EscalateServer { EXEC_WRAPPER_ENV_VAR.to_string(), self.execve_wrapper.to_string_lossy().to_string(), ); - env.insert( - LEGACY_BASH_EXEC_WRAPPER_ENV_VAR.to_string(), - self.execve_wrapper.to_string_lossy().to_string(), - ); Ok(EscalationSession { env, task, @@ -595,7 +590,7 @@ mod tests { /// overlay and does not need to touch the configured shell or wrapper /// executable paths. /// - /// The `/bin/bash` and `/tmp/codex-execve-wrapper` values here are + /// The `/bin/zsh` and `/tmp/codex-execve-wrapper` values here are /// intentionally fake sentinels: this test asserts that the paths are /// copied into the exported environment and that the socket fd stays valid /// until `close_client_socket()` is called. @@ -605,7 +600,7 @@ mod tests { let execve_wrapper = PathBuf::from("/tmp/codex-execve-wrapper"); let execve_wrapper_str = execve_wrapper.to_string_lossy().to_string(); let server = EscalateServer::new( - PathBuf::from("/bin/bash"), + PathBuf::from("/bin/zsh"), execve_wrapper.clone(), DeterministicEscalationPolicy { decision: EscalationDecision::run(), @@ -618,10 +613,6 @@ mod tests { )?; let env = session.env(); assert_eq!(env.get(EXEC_WRAPPER_ENV_VAR), Some(&execve_wrapper_str)); - assert_eq!( - env.get(LEGACY_BASH_EXEC_WRAPPER_ENV_VAR), - Some(&execve_wrapper_str) - ); let socket_fd = env .get(ESCALATE_SOCKET_ENV_VAR) .expect("session should export shell escalation socket"); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 102869a683c..6939665150a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,30 +75,6 @@ importers: specifier: ^3.24.6 version: 3.24.6(zod@3.25.76) - 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/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)) - prettier: - specifier: ^3.6.2 - version: 3.6.2 - ts-jest: - specifier: ^29.3.4 - version: 29.4.4(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.10)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)))(typescript@5.9.2) - tsup: - specifier: ^8.5.0 - version: 8.5.0(postcss@8.5.6)(typescript@5.9.2)(yaml@2.8.1) - typescript: - specifier: ^5.9.2 - version: 5.9.2 - packages: '@babel/code-frame@7.27.1': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 09a61536ab1..a47e4bd8a1b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,7 +2,6 @@ packages: - codex-cli - codex-rs/responses-api-proxy/npm - sdk/typescript - - shell-tool-mcp ignoredBuiltDependencies: - esbuild diff --git a/shell-tool-mcp/.gitignore b/shell-tool-mcp/.gitignore deleted file mode 100644 index c6958891dd2..00000000000 --- a/shell-tool-mcp/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/bin/ -/node_modules/ diff --git a/shell-tool-mcp/README.md b/shell-tool-mcp/README.md deleted file mode 100644 index deadaed4319..00000000000 --- a/shell-tool-mcp/README.md +++ /dev/null @@ -1,106 +0,0 @@ -# @openai/codex-shell-tool-mcp - -**Note: This MCP server is still experimental. When using it with Codex CLI, ensure the CLI version matches the MCP server version.** - -`@openai/codex-shell-tool-mcp` is an MCP server that provides a tool named `shell` that runs a shell command inside a sandboxed instance of Bash. This special instance of Bash intercepts requests to spawn new processes (specifically, [`execve(2)`](https://man7.org/linux/man-pages/man2/execve.2.html) 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. diff --git a/shell-tool-mcp/jest.config.cjs b/shell-tool-mcp/jest.config.cjs deleted file mode 100644 index 8ade9496e70..00000000000 --- a/shell-tool-mcp/jest.config.cjs +++ /dev/null @@ -1,6 +0,0 @@ -/** @type {import('jest').Config} */ -module.exports = { - preset: "ts-jest", - testEnvironment: "node", - roots: ["/tests"], -}; diff --git a/shell-tool-mcp/package.json b/shell-tool-mcp/package.json deleted file mode 100644 index 28918d75865..00000000000 --- a/shell-tool-mcp/package.json +++ /dev/null @@ -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" -} diff --git a/shell-tool-mcp/patches/bash-exec-wrapper.patch b/shell-tool-mcp/patches/bash-exec-wrapper.patch deleted file mode 100644 index f4471f2c2df..00000000000 --- a/shell-tool-mcp/patches/bash-exec-wrapper.patch +++ /dev/null @@ -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() */ diff --git a/shell-tool-mcp/src/bashSelection.ts b/shell-tool-mcp/src/bashSelection.ts deleted file mode 100644 index 7da137f6a78..00000000000 --- a/shell-tool-mcp/src/bashSelection.ts +++ /dev/null @@ -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}`); -} diff --git a/shell-tool-mcp/src/constants.ts b/shell-tool-mcp/src/constants.ts deleted file mode 100644 index e2bbcbf5b28..00000000000 --- a/shell-tool-mcp/src/constants.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { DarwinBashVariant, LinuxBashVariant } from "./types"; - -export const LINUX_BASH_VARIANTS: ReadonlyArray = [ - { 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 = [ - { name: "macos-15", minDarwin: 24 }, - { name: "macos-14", minDarwin: 23 }, - { name: "macos-13", minDarwin: 22 }, -]; diff --git a/shell-tool-mcp/src/index.ts b/shell-tool-mcp/src/index.ts deleted file mode 100644 index 60030403657..00000000000 --- a/shell-tool-mcp/src/index.ts +++ /dev/null @@ -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 { - 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); -}); diff --git a/shell-tool-mcp/src/osRelease.ts b/shell-tool-mcp/src/osRelease.ts deleted file mode 100644 index 3e55bfb1da8..00000000000 --- a/shell-tool-mcp/src/osRelease.ts +++ /dev/null @@ -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 = {}; - 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: "" }; - } -} diff --git a/shell-tool-mcp/src/platform.ts b/shell-tool-mcp/src/platform.ts deleted file mode 100644 index 177ba52b22f..00000000000 --- a/shell-tool-mcp/src/platform.ts +++ /dev/null @@ -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})`); -} diff --git a/shell-tool-mcp/src/types.ts b/shell-tool-mcp/src/types.ts deleted file mode 100644 index 28748101c20..00000000000 --- a/shell-tool-mcp/src/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type LinuxBashVariant = { - name: string; - ids: Array; - versions: Array; -}; - -export type DarwinBashVariant = { - name: string; - minDarwin: number; -}; - -export type OsReleaseInfo = { - id: string; - idLike: Array; - versionId: string; -}; - -export type BashSelection = { - path: string; - variant: string; -}; diff --git a/shell-tool-mcp/tests/bashSelection.test.ts b/shell-tool-mcp/tests/bashSelection.test.ts deleted file mode 100644 index 90753e675bf..00000000000 --- a/shell-tool-mcp/tests/bashSelection.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/shell-tool-mcp/tests/osRelease.test.ts b/shell-tool-mcp/tests/osRelease.test.ts deleted file mode 100644 index 22911298686..00000000000 --- a/shell-tool-mcp/tests/osRelease.test.ts +++ /dev/null @@ -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"]); - }); -}); diff --git a/shell-tool-mcp/tsconfig.json b/shell-tool-mcp/tsconfig.json deleted file mode 100644 index 90bb3e0d78e..00000000000 --- a/shell-tool-mcp/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "CommonJS", - "moduleResolution": "Node", - "noEmit": true, - "strict": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "skipLibCheck": true - }, - "include": ["src", "tests"] -} diff --git a/shell-tool-mcp/tsup.config.ts b/shell-tool-mcp/tsup.config.ts deleted file mode 100644 index 8e4e2d4e389..00000000000 --- a/shell-tool-mcp/tsup.config.ts +++ /dev/null @@ -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", - }, -});