diff --git a/.codex/environments/environment.toml b/.codex/environments/environment.toml
index b3c5910fbc..ae0bd24ca8 100644
--- a/.codex/environments/environment.toml
+++ b/.codex/environments/environment.toml
@@ -2,6 +2,10 @@
version = 1
name = "codex"
+# TODO(anp) make it optional to specify this field
+[setup]
+script = ""
+
[[actions]]
name = "Run"
icon = "run"
diff --git a/.github/actions/prepare-bazel-ci/action.yml b/.github/actions/prepare-bazel-ci/action.yml
index b41d80e0bc..a1beece2df 100644
--- a/.github/actions/prepare-bazel-ci/action.yml
+++ b/.github/actions/prepare-bazel-ci/action.yml
@@ -7,10 +7,6 @@ inputs:
cache-scope:
description: Logical namespace used to keep concurrent Bazel jobs from reserving the same repository cache key.
required: true
- install-test-prereqs:
- description: Install DotSlash for Bazel-backed test jobs.
- required: false
- default: "false"
outputs:
repository-cache-path:
description: Filesystem path used for the Bazel repository cache.
@@ -30,7 +26,6 @@ runs:
uses: ./.github/actions/setup-bazel-ci
with:
target: ${{ inputs.target }}
- install-test-prereqs: ${{ inputs.install-test-prereqs }}
- name: Compute bazel repository cache key
id: cache_bazel_repository_key
diff --git a/.github/actions/run-argument-comment-lint/action.yml b/.github/actions/run-argument-comment-lint/action.yml
index 80fb23d417..31c752e86e 100644
--- a/.github/actions/run-argument-comment-lint/action.yml
+++ b/.github/actions/run-argument-comment-lint/action.yml
@@ -16,7 +16,6 @@ runs:
- uses: ./.github/actions/setup-bazel-ci
with:
target: ${{ inputs.target }}
- install-test-prereqs: true
- name: Install Linux sandbox build dependencies
if: ${{ runner.os == 'Linux' }}
diff --git a/.github/actions/setup-bazel-ci/action.yml b/.github/actions/setup-bazel-ci/action.yml
index 658dca2aa4..57e71fbc78 100644
--- a/.github/actions/setup-bazel-ci/action.yml
+++ b/.github/actions/setup-bazel-ci/action.yml
@@ -1,13 +1,9 @@
name: setup-bazel-ci
-description: Prepare a Bazel CI runner with shared caches and optional test prerequisites.
+description: Prepare a Bazel CI runner with shared caches.
inputs:
target:
description: Target triple used for cache namespacing.
required: true
- install-test-prereqs:
- description: Install DotSlash for Bazel-backed test jobs.
- required: false
- default: "false"
outputs:
repository-cache-path:
description: Filesystem path used for the Bazel repository cache.
@@ -16,21 +12,7 @@ outputs:
runs:
using: composite
steps:
- # Some integration tests rely on DotSlash being installed.
- # See https://github.com/openai/codex/pull/7617.
- - name: Install DotSlash
- if: inputs.install-test-prereqs == 'true'
- uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
-
- - name: Make DotSlash available in PATH (Unix)
- if: inputs.install-test-prereqs == 'true' && runner.os != 'Windows'
- shell: bash
- run: cp "$(which dotslash)" /usr/local/bin
-
- - name: Make DotSlash available in PATH (Windows)
- if: inputs.install-test-prereqs == 'true' && runner.os == 'Windows'
- shell: pwsh
- run: Copy-Item (Get-Command dotslash).Source -Destination "$env:LOCALAPPDATA\Microsoft\WindowsApps\dotslash.exe"
+ - uses: ./.github/actions/setup-ci
- name: Set up Bazel
uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
@@ -124,8 +106,3 @@ runs:
if: runner.os == 'Windows'
shell: pwsh
run: ./.github/scripts/compute-bazel-windows-path.ps1
-
- - name: Enable Git long paths (Windows)
- if: runner.os == 'Windows'
- shell: pwsh
- run: git config --global core.longpaths true
diff --git a/.github/actions/setup-ci/action.yml b/.github/actions/setup-ci/action.yml
new file mode 100644
index 0000000000..0924e29616
--- /dev/null
+++ b/.github/actions/setup-ci/action.yml
@@ -0,0 +1,39 @@
+name: setup-ci
+description: Prepare common tools and environment shared by CI jobs.
+
+runs:
+ using: composite
+ steps:
+ - name: Prefer the Git CLI for Cargo git dependencies
+ shell: bash
+ run: echo "CARGO_NET_GIT_FETCH_WITH_CLI=true" >> "$GITHUB_ENV"
+
+ - name: Install DotSlash
+ uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
+
+ - name: Install just
+ uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
+ with:
+ tool: just@1.51.0
+
+ # Some integration tests spawn DotSlash from a stable system path rather
+ # than inheriting the action-local PATH entry.
+ - name: Make DotSlash available in PATH (Unix)
+ if: runner.os != 'Windows'
+ shell: bash
+ run: |
+ if [[ -w /usr/local/bin ]]; then
+ cp "$(which dotslash)" /usr/local/bin
+ else
+ sudo cp "$(which dotslash)" /usr/local/bin
+ fi
+
+ - name: Make DotSlash available in PATH (Windows)
+ if: runner.os == 'Windows'
+ shell: pwsh
+ run: Copy-Item (Get-Command dotslash).Source -Destination "$env:LOCALAPPDATA\Microsoft\WindowsApps\dotslash.exe"
+
+ - name: Enable Git long paths (Windows)
+ if: runner.os == 'Windows'
+ shell: pwsh
+ run: git config --global core.longpaths true
diff --git a/.github/scripts/build-codex-package-archive.sh b/.github/scripts/build-codex-package-archive.sh
index 229213cc53..4592e78ce5 100644
--- a/.github/scripts/build-codex-package-archive.sh
+++ b/.github/scripts/build-codex-package-archive.sh
@@ -9,6 +9,7 @@ Usage: build-codex-package-archive.sh \
--entrypoint-dir
\
--archive-dir \
[--bwrap-bin ] \
+ [--code-mode-host-bin ] \
[--zsh-manifest ] \
[--codex-command-runner-bin ] \
[--codex-windows-sandbox-setup-bin ] \
@@ -23,6 +24,7 @@ archive_dir=""
target_suffixed_entrypoint="false"
resource_args=()
bwrap_bin_provided="false"
+code_mode_host_bin_provided="false"
command_runner_bin_provided="false"
sandbox_setup_bin_provided="false"
@@ -49,6 +51,11 @@ while [[ $# -gt 0 ]]; do
bwrap_bin_provided="true"
shift 2
;;
+ --code-mode-host-bin)
+ resource_args+=(--code-mode-host-bin "${2:?--code-mode-host-bin requires a value}")
+ code_mode_host_bin_provided="true"
+ shift 2
+ ;;
--zsh-manifest)
resource_args+=(--zsh-manifest "${2:?--zsh-manifest requires a value}")
shift 2
@@ -114,6 +121,11 @@ case "$target" in
;;
esac
+code_mode_host_bin="${entrypoint_dir%/}/codex-code-mode-host${exe_suffix}"
+if [[ "$code_mode_host_bin_provided" == "false" && -f "$code_mode_host_bin" ]]; then
+ resource_args+=(--code-mode-host-bin "$code_mode_host_bin")
+fi
+
entrypoint_name="$entrypoint"
if [[ "$target_suffixed_entrypoint" == "true" ]]; then
entrypoint_name="${entrypoint_name}-${target}"
diff --git a/.github/scripts/test_v8_canary_changes.py b/.github/scripts/test_v8_canary_changes.py
index e4ce6424ed..0d8d9a2b32 100644
--- a/.github/scripts/test_v8_canary_changes.py
+++ b/.github/scripts/test_v8_canary_changes.py
@@ -4,6 +4,7 @@ import unittest
from pathlib import Path
from v8_canary_changes import changed_files
+from v8_canary_changes import canary_required
from v8_canary_changes import merge_base
from v8_canary_changes import resolved_v8_version
from v8_canary_changes import windows_source_required
@@ -46,6 +47,14 @@ version = "149.2.0"
)
)
+ def test_setup_ci_change_requires_canary_and_source_build(self) -> None:
+ changed_files = {".github/actions/setup-ci/action.yml"}
+
+ self.assertTrue(canary_required(changed_files, "149.2.0", "149.2.0"))
+ self.assertTrue(
+ windows_source_required(changed_files, "149.2.0", "149.2.0")
+ )
+
def test_manual_dispatch_requires_source_build(self) -> None:
self.assertTrue(
windows_source_required(
diff --git a/.github/scripts/v8_canary_changes.py b/.github/scripts/v8_canary_changes.py
index 0acc4ec3f6..53a064e384 100644
--- a/.github/scripts/v8_canary_changes.py
+++ b/.github/scripts/v8_canary_changes.py
@@ -21,6 +21,7 @@ ROOT = Path(__file__).resolve().parents[2]
CANARY_PATH_PATTERNS = {
".bazelrc",
".github/actions/setup-bazel-ci/**",
+ ".github/actions/setup-ci/**",
".github/scripts/run_bazel_with_buildbuddy.py",
".github/scripts/rusty_v8_bazel.py",
".github/scripts/rusty_v8_module_bazel.py",
@@ -40,6 +41,7 @@ CANARY_PATH_PATTERNS = {
# Windows source builds are a narrower, more expensive subset of the canary.
# A V8 version change also requires them even when no path below changed.
WINDOWS_SOURCE_BUILD_PATHS = {
+ ".github/actions/setup-ci/**",
".github/scripts/rusty_v8_bazel.py",
".github/scripts/rusty_v8_module_bazel.py",
".github/scripts/v8_canary_changes.py",
@@ -72,6 +74,15 @@ def canary_required(
)
+def matching_windows_source_paths(changed_files: set[str]) -> set[str]:
+ """Return changed paths that require Windows rusty_v8 source builds."""
+ return {
+ path
+ for path in changed_files
+ if any(fnmatchcase(path, pattern) for pattern in WINDOWS_SOURCE_BUILD_PATHS)
+ }
+
+
def resolved_v8_version(cargo_lock: bytes) -> str:
versions = sorted(
{
@@ -96,7 +107,7 @@ def windows_source_required(
return (
force
or base_v8_version != head_v8_version
- or not changed_files.isdisjoint(WINDOWS_SOURCE_BUILD_PATHS)
+ or bool(matching_windows_source_paths(changed_files))
)
@@ -165,7 +176,7 @@ def main() -> None:
if matched_canary_paths
else "no relevant changes"
)
- matched_windows_paths = sorted(files & WINDOWS_SOURCE_BUILD_PATHS)
+ matched_windows_paths = sorted(matching_windows_source_paths(files))
windows_source_reason = (
", ".join(matched_windows_paths)
if matched_windows_paths
diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml
index 10dd8670de..3e48b741b7 100644
--- a/.github/workflows/bazel.yml
+++ b/.github/workflows/bazel.yml
@@ -57,10 +57,12 @@ jobs:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
- if: matrix.os == 'ubuntu-24.04' && matrix.target == 'x86_64-unknown-linux-gnu'
+ - name: Prepare Bazel CI
+ id: prepare_bazel
+ uses: ./.github/actions/prepare-bazel-ci
with:
- tool: just
+ target: ${{ matrix.target }}
+ cache-scope: bazel-${{ github.job }}
- name: Check rusty_v8 MODULE.bazel checksums
if: matrix.os == 'ubuntu-24.04' && matrix.target == 'x86_64-unknown-linux-gnu'
@@ -69,13 +71,6 @@ jobs:
python3 .github/scripts/rusty_v8_bazel.py check-module-bazel
just test-github-scripts
- - name: Prepare Bazel CI
- id: prepare_bazel
- uses: ./.github/actions/prepare-bazel-ci
- with:
- target: ${{ matrix.target }}
- cache-scope: bazel-${{ github.job }}
- install-test-prereqs: "true"
- name: Check MODULE.bazel.lock is up to date
if: matrix.os == 'ubuntu-24.04' && matrix.target == 'x86_64-unknown-linux-gnu'
shell: bash
@@ -176,7 +171,6 @@ jobs:
# not save it from every shard below; duplicate uploads would sit on
# the PR-blocking critical path after the useful test work is done.
cache-scope: bazel-test
- install-test-prereqs: "true"
- name: bazel test shard
env:
@@ -287,7 +281,6 @@ jobs:
with:
target: x86_64-pc-windows-gnullvm
cache-scope: bazel-${{ github.job }}
- install-test-prereqs: "true"
- name: bazel test //...
env:
diff --git a/.github/workflows/cargo-deny.yml b/.github/workflows/cargo-deny.yml
index be00e1147d..ccbf80e12f 100644
--- a/.github/workflows/cargo-deny.yml
+++ b/.github/workflows/cargo-deny.yml
@@ -3,11 +3,6 @@ name: cargo-deny
on:
workflow_call:
-# Cargo's libgit2 transport has been flaky when fetching git dependencies with
-# nested submodules. Prefer the system git CLI across every Cargo invocation.
-env:
- CARGO_NET_GIT_FETCH_WITH_CLI: "true"
-
jobs:
cargo-deny:
runs-on: ubuntu-latest
@@ -21,6 +16,8 @@ jobs:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
+ - uses: ./.github/actions/setup-ci
+
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
diff --git a/.github/workflows/repo-checks.yml b/.github/workflows/repo-checks.yml
index 3d0504172d..97932c4fd4 100644
--- a/.github/workflows/repo-checks.yml
+++ b/.github/workflows/repo-checks.yml
@@ -16,6 +16,8 @@ jobs:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
+ - uses: ./.github/actions/setup-ci
+
- name: Verify codex-rs Cargo manifests inherit workspace settings
run: python3 .github/scripts/verify_cargo_workspace_manifests.py
@@ -76,15 +78,10 @@ jobs:
- name: Check root README ToC
run: python3 scripts/readme_toc.py README.md
- - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
- with:
- tool: just@1.51.0
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: "0.11.3"
- - name: Install DotSlash
- uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
- name: Check formatting (run `just fmt` to fix)
run: just fmt-check
diff --git a/.github/workflows/rust-ci-full-nextest-platform.yml b/.github/workflows/rust-ci-full-nextest-platform.yml
index 65d1fb2be5..825c2d73d6 100644
--- a/.github/workflows/rust-ci-full-nextest-platform.yml
+++ b/.github/workflows/rust-ci-full-nextest-platform.yml
@@ -48,11 +48,6 @@ on:
default: false
type: boolean
-# Caller workflow-level env does not flow through workflow_call, so keep the
-# Cargo git transport hardening on the archive and shard jobs directly here.
-env:
- CARGO_NET_GIT_FETCH_WITH_CLI: "true"
-
jobs:
archive:
name: Build nextest archive
@@ -81,6 +76,8 @@ jobs:
shell: pwsh
run: ../.github/scripts/setup-dev-drive.ps1
+ - uses: ./.github/actions/setup-ci
+
- name: Install Linux build dependencies
if: ${{ runner.os == 'Linux' }}
shell: bash
@@ -91,9 +88,6 @@ jobs:
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev bubblewrap
fi
- - name: Install DotSlash
- uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
-
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
targets: ${{ inputs.target }}
@@ -306,6 +300,8 @@ jobs:
with:
persist-credentials: false
+ - uses: ./.github/actions/setup-ci
+
- name: Install Linux build dependencies
if: ${{ runner.os == 'Linux' }}
shell: bash
@@ -316,9 +312,6 @@ jobs:
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev bubblewrap
fi
- - name: Install DotSlash
- uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
-
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
targets: ${{ inputs.target }}
diff --git a/.github/workflows/rust-ci-full.yml b/.github/workflows/rust-ci-full.yml
index cb99c8911e..dc38aaa86d 100644
--- a/.github/workflows/rust-ci-full.yml
+++ b/.github/workflows/rust-ci-full.yml
@@ -8,13 +8,6 @@ on:
- "**full-ci**"
workflow_dispatch:
-# CI builds in debug (dev) for faster signal.
-env:
- # Cargo's libgit2 transport has been flaky on macOS when fetching git
- # dependencies with nested submodules. Use the system git CLI, which has
- # better network/proxy behavior and matches Cargo's own suggested fallback.
- CARGO_NET_GIT_FETCH_WITH_CLI: "true"
-
jobs:
# --- CI that doesn't need specific targets ---------------------------------
general:
@@ -27,12 +20,10 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
+ - uses: ./.github/actions/setup-ci
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
components: rustfmt
- - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
- with:
- tool: just
- name: cargo fmt
run: cargo fmt -- --config imports_granularity=Item --check
- name: Rust benchmark smoke test
@@ -48,6 +39,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
+ - uses: ./.github/actions/setup-ci
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
@@ -65,6 +57,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
+ - uses: ./.github/actions/setup-ci
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
toolchain: nightly-2025-09-18
@@ -123,7 +116,6 @@ jobs:
- uses: ./.github/actions/setup-bazel-ci
with:
target: ${{ runner.os }}
- install-test-prereqs: true
- name: Install Linux sandbox build dependencies
if: ${{ runner.os == 'Linux' }}
shell: bash
@@ -256,6 +248,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
+ - uses: ./.github/actions/setup-ci
- name: Install Linux build dependencies
if: ${{ runner.os == 'Linux' }}
shell: bash
diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml
index cf1afdfe2d..01d51aac0e 100644
--- a/.github/workflows/rust-ci.yml
+++ b/.github/workflows/rust-ci.yml
@@ -3,11 +3,6 @@ on:
workflow_call:
workflow_dispatch:
-# Cargo's libgit2 transport has been flaky when fetching git dependencies with
-# nested submodules. Prefer the system git CLI across every Cargo invocation.
-env:
- CARGO_NET_GIT_FETCH_WITH_CLI: "true"
-
jobs:
# --- Detect what changed so the fast PR workflow only runs relevant jobs ----
changed:
@@ -76,12 +71,10 @@ jobs:
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
+ - uses: ./.github/actions/setup-ci
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
components: rustfmt
- - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
- with:
- tool: just
- name: cargo fmt
run: cargo fmt -- --config imports_granularity=Item --check
- name: Rust benchmark smoke test
@@ -104,6 +97,7 @@ jobs:
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
+ - uses: ./.github/actions/setup-ci
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
with:
@@ -128,6 +122,7 @@ jobs:
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
+ - uses: ./.github/actions/setup-ci
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
- name: Install nightly argument-comment-lint toolchain
shell: bash
diff --git a/.github/workflows/rust-release-windows.yml b/.github/workflows/rust-release-windows.yml
index 7069b6ca8f..ccb2ee13f7 100644
--- a/.github/workflows/rust-release-windows.yml
+++ b/.github/workflows/rust-release-windows.yml
@@ -3,11 +3,8 @@ name: rust-release-windows
on:
workflow_call:
-# Cargo's libgit2 transport has been flaky when fetching git dependencies with
-# nested submodules. Prefer the system git CLI across every Cargo invocation.
env:
- CARGO_NET_GIT_FETCH_WITH_CLI: "true"
- WINDOWS_BINARIES: "codex codex-responses-api-proxy codex-windows-sandbox-setup codex-command-runner codex-app-server"
+ WINDOWS_BINARIES: "codex codex-code-mode-host codex-responses-api-proxy codex-windows-sandbox-setup codex-command-runner codex-app-server"
jobs:
build-windows-binaries:
@@ -28,14 +25,14 @@ jobs:
- runner: windows-x64
target: x86_64-pc-windows-msvc
bundle: primary
- binaries: "codex codex-responses-api-proxy"
+ binaries: "codex codex-code-mode-host codex-responses-api-proxy"
runs_on:
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-x64
- runner: windows-arm64
target: aarch64-pc-windows-msvc
bundle: primary
- binaries: "codex codex-responses-api-proxy"
+ binaries: "codex codex-code-mode-host codex-responses-api-proxy"
runs_on:
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-arm64
@@ -56,14 +53,14 @@ jobs:
- runner: windows-x64
target: x86_64-pc-windows-msvc
bundle: app-server
- binaries: "codex-app-server"
+ binaries: "codex-app-server codex-code-mode-host"
runs_on:
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-x64
- runner: windows-arm64
target: aarch64-pc-windows-msvc
bundle: app-server
- binaries: "codex-app-server"
+ binaries: "codex-app-server codex-code-mode-host"
runs_on:
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-arm64
@@ -72,6 +69,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
+ - uses: ./.github/actions/setup-ci
- name: Print runner specs (Windows)
shell: powershell
run: |
@@ -164,6 +162,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
+ - uses: ./.github/actions/setup-ci
- name: Download prebuilt Windows binaries
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
@@ -221,6 +220,8 @@ jobs:
with:
persist-credentials: false
+ - uses: ./.github/actions/setup-ci
+
- name: Download prebuilt Windows primary binaries
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
@@ -270,9 +271,6 @@ jobs:
"$dest/${binary}-${{ matrix.target }}.exe"
done
- - name: Install DotSlash
- uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
-
- name: Build Codex package archives
shell: bash
run: |
diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml
index 49d24abc18..93f03e63d1 100644
--- a/.github/workflows/rust-release.yml
+++ b/.github/workflows/rust-release.yml
@@ -83,50 +83,50 @@ jobs:
target: aarch64-apple-darwin
bundle: primary
artifact_name: aarch64-apple-darwin
- binaries: "codex codex-responses-api-proxy"
+ binaries: "codex codex-code-mode-host codex-responses-api-proxy"
build_dmg: "true"
- runner: macos-15-xlarge
target: aarch64-apple-darwin
bundle: app-server
artifact_name: aarch64-apple-darwin-app-server
- binaries: "codex-app-server"
+ binaries: "codex-app-server codex-code-mode-host"
build_dmg: "false"
- runner: macos-15-xlarge
target: x86_64-apple-darwin
bundle: primary
artifact_name: x86_64-apple-darwin
- binaries: "codex codex-responses-api-proxy"
+ binaries: "codex codex-code-mode-host codex-responses-api-proxy"
build_dmg: "true"
- runner: macos-15-xlarge
target: x86_64-apple-darwin
bundle: app-server
artifact_name: x86_64-apple-darwin-app-server
- binaries: "codex-app-server"
+ binaries: "codex-app-server codex-code-mode-host"
build_dmg: "false"
# Release artifacts intentionally ship MUSL-linked Linux binaries.
- runner: ${{ github.event.repository.name }}-linux-x64-xl
target: x86_64-unknown-linux-musl
bundle: primary
artifact_name: x86_64-unknown-linux-musl
- binaries: "codex codex-responses-api-proxy bwrap"
+ binaries: "codex codex-code-mode-host codex-responses-api-proxy bwrap"
build_dmg: "false"
- runner: ${{ github.event.repository.name }}-linux-x64-xl
target: x86_64-unknown-linux-musl
bundle: app-server
artifact_name: x86_64-unknown-linux-musl-app-server
- binaries: "codex-app-server"
+ binaries: "codex-app-server codex-code-mode-host"
build_dmg: "false"
- runner: ${{ github.event.repository.name }}-linux-arm64
target: aarch64-unknown-linux-musl
bundle: primary
artifact_name: aarch64-unknown-linux-musl
- binaries: "codex codex-responses-api-proxy bwrap"
+ binaries: "codex codex-code-mode-host codex-responses-api-proxy bwrap"
build_dmg: "false"
- runner: ${{ github.event.repository.name }}-linux-arm64
target: aarch64-unknown-linux-musl
bundle: app-server
artifact_name: aarch64-unknown-linux-musl-app-server
- binaries: "codex-app-server"
+ binaries: "codex-app-server codex-code-mode-host"
build_dmg: "false"
steps:
@@ -335,6 +335,11 @@ jobs:
mkdir -p "$dest"
for binary in ${{ matrix.binaries }}; do
+ # Both variants package the host, but only the primary bundle publishes
+ # standalone binary archives to avoid duplicate release asset names.
+ if [[ "${{ matrix.bundle }}" == "app-server" && "$binary" == "codex-code-mode-host" ]]; then
+ continue
+ fi
cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}"
if [[ "${{ matrix.target }}" == *linux* ]]; then
cp "target/${{ matrix.target }}/release/${binary}.sigstore" \
@@ -347,9 +352,13 @@ jobs:
rm -rf "$bundle_root"
mkdir -p "$bundle_root/codex-resources"
cp "$dest/codex-${{ matrix.target }}" "$bundle_root/codex"
+ cp "$dest/codex-code-mode-host-${{ matrix.target }}" "$bundle_root/codex-code-mode-host"
cp "$dest/bwrap-${{ matrix.target }}" "$bundle_root/codex-resources/bwrap"
- chmod 0755 "$bundle_root/codex" "$bundle_root/codex-resources/bwrap"
- tar -C "$bundle_root" -cf - codex codex-resources/bwrap |
+ chmod 0755 \
+ "$bundle_root/codex" \
+ "$bundle_root/codex-code-mode-host" \
+ "$bundle_root/codex-resources/bwrap"
+ tar -C "$bundle_root" -cf - codex codex-code-mode-host codex-resources/bwrap |
zstd -T0 -19 -o "$dest/codex-${{ matrix.target }}-bundle.tar.zst"
fi
@@ -497,19 +506,19 @@ jobs:
- target: aarch64-apple-darwin
bundle: primary
artifact_name: aarch64-apple-darwin
- binaries: "codex codex-responses-api-proxy"
+ binaries: "codex codex-code-mode-host codex-responses-api-proxy"
- target: aarch64-apple-darwin
bundle: app-server
artifact_name: aarch64-apple-darwin-app-server
- binaries: "codex-app-server"
+ binaries: "codex-app-server codex-code-mode-host"
- target: x86_64-apple-darwin
bundle: primary
artifact_name: x86_64-apple-darwin
- binaries: "codex codex-responses-api-proxy"
+ binaries: "codex codex-code-mode-host codex-responses-api-proxy"
- target: x86_64-apple-darwin
bundle: app-server
artifact_name: x86_64-apple-darwin-app-server
- binaries: "codex-app-server"
+ binaries: "codex-app-server codex-code-mode-host"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -615,22 +624,22 @@ jobs:
- target: aarch64-apple-darwin
bundle: primary
artifact_name: aarch64-apple-darwin
- binaries: "codex codex-responses-api-proxy"
+ binaries: "codex codex-code-mode-host codex-responses-api-proxy"
build_dmg: "true"
- target: aarch64-apple-darwin
bundle: app-server
artifact_name: aarch64-apple-darwin-app-server
- binaries: "codex-app-server"
+ binaries: "codex-app-server codex-code-mode-host"
build_dmg: "false"
- target: x86_64-apple-darwin
bundle: primary
artifact_name: x86_64-apple-darwin
- binaries: "codex codex-responses-api-proxy"
+ binaries: "codex codex-code-mode-host codex-responses-api-proxy"
build_dmg: "true"
- target: x86_64-apple-darwin
bundle: app-server
artifact_name: x86_64-apple-darwin-app-server
- binaries: "codex-app-server"
+ binaries: "codex-app-server codex-code-mode-host"
build_dmg: "false"
steps:
@@ -707,6 +716,11 @@ jobs:
mkdir -p "$dest"
for binary in ${{ matrix.binaries }}; do
+ # Both variants package the host, but only the primary bundle publishes
+ # standalone binary archives to avoid duplicate release asset names.
+ if [[ "${{ matrix.bundle }}" == "app-server" && "$binary" == "codex-code-mode-host" ]]; then
+ continue
+ fi
cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}"
done
@@ -911,22 +925,22 @@ jobs:
- target: aarch64-apple-darwin
bundle: primary
artifact_name: aarch64-apple-darwin
- binaries: "codex codex-responses-api-proxy"
+ binaries: "codex codex-code-mode-host codex-responses-api-proxy"
verify_dmg: "true"
- target: aarch64-apple-darwin
bundle: app-server
artifact_name: aarch64-apple-darwin-app-server
- binaries: "codex-app-server"
+ binaries: "codex-app-server codex-code-mode-host"
verify_dmg: "false"
- target: x86_64-apple-darwin
bundle: primary
artifact_name: x86_64-apple-darwin
- binaries: "codex codex-responses-api-proxy"
+ binaries: "codex codex-code-mode-host codex-responses-api-proxy"
verify_dmg: "true"
- target: x86_64-apple-darwin
bundle: app-server
artifact_name: x86_64-apple-darwin-app-server
- binaries: "codex-app-server"
+ binaries: "codex-app-server codex-code-mode-host"
verify_dmg: "false"
steps:
@@ -983,6 +997,12 @@ jobs:
binary_path="${RUNNER_TEMP}/signed-binaries/${binary}"
verify_signed_binary "$binary_path"
+ # The app-server package contains the host, but its standalone archives
+ # are omitted above to avoid duplicate release asset names.
+ if [[ "${{ matrix.bundle }}" == "app-server" && "$binary" == "codex-code-mode-host" ]]; then
+ continue
+ fi
+
direct_archive_dir="${RUNNER_TEMP}/direct-archive-${binary}-${target}"
rm -rf "$direct_archive_dir"
mkdir -p "$direct_archive_dir"
@@ -1014,6 +1034,7 @@ jobs:
mkdir -p "$package_dir"
tar -xzf "${packaged_dir}/${package_stem}-${target}.tar.gz" -C "$package_dir"
verify_signed_binary "${package_dir}/bin/${package_entrypoint}"
+ verify_signed_binary "${package_dir}/bin/codex-code-mode-host"
if [[ "${{ matrix.verify_dmg }}" != "true" ]]; then
exit 0
diff --git a/.github/workflows/v8-canary.yml b/.github/workflows/v8-canary.yml
index 53d9585108..f338c341e4 100644
--- a/.github/workflows/v8-canary.yml
+++ b/.github/workflows/v8-canary.yml
@@ -9,11 +9,6 @@ on:
pull_request: {}
workflow_dispatch:
-# Cargo's libgit2 transport has been flaky when fetching git dependencies with
-# nested submodules. Prefer the system git CLI for Cargo builds and smoke tests.
-env:
- CARGO_NET_GIT_FETCH_WITH_CLI: "true"
-
concurrency:
group: ${{ github.workflow }}::${{ github.event.pull_request.number > 0 && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}
cancel-in-progress: ${{ github.ref_name != 'main' }}
@@ -325,6 +320,8 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ - uses: ./.github/actions/setup-ci
+
- name: Configure git for upstream checkout
shell: bash
run: git config --global core.symlinks true
diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock
index c0ff4a585a..9d89f8eb1f 100644
--- a/codex-rs/Cargo.lock
+++ b/codex-rs/Cargo.lock
@@ -1936,6 +1936,7 @@ dependencies = [
"bytes",
"chrono",
"codex-client",
+ "codex-http-client",
"codex-protocol",
"codex-utils-rustls-provider",
"eventsource-stream",
@@ -2262,7 +2263,7 @@ dependencies = [
"anyhow",
"codex-api",
"codex-backend-openapi-models",
- "codex-client",
+ "codex-http-client",
"codex-login",
"codex-model-provider",
"codex-protocol",
@@ -2392,33 +2393,12 @@ dependencies = [
name = "codex-client"
version = "0.0.0"
dependencies = [
- "bytes",
- "codex-utils-cargo-bin",
- "codex-utils-rustls-provider",
+ "codex-http-client",
"eventsource-stream",
"futures",
"http 1.4.0",
- "opentelemetry",
- "opentelemetry_sdk",
- "pretty_assertions",
"rand 0.9.3",
- "rcgen",
- "reqwest 0.12.28",
- "rustls",
- "rustls-native-certs",
- "rustls-pki-types",
- "serde",
- "serde_json",
- "sha2 0.10.9",
- "system-configuration",
- "tempfile",
- "thiserror 2.0.18",
"tokio",
- "tracing",
- "tracing-opentelemetry",
- "tracing-subscriber",
- "windows-sys 0.52.0",
- "zstd 0.13.3",
]
[[package]]
@@ -2452,11 +2432,11 @@ dependencies = [
"anyhow",
"chrono",
"clap",
- "codex-client",
"codex-cloud-tasks-client",
"codex-cloud-tasks-mock-client",
"codex-core",
"codex-git-utils",
+ "codex-http-client",
"codex-login",
"codex-model-provider",
"codex-tui",
@@ -2916,9 +2896,9 @@ dependencies = [
"bytes",
"clatter",
"codex-api",
- "codex-client",
"codex-exec-server-protocol",
"codex-file-system",
+ "codex-http-client",
"codex-network-proxy",
"codex-otel",
"codex-protocol",
@@ -3214,6 +3194,37 @@ dependencies = [
"uuid",
]
+[[package]]
+name = "codex-http-client"
+version = "0.0.0"
+dependencies = [
+ "bytes",
+ "codex-utils-cargo-bin",
+ "codex-utils-rustls-provider",
+ "futures",
+ "http 1.4.0",
+ "opentelemetry",
+ "opentelemetry_sdk",
+ "pretty_assertions",
+ "rcgen",
+ "reqwest 0.12.28",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "sha2 0.10.9",
+ "system-configuration",
+ "tempfile",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+ "tracing-opentelemetry",
+ "tracing-subscriber",
+ "windows-sys 0.52.0",
+ "zstd 0.13.3",
+]
+
[[package]]
name = "codex-image-generation-extension"
version = "0.0.0"
@@ -3305,8 +3316,8 @@ dependencies = [
"base64 0.22.1",
"chrono",
"codex-agent-identity",
- "codex-client",
"codex-config",
+ "codex-http-client",
"codex-keyring-store",
"codex-model-provider-info",
"codex-otel",
@@ -3520,8 +3531,8 @@ dependencies = [
"codex-agent-identity",
"codex-api",
"codex-aws-auth",
- "codex-client",
"codex-feedback",
+ "codex-http-client",
"codex-login",
"codex-model-provider-info",
"codex-models-manager",
diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml
index 2f73c216a9..ad08ce8e02 100644
--- a/codex-rs/Cargo.toml
+++ b/codex-rs/Cargo.toml
@@ -41,6 +41,7 @@ members = [
"core-plugins",
"core-skills",
"hooks",
+ "http-client",
"secrets",
"exec",
"file-system",
@@ -164,6 +165,7 @@ codex-cloud-tasks-mock-client = { path = "cloud-tasks-mock-client" }
codex-code-mode = { path = "code-mode" }
codex-code-mode-protocol = { path = "code-mode-protocol" }
codex-home = { path = "codex-home" }
+codex-http-client = { path = "http-client" }
codex-config = { path = "config" }
codex-connectors = { path = "connectors" }
codex-connectors-extension = { path = "ext/connectors" }
diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
index 0eedef903a..2ffff2f96f 100644
--- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
+++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
@@ -13423,6 +13423,13 @@
],
"type": "string"
},
+ "PluginInstallPolicySource": {
+ "enum": [
+ "WORKSPACE_SETTING",
+ "IMPLICIT_CANONICAL_APP"
+ ],
+ "type": "string"
+ },
"PluginInstallResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -14306,6 +14313,16 @@
"installPolicy": {
"$ref": "#/definitions/v2/PluginInstallPolicy"
},
+ "installPolicySource": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/v2/PluginInstallPolicySource"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"installed": {
"type": "boolean"
},
diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
index b428f25258..dc6e2c9693 100644
--- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
+++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
@@ -9827,6 +9827,13 @@
],
"type": "string"
},
+ "PluginInstallPolicySource": {
+ "enum": [
+ "WORKSPACE_SETTING",
+ "IMPLICIT_CANONICAL_APP"
+ ],
+ "type": "string"
+ },
"PluginInstallResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -10710,6 +10717,16 @@
"installPolicy": {
"$ref": "#/definitions/PluginInstallPolicy"
},
+ "installPolicySource": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/PluginInstallPolicySource"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"installed": {
"type": "boolean"
},
diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginInstalledResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PluginInstalledResponse.json
index 55ad95be2e..f39b0f19b3 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/PluginInstalledResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/PluginInstalledResponse.json
@@ -63,6 +63,13 @@
],
"type": "string"
},
+ "PluginInstallPolicySource": {
+ "enum": [
+ "WORKSPACE_SETTING",
+ "IMPLICIT_CANONICAL_APP"
+ ],
+ "type": "string"
+ },
"PluginInterface": {
"properties": {
"brandColor": {
@@ -489,6 +496,16 @@
"installPolicy": {
"$ref": "#/definitions/PluginInstallPolicy"
},
+ "installPolicySource": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/PluginInstallPolicySource"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"installed": {
"type": "boolean"
},
diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json
index 25d12e50dd..2f0c6e4b40 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json
@@ -63,6 +63,13 @@
],
"type": "string"
},
+ "PluginInstallPolicySource": {
+ "enum": [
+ "WORKSPACE_SETTING",
+ "IMPLICIT_CANONICAL_APP"
+ ],
+ "type": "string"
+ },
"PluginInterface": {
"properties": {
"brandColor": {
@@ -489,6 +496,16 @@
"installPolicy": {
"$ref": "#/definitions/PluginInstallPolicy"
},
+ "installPolicySource": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/PluginInstallPolicySource"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"installed": {
"type": "boolean"
},
diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json
index b4b917602a..628cb13189 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json
@@ -242,6 +242,13 @@
],
"type": "string"
},
+ "PluginInstallPolicySource": {
+ "enum": [
+ "WORKSPACE_SETTING",
+ "IMPLICIT_CANONICAL_APP"
+ ],
+ "type": "string"
+ },
"PluginInterface": {
"properties": {
"brandColor": {
@@ -629,6 +636,16 @@
"installPolicy": {
"$ref": "#/definitions/PluginInstallPolicy"
},
+ "installPolicySource": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/PluginInstallPolicySource"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"installed": {
"type": "boolean"
},
diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginShareListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PluginShareListResponse.json
index 751a6c5e7f..32bf946e38 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/PluginShareListResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/PluginShareListResponse.json
@@ -37,6 +37,13 @@
],
"type": "string"
},
+ "PluginInstallPolicySource": {
+ "enum": [
+ "WORKSPACE_SETTING",
+ "IMPLICIT_CANONICAL_APP"
+ ],
+ "type": "string"
+ },
"PluginInterface": {
"properties": {
"brandColor": {
@@ -445,6 +452,16 @@
"installPolicy": {
"$ref": "#/definitions/PluginInstallPolicy"
},
+ "installPolicySource": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/PluginInstallPolicySource"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"installed": {
"type": "boolean"
},
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PluginInstallPolicySource.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PluginInstallPolicySource.ts
new file mode 100644
index 0000000000..caa39628ac
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginInstallPolicySource.ts
@@ -0,0 +1,5 @@
+// GENERATED CODE! DO NOT MODIFY BY HAND!
+
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
+export type PluginInstallPolicySource = "WORKSPACE_SETTING" | "IMPLICIT_CANONICAL_APP";
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PluginSummary.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PluginSummary.ts
index d12f55b3a3..10fd5aa25f 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/PluginSummary.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginSummary.ts
@@ -4,6 +4,7 @@
import type { PluginAuthPolicy } from "./PluginAuthPolicy";
import type { PluginAvailability } from "./PluginAvailability";
import type { PluginInstallPolicy } from "./PluginInstallPolicy";
+import type { PluginInstallPolicySource } from "./PluginInstallPolicySource";
import type { PluginInterface } from "./PluginInterface";
import type { PluginShareContext } from "./PluginShareContext";
import type { PluginSource } from "./PluginSource";
@@ -24,7 +25,7 @@ localVersion: string | null, name: string,
/**
* Remote sharing context associated with this plugin when available.
*/
-shareContext: PluginShareContext | null, source: PluginSource, installed: boolean, enabled: boolean, installPolicy: PluginInstallPolicy, authPolicy: PluginAuthPolicy,
+shareContext: PluginShareContext | null, source: PluginSource, installed: boolean, enabled: boolean, installPolicy: PluginInstallPolicy, installPolicySource: PluginInstallPolicySource | null, authPolicy: PluginAuthPolicy,
/**
* Availability state for installing and using the plugin.
*/
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts
index c335f144ec..00d39b31e3 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts
@@ -294,6 +294,7 @@ export type { PluginDetail } from "./PluginDetail";
export type { PluginHookSummary } from "./PluginHookSummary";
export type { PluginInstallParams } from "./PluginInstallParams";
export type { PluginInstallPolicy } from "./PluginInstallPolicy";
+export type { PluginInstallPolicySource } from "./PluginInstallPolicySource";
export type { PluginInstallResponse } from "./PluginInstallResponse";
export type { PluginInstalledParams } from "./PluginInstalledParams";
export type { PluginInstalledResponse } from "./PluginInstalledResponse";
diff --git a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs
index 19a4ccd889..9bd53b5d1a 100644
--- a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs
+++ b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs
@@ -567,6 +567,17 @@ pub enum PluginInstallPolicy {
InstalledByDefault,
}
+#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
+#[ts(export_to = "v2/")]
+pub enum PluginInstallPolicySource {
+ #[serde(rename = "WORKSPACE_SETTING")]
+ #[ts(rename = "WORKSPACE_SETTING")]
+ WorkspaceSetting,
+ #[serde(rename = "IMPLICIT_CANONICAL_APP")]
+ #[ts(rename = "IMPLICIT_CANONICAL_APP")]
+ ImplicitCanonicalApp,
+}
+
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[ts(export_to = "v2/")]
pub enum PluginAuthPolicy {
@@ -613,6 +624,7 @@ pub struct PluginSummary {
pub installed: bool,
pub enabled: bool,
pub install_policy: PluginInstallPolicy,
+ pub install_policy_source: Option,
pub auth_policy: PluginAuthPolicy,
/// Availability state for installing and using the plugin.
#[serde(default)]
diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs
index 77d8a1439b..377ecba2c2 100644
--- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs
+++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs
@@ -3650,6 +3650,7 @@ fn plugin_share_list_response_serializes_share_items() {
installed: false,
enabled: false,
install_policy: PluginInstallPolicy::Available,
+ install_policy_source: Some(PluginInstallPolicySource::WorkspaceSetting),
auth_policy: PluginAuthPolicy::OnUse,
availability: PluginAvailability::Available,
interface: None,
@@ -3672,6 +3673,7 @@ fn plugin_share_list_response_serializes_share_items() {
"installed": false,
"enabled": false,
"installPolicy": "AVAILABLE",
+ "installPolicySource": "WORKSPACE_SETTING",
"authPolicy": "ON_USE",
"availability": "AVAILABLE",
"interface": null,
diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md
index a7986753ee..8300c70d94 100644
--- a/codex-rs/app-server/README.md
+++ b/codex-rs/app-server/README.md
@@ -213,8 +213,8 @@ Example with notification opt-out:
- `marketplace/add` — add a remote plugin marketplace from an HTTP(S) Git URL, SSH Git URL, or GitHub `owner/repo` shorthand, then persist it into the user marketplace config. Returns the installed root path plus whether the marketplace was already present.
- `marketplace/remove` — remove a configured marketplace by name from the user marketplace config, and delete its installed marketplace root when one exists.
- `marketplace/upgrade` — upgrade all configured Git plugin marketplaces, or one named marketplace when `marketplaceName` is provided. Returns selected marketplace names, upgraded roots, and per-marketplace errors.
-- `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, the remote marketplace `version` and locally materialized `localVersion` when available, plugin `availability` (`AVAILABLE` by default or `DISABLED_BY_ADMIN` for remote plugins blocked upstream), fail-open `marketplaceLoadErrors` entries for marketplace files that could not be parsed or loaded, and best-effort `featuredPluginIds` for the official curated marketplace. Clients can explicitly request the remote `workspace-directory`, `shared-with-me`, or `created-by-me-remote` marketplace kinds. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category (**under development; do not call from production clients yet**).
-- `plugin/installed` — list installed plugin rows plus any explicitly requested local install-suggestion plugin names, without fetching the broader remote catalog. Mention surfaces can use this narrower view when they need plugin mention payloads rather than plugin-page discovery data (**under development; do not call from production clients yet**).
+- `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, nullable remote install-policy provenance in `installPolicySource` (`WORKSPACE_SETTING` or `IMPLICIT_CANONICAL_APP`), the remote marketplace `version` and locally materialized `localVersion` when available, plugin `availability` (`AVAILABLE` by default or `DISABLED_BY_ADMIN` for remote plugins blocked upstream), fail-open `marketplaceLoadErrors` entries for marketplace files that could not be parsed or loaded, and best-effort `featuredPluginIds` for the official curated marketplace. Clients can explicitly request the remote `workspace-directory`, `shared-with-me`, or `created-by-me-remote` marketplace kinds. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category (**under development; do not call from production clients yet**).
+- `plugin/installed` — list installed plugin rows plus any explicitly requested local install-suggestion plugin names, without fetching the broader remote catalog. Remote rows include nullable `installPolicySource`; local rows return `null`. Mention surfaces can use this narrower view when they need plugin mention payloads rather than plugin-page discovery data (**under development; do not call from production clients yet**).
- `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/hooks/apps/MCP server names. Remote plugin details expose the canonical `shareUrl` supplied by the remote catalog when available; it is `null` for local plugins or when the catalog omits it. This field is separate from `summary.shareContext`, which continues to describe user and workspace sharing state. Returned plugin skills include their current `enabled` state after local config filtering; bundled hooks are returned as lightweight declaration summaries keyed for correlation with `hooks/list`. Use `plugin/install`'s `appsNeedingAuth` to drive post-install authentication and `app/list`'s `isAccessible` to determine current connector accessibility (**under development; do not call from production clients yet**).
- `plugin/skill/read` — read remote plugin skill markdown on demand by `remoteMarketplaceName`, `remotePluginId`, and `skillName`. This lets clients preview uninstalled remote plugin skills without downloading the plugin bundle.
- `skills/changed` — notification emitted when watched local skill files change.
diff --git a/codex-rs/app-server/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs
index 35c12d49c1..f0d7558526 100644
--- a/codex-rs/app-server/src/request_processors/plugins.rs
+++ b/codex-rs/app-server/src/request_processors/plugins.rs
@@ -163,6 +163,7 @@ fn convert_configured_marketplace_plugin_to_plugin_summary(
share_context,
source: marketplace_plugin_source_to_info(plugin.source),
install_policy: plugin.policy.installation.into(),
+ install_policy_source: None,
auth_policy: plugin.policy.authentication.into(),
availability: PluginAvailability::Available,
interface: plugin.interface.map(local_plugin_interface_to_info),
@@ -1104,6 +1105,7 @@ impl PluginRequestProcessor {
installed: outcome.plugin.installed,
enabled: outcome.plugin.enabled,
install_policy: outcome.plugin.policy.installation.into(),
+ install_policy_source: None,
auth_policy: outcome.plugin.policy.authentication.into(),
availability: PluginAvailability::Available,
interface: outcome.plugin.interface.map(local_plugin_interface_to_info),
@@ -2180,6 +2182,7 @@ fn remote_plugin_summary_to_info(summary: RemoteCatalogPluginSummary) -> PluginS
installed: summary.installed,
enabled: summary.enabled,
install_policy: summary.install_policy,
+ install_policy_source: summary.install_policy_source,
auth_policy: summary.auth_policy,
availability: summary.availability,
interface: summary.interface,
diff --git a/codex-rs/app-server/tests/common/test_app_server.rs b/codex-rs/app-server/tests/common/test_app_server.rs
index cbb8ff161e..0fc0594baa 100644
--- a/codex-rs/app-server/tests/common/test_app_server.rs
+++ b/codex-rs/app-server/tests/common/test_app_server.rs
@@ -111,6 +111,7 @@ use codex_app_server_protocol::TurnCompletedNotification;
use codex_app_server_protocol::TurnEnvironmentParams;
use codex_app_server_protocol::TurnInterruptParams;
use codex_app_server_protocol::TurnStartParams;
+use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::TurnSteerParams;
use codex_app_server_protocol::WindowsSandboxSetupStartParams;
use codex_exec_server::CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR;
@@ -216,15 +217,21 @@ impl TestAppServer {
Ok(app_server)
}
+ /// Returns the automatically selected test environment retained by this server.
+ ///
+ /// Tests can use the environment to arrange target-native filesystem fixtures before starting
+ /// a thread. Returns an error unless this server was created with [`Self::new_with_auto_env`].
+ pub fn auto_env(&self) -> anyhow::Result<&TestEnv> {
+ self.auto_env
+ .as_ref()
+ .context("auto environment is unavailable; use TestAppServer::new_with_auto_env")
+ }
+
/// Returns app-server protocol parameters for the automatically selected
/// test environment. Returns an error unless this server was created with
/// [`Self::new_with_auto_env`].
pub fn auto_env_params(&self) -> anyhow::Result {
- let selection = self
- .auto_env
- .as_ref()
- .context("auto environment is unavailable; use TestAppServer::new_with_auto_env")?
- .selection();
+ let selection = self.auto_env()?.selection();
Ok(TurnEnvironmentParams {
environment_id: selection.environment_id.clone(),
cwd: selection.cwd.clone().into(),
@@ -1052,6 +1059,39 @@ impl TestAppServer {
self.send_request("turn/start", params).await
}
+ /// Start a turn and return its matching typed completion notification.
+ pub async fn start_turn_and_wait_for_completion(
+ &mut self,
+ params: TurnStartParams,
+ ) -> anyhow::Result {
+ let thread_id = params.thread_id.clone();
+ let request_id = self.send_turn_start_request(params).await?;
+ let response = self
+ .read_stream_until_response_message(RequestId::Integer(request_id))
+ .await?;
+ let TurnStartResponse { turn } = crate::to_response(response)?;
+ let notification = self
+ .read_stream_until_matching_notification(
+ "turn/completed for started turn",
+ |notification| {
+ notification.method == "turn/completed"
+ && notification.params.as_ref().is_some_and(|params| {
+ serde_json::from_value::(params.clone())
+ .is_ok_and(|completed| {
+ completed.thread_id == thread_id && completed.turn.id == turn.id
+ })
+ })
+ },
+ )
+ .await?;
+ let params = notification
+ .params
+ .context("turn/completed notification must include params")?;
+ let completed = serde_json::from_value(params)
+ .context("failed to deserialize turn/completed notification")?;
+ Ok(completed)
+ }
+
/// Send a `thread/inject_items` JSON-RPC request (v2).
pub async fn send_thread_inject_items_request(
&mut self,
diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs
index bdb03271d0..3d55079dc6 100644
--- a/codex-rs/app-server/tests/suite/v2/mod.rs
+++ b/codex-rs/app-server/tests/suite/v2/mod.rs
@@ -60,6 +60,7 @@ mod review;
mod safety_check_downgrade;
#[cfg(not(target_os = "windows"))]
mod selected_capability_stack;
+mod selected_environment;
mod skills_list;
mod sleep;
mod thread_archive;
diff --git a/codex-rs/app-server/tests/suite/v2/plugin_list.rs b/codex-rs/app-server/tests/suite/v2/plugin_list.rs
index ae03b3a1a9..0e4bb4a16a 100644
--- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs
+++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs
@@ -9,6 +9,7 @@ use app_test_support::write_chatgpt_auth;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::PluginAuthPolicy;
use codex_app_server_protocol::PluginInstallPolicy;
+use codex_app_server_protocol::PluginInstallPolicySource;
use codex_app_server_protocol::PluginInstalledParams;
use codex_app_server_protocol::PluginInstalledResponse;
use codex_app_server_protocol::PluginListMarketplaceKind;
@@ -196,6 +197,12 @@ enabled = true
]
);
assert_eq!(response.marketplace_load_errors, Vec::new());
+ assert!(
+ response.marketplaces[0]
+ .plugins
+ .iter()
+ .all(|plugin| plugin.install_policy_source.is_none())
+ );
Ok(())
}
@@ -289,11 +296,17 @@ enabled = true
remote_marketplace
.plugins
.iter()
- .map(|plugin| plugin.id.clone())
+ .map(|plugin| (plugin.id.clone(), plugin.install_policy_source))
.collect::>(),
vec![
- "linear@openai-curated-remote".to_string(),
- "remote-only@openai-curated-remote".to_string(),
+ (
+ "linear@openai-curated-remote".to_string(),
+ Some(PluginInstallPolicySource::WorkspaceSetting),
+ ),
+ (
+ "remote-only@openai-curated-remote".to_string(),
+ Some(PluginInstallPolicySource::WorkspaceSetting),
+ ),
]
);
assert_eq!(response.marketplace_load_errors, Vec::new());
@@ -462,6 +475,7 @@ async fn plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_
installed: false,
enabled: false,
install_policy: PluginInstallPolicy::Available,
+ install_policy_source: None,
auth_policy: PluginAuthPolicy::OnInstall,
availability: codex_app_server_protocol::PluginAvailability::Available,
interface: None,
@@ -754,6 +768,7 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab
installed: false,
enabled: false,
install_policy: PluginInstallPolicy::Available,
+ install_policy_source: None,
auth_policy: PluginAuthPolicy::OnInstall,
availability: codex_app_server_protocol::PluginAvailability::Available,
interface: Some(codex_app_server_protocol::PluginInterface {
@@ -794,6 +809,7 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab
installed: false,
enabled: false,
install_policy: PluginInstallPolicy::Available,
+ install_policy_source: None,
auth_policy: PluginAuthPolicy::OnInstall,
availability: codex_app_server_protocol::PluginAvailability::Available,
interface: None,
@@ -1683,6 +1699,7 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
"name": "linear",
"scope": "GLOBAL",
"installation_policy": "AVAILABLE",
+ "installation_policy_source": "IMPLICIT_CANONICAL_APP",
"authentication_policy": "ON_USE",
"status": "ENABLED",
"release": {
@@ -1722,6 +1739,7 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
"name": "linear",
"scope": "GLOBAL",
"installation_policy": "AVAILABLE",
+ "installation_policy_source": "WORKSPACE_SETTING",
"authentication_policy": "ON_USE",
"status": "ENABLED",
"release": {
@@ -1845,6 +1863,10 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
);
assert_eq!(remote_marketplace.plugins[0].installed, true);
assert_eq!(remote_marketplace.plugins[0].enabled, true);
+ assert_eq!(
+ remote_marketplace.plugins[0].install_policy_source,
+ Some(PluginInstallPolicySource::ImplicitCanonicalApp)
+ );
assert_eq!(
remote_marketplace.plugins[0].availability,
codex_app_server_protocol::PluginAvailability::Available
@@ -1880,6 +1902,10 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
let cached_catalog: serde_json::Value =
serde_json::from_slice(&std::fs::read(&cache_files[0])?)?;
assert_eq!(cached_catalog["schema_version"], serde_json::json!(1));
+ assert_eq!(
+ cached_catalog["plugins"][0]["installation_policy_source"],
+ serde_json::json!("IMPLICIT_CANONICAL_APP")
+ );
assert_eq!(
cached_catalog["plugins"][0]["release"]["interface"]["default_prompts"],
serde_json::json!(["Create a Linear issue", "Review my Linear projects"])
@@ -3938,6 +3964,7 @@ fn remote_installed_plugin_body_with_optional_app_manifest(
"name": "linear",
"scope": "GLOBAL",
"installation_policy": "AVAILABLE",
+ "installation_policy_source": "WORKSPACE_SETTING",
"authentication_policy": "ON_USE",
"release": {{
"version": "{release_version}",
diff --git a/codex-rs/app-server/tests/suite/v2/plugin_read.rs b/codex-rs/app-server/tests/suite/v2/plugin_read.rs
index 41b7990ec8..e3e83957cd 100644
--- a/codex-rs/app-server/tests/suite/v2/plugin_read.rs
+++ b/codex-rs/app-server/tests/suite/v2/plugin_read.rs
@@ -25,6 +25,7 @@ use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::PluginAuthPolicy;
use codex_app_server_protocol::PluginAvailability;
use codex_app_server_protocol::PluginInstallPolicy;
+use codex_app_server_protocol::PluginInstallPolicySource;
use codex_app_server_protocol::PluginReadParams;
use codex_app_server_protocol::PluginReadResponse;
use codex_app_server_protocol::PluginShareDiscoverability;
@@ -144,6 +145,7 @@ apps = true
"name": "example-plugin",
"scope": "GLOBAL",
"installation_policy": "AVAILABLE",
+ "installation_policy_source": "IMPLICIT_CANONICAL_APP",
"authentication_policy": "ON_USE",
"release": {
"version": "1.2.1",
@@ -267,6 +269,10 @@ apps = true
assert_eq!(response.plugin.summary.name, "example-plugin");
assert_eq!(response.plugin.summary.source, PluginSource::Remote);
assert_eq!(response.plugin.summary.share_context, None);
+ assert_eq!(
+ response.plugin.summary.install_policy_source,
+ Some(PluginInstallPolicySource::ImplicitCanonicalApp)
+ );
assert_eq!(
response
.plugin
diff --git a/codex-rs/app-server/tests/suite/v2/plugin_share.rs b/codex-rs/app-server/tests/suite/v2/plugin_share.rs
index a82b363806..3f1c2734f2 100644
--- a/codex-rs/app-server/tests/suite/v2/plugin_share.rs
+++ b/codex-rs/app-server/tests/suite/v2/plugin_share.rs
@@ -179,6 +179,7 @@ async fn plugin_share_save_uploads_local_plugin() -> Result<()> {
installed: true,
enabled: true,
install_policy: PluginInstallPolicy::Available,
+ install_policy_source: None,
auth_policy: PluginAuthPolicy::OnUse,
availability: codex_app_server_protocol::PluginAvailability::Available,
interface: Some(expected_plugin_interface()),
@@ -583,6 +584,7 @@ async fn plugin_share_list_returns_created_workspace_plugins() -> Result<()> {
installed: true,
enabled: true,
install_policy: PluginInstallPolicy::Available,
+ install_policy_source: None,
auth_policy: PluginAuthPolicy::OnUse,
availability: codex_app_server_protocol::PluginAvailability::Available,
interface: Some(expected_plugin_interface()),
@@ -1184,6 +1186,7 @@ async fn plugin_share_delete_removes_created_workspace_plugin() -> Result<()> {
installed: true,
enabled: true,
install_policy: PluginInstallPolicy::Available,
+ install_policy_source: None,
auth_policy: PluginAuthPolicy::OnUse,
availability: codex_app_server_protocol::PluginAvailability::Available,
interface: Some(expected_plugin_interface()),
diff --git a/codex-rs/app-server/tests/suite/v2/selected_environment.rs b/codex-rs/app-server/tests/suite/v2/selected_environment.rs
new file mode 100644
index 0000000000..513cd25d7f
--- /dev/null
+++ b/codex-rs/app-server/tests/suite/v2/selected_environment.rs
@@ -0,0 +1,252 @@
+use std::collections::BTreeMap;
+use std::time::Duration;
+
+use anyhow::Context;
+use anyhow::Result;
+use app_test_support::PathBufExt;
+use app_test_support::TestAppServer;
+use app_test_support::to_response;
+use app_test_support::write_mock_responses_config_toml;
+use codex_app_server_protocol::JSONRPCResponse;
+use codex_app_server_protocol::RequestId;
+use codex_app_server_protocol::ThreadStartParams;
+use codex_app_server_protocol::ThreadStartResponse;
+use codex_app_server_protocol::TurnStartParams;
+use codex_app_server_protocol::UserInput as V2UserInput;
+use core_test_support::responses;
+use pretty_assertions::assert_eq;
+use tempfile::TempDir;
+use tokio::time::timeout;
+
+const AGENTS_INSTRUCTIONS: &str = "selected environment workspace instructions";
+const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10);
+
+fn text_turn_params(thread_id: String, prompt: &str) -> TurnStartParams {
+ TurnStartParams {
+ thread_id,
+ input: vec![V2UserInput::Text {
+ text: prompt.to_string(),
+ text_elements: Vec::new(),
+ }],
+ ..Default::default()
+ }
+}
+
+#[tokio::test]
+async fn thread_start_reports_selected_environment_metadata() -> Result<()> {
+ let server = responses::start_mock_server().await;
+ let codex_home = TempDir::new()?;
+ write_mock_responses_config_toml(
+ codex_home.path(),
+ &server.uri(),
+ &BTreeMap::new(),
+ /*auto_compact_limit*/ 100_000,
+ /*requires_openai_auth*/ None,
+ "mock_provider",
+ "compact",
+ )?;
+ let mut app_server = TestAppServer::new_with_auto_env(codex_home.path()).await?;
+ timeout(DEFAULT_READ_TIMEOUT, app_server.initialize()).await??;
+
+ let request_id = app_server
+ .send_thread_start_request_with_auto_env(ThreadStartParams::default())
+ .await?;
+ let response: JSONRPCResponse = timeout(
+ DEFAULT_READ_TIMEOUT,
+ app_server.read_stream_until_response_message(RequestId::Integer(request_id)),
+ )
+ .await??;
+ let ThreadStartResponse {
+ cwd,
+ runtime_workspace_roots,
+ active_permission_profile,
+ ..
+ } = to_response(response)?;
+ let host_cwd = codex_home.path().to_path_buf().abs().canonicalize()?;
+ let cwd = cwd.canonicalize()?;
+ let runtime_workspace_roots = runtime_workspace_roots
+ .into_iter()
+ .map(|root| root.canonicalize())
+ .collect::>>()?;
+ assert_eq!(
+ (cwd, runtime_workspace_roots, active_permission_profile),
+ (
+ // TODO(anp): Return the selected environment's native cwd from thread/start.
+ host_cwd.clone(),
+ // TODO(anp): Derive runtime workspace roots from the selected remote environment.
+ vec![host_cwd],
+ // TODO(anp): Report the implicit built-in permission profile instead of None.
+ None,
+ )
+ );
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn thread_start_reports_selected_environment_instruction_source() -> Result<()> {
+ let server = responses::start_mock_server().await;
+ let response_mock = responses::mount_sse_once(
+ &server,
+ responses::sse(vec![
+ responses::ev_response_created("resp-1"),
+ responses::ev_assistant_message("msg-1", "done"),
+ responses::ev_completed("resp-1"),
+ ]),
+ )
+ .await;
+ let codex_home = TempDir::new()?;
+ write_mock_responses_config_toml(
+ codex_home.path(),
+ &server.uri(),
+ &BTreeMap::new(),
+ /*auto_compact_limit*/ 100_000,
+ /*requires_openai_auth*/ None,
+ "mock_provider",
+ "compact",
+ )?;
+ let mut app_server = TestAppServer::new_with_auto_env(codex_home.path()).await?;
+ timeout(DEFAULT_READ_TIMEOUT, app_server.initialize()).await??;
+
+ let (agents_source, environment_cwd) = {
+ let auto_env = app_server.auto_env()?;
+ let environment_cwd = auto_env.selection().cwd.clone();
+ let agents_source = environment_cwd.join("AGENTS.md")?;
+ auto_env
+ .environment()
+ .get_filesystem()
+ .write_file(
+ &agents_source,
+ AGENTS_INSTRUCTIONS.as_bytes().to_vec(),
+ /*sandbox*/ None,
+ )
+ .await?;
+ (agents_source, environment_cwd)
+ };
+
+ let request_id = app_server
+ .send_thread_start_request_with_auto_env(ThreadStartParams::default())
+ .await?;
+ let response: JSONRPCResponse = timeout(
+ DEFAULT_READ_TIMEOUT,
+ app_server.read_stream_until_response_message(RequestId::Integer(request_id)),
+ )
+ .await??;
+ let response: ThreadStartResponse = to_response(response)?;
+
+ assert_eq!(response.instruction_sources, vec![agents_source.into()]);
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ app_server.start_turn_and_wait_for_completion(text_turn_params(
+ response.thread.id,
+ "inspect workspace instructions",
+ )),
+ )
+ .await??;
+
+ let user_context = response_mock.single_request().message_input_texts("user");
+ let instructions = user_context
+ .iter()
+ .find(|text| text.starts_with("# AGENTS.md instructions"))
+ .context("selected environment instructions should be model visible")?;
+ let expected_instructions = format!(
+ "# AGENTS.md instructions for {}\n\n\n{AGENTS_INSTRUCTIONS}\n",
+ environment_cwd.inferred_native_path_string()
+ );
+ assert_eq!(instructions, &expected_instructions);
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn turn_model_context_uses_selected_environment() -> Result<()> {
+ let server = responses::start_mock_server().await;
+ let response_mock = responses::mount_sse_once(
+ &server,
+ responses::sse(vec![
+ responses::ev_response_created("resp-1"),
+ responses::ev_assistant_message("msg-1", "done"),
+ responses::ev_completed("resp-1"),
+ ]),
+ )
+ .await;
+ let codex_home = TempDir::new()?;
+ write_mock_responses_config_toml(
+ codex_home.path(),
+ &server.uri(),
+ &BTreeMap::new(),
+ /*auto_compact_limit*/ 100_000,
+ /*requires_openai_auth*/ None,
+ "mock_provider",
+ "compact",
+ )?;
+ let mut app_server = TestAppServer::new_with_auto_env(codex_home.path()).await?;
+ timeout(DEFAULT_READ_TIMEOUT, app_server.initialize()).await??;
+ let (environment_cwd, environment_shell) = {
+ let auto_env = app_server.auto_env()?;
+ (
+ auto_env.selection().cwd.clone(),
+ auto_env.environment().info().await?.shell.name,
+ )
+ };
+
+ let request_id = app_server
+ .send_thread_start_request_with_auto_env(ThreadStartParams::default())
+ .await?;
+ let response: JSONRPCResponse = timeout(
+ DEFAULT_READ_TIMEOUT,
+ app_server.read_stream_until_response_message(RequestId::Integer(request_id)),
+ )
+ .await??;
+ let ThreadStartResponse {
+ thread,
+ runtime_workspace_roots,
+ ..
+ } = to_response(response)?;
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ app_server.start_turn_and_wait_for_completion(text_turn_params(
+ thread.id,
+ "inspect the selected environment",
+ )),
+ )
+ .await??;
+
+ let user_context = response_mock.single_request().message_input_texts("user");
+ let environment_context = user_context
+ .iter()
+ .find(|text| text.starts_with(""))
+ .context("selected environment context should be model visible")?;
+ let shell = environment_context
+ .lines()
+ .find(|line| line.trim_start().starts_with(""))
+ .map(str::trim)
+ .map(str::to_string);
+ let cwd = environment_context
+ .lines()
+ .find(|line| line.trim_start().starts_with(""))
+ .map(str::trim)
+ .map(str::to_string);
+ assert_eq!(
+ (shell, cwd),
+ (
+ Some(format!("{environment_shell}")),
+ Some(format!(
+ "{}",
+ environment_cwd.inferred_native_path_string()
+ )),
+ )
+ );
+ let [runtime_workspace_root] = runtime_workspace_roots.as_slice() else {
+ anyhow::bail!("expected one runtime workspace root");
+ };
+ let expected_workspace_roots = format!(
+ "{}",
+ runtime_workspace_root.as_path().display()
+ );
+ // TODO(anp): Derive model-visible workspace roots from the selected remote environment and
+ // render them using its native path convention.
+ assert!(environment_context.contains(&expected_workspace_roots));
+
+ Ok(())
+}
diff --git a/codex-rs/backend-client/Cargo.toml b/codex-rs/backend-client/Cargo.toml
index f7b0c8b0f5..4ff5fb9b86 100644
--- a/codex-rs/backend-client/Cargo.toml
+++ b/codex-rs/backend-client/Cargo.toml
@@ -19,7 +19,7 @@ serde_json = "1"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
codex-backend-openapi-models = { path = "../codex-backend-openapi-models" }
codex-api = { workspace = true }
-codex-client = { workspace = true }
+codex-http-client = { workspace = true }
codex-login = { workspace = true }
codex-model-provider = { workspace = true }
codex-protocol = { workspace = true }
diff --git a/codex-rs/backend-client/src/client.rs b/codex-rs/backend-client/src/client.rs
index be444b6247..a5db25618b 100644
--- a/codex-rs/backend-client/src/client.rs
+++ b/codex-rs/backend-client/src/client.rs
@@ -9,8 +9,8 @@ use crate::types::TokenUsageProfile;
use crate::types::TurnAttemptsSiblingTurnsResponse;
use anyhow::Result;
use codex_api::SharedAuthProvider;
-use codex_client::build_reqwest_client_with_custom_ca;
-use codex_client::with_chatgpt_cloudflare_cookie_store;
+use codex_http_client::build_reqwest_client_with_custom_ca;
+use codex_http_client::with_chatgpt_cloudflare_cookie_store;
use codex_login::CodexAuth;
use codex_login::default_client::get_codex_user_agent;
use codex_protocol::account::PlanType as AccountPlanType;
diff --git a/codex-rs/cloud-tasks/Cargo.toml b/codex-rs/cloud-tasks/Cargo.toml
index e7997259a5..d842a95a94 100644
--- a/codex-rs/cloud-tasks/Cargo.toml
+++ b/codex-rs/cloud-tasks/Cargo.toml
@@ -16,7 +16,7 @@ workspace = true
anyhow = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
clap = { workspace = true, features = ["derive"] }
-codex-client = { workspace = true }
+codex-http-client = { workspace = true }
codex-cloud-tasks-client = { workspace = true }
# TODO: codex-cloud-tasks-mock-client should be in dev-dependencies.
codex-cloud-tasks-mock-client = { workspace = true }
diff --git a/codex-rs/cloud-tasks/src/env_detect.rs b/codex-rs/cloud-tasks/src/env_detect.rs
index cd38c7f347..6137f27ea8 100644
--- a/codex-rs/cloud-tasks/src/env_detect.rs
+++ b/codex-rs/cloud-tasks/src/env_detect.rs
@@ -1,4 +1,4 @@
-use codex_client::build_reqwest_client_with_custom_ca;
+use codex_http_client::build_reqwest_client_with_custom_ca;
use reqwest::header::CONTENT_TYPE;
use reqwest::header::HeaderMap;
use std::collections::HashMap;
diff --git a/codex-rs/codex-api/Cargo.toml b/codex-rs/codex-api/Cargo.toml
index 72aa66a810..084b058e12 100644
--- a/codex-rs/codex-api/Cargo.toml
+++ b/codex-rs/codex-api/Cargo.toml
@@ -10,6 +10,7 @@ base64 = { workspace = true }
bytes = { workspace = true }
chrono = { workspace = true }
codex-client = { workspace = true }
+codex-http-client = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-rustls-provider = { workspace = true }
futures = { workspace = true }
diff --git a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs
index 0ae443d00a..7b1a74e4ae 100644
--- a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs
+++ b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs
@@ -16,7 +16,7 @@ use crate::endpoint::realtime_websocket::protocol::parse_realtime_event;
use crate::error::ApiError;
use crate::provider::Provider;
use codex_client::backoff;
-use codex_client::maybe_build_rustls_client_config_with_custom_ca;
+use codex_http_client::maybe_build_rustls_client_config_with_custom_ca;
use codex_protocol::protocol::ConversationTextRole;
use codex_protocol::protocol::RealtimeTranscriptDelta;
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
diff --git a/codex-rs/codex-api/src/endpoint/responses_websocket.rs b/codex-rs/codex-api/src/endpoint/responses_websocket.rs
index 7f261bba36..447450fae1 100644
--- a/codex-rs/codex-api/src/endpoint/responses_websocket.rs
+++ b/codex-rs/codex-api/src/endpoint/responses_websocket.rs
@@ -11,7 +11,7 @@ use crate::sse::ResponsesStreamEvent;
use crate::sse::process_responses_event;
use crate::telemetry::WebsocketTelemetry;
use codex_client::TransportError;
-use codex_client::maybe_build_rustls_client_config_with_custom_ca;
+use codex_http_client::maybe_build_rustls_client_config_with_custom_ca;
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
use futures::SinkExt;
use futures::StreamExt;
diff --git a/codex-rs/codex-api/src/files.rs b/codex-rs/codex-api/src/files.rs
index 8bcf1a4d0d..3cd3932d06 100644
--- a/codex-rs/codex-api/src/files.rs
+++ b/codex-rs/codex-api/src/files.rs
@@ -2,7 +2,7 @@ use std::time::Duration;
use crate::AuthProvider;
use bytes::Bytes;
-use codex_client::build_reqwest_client_with_custom_ca;
+use codex_http_client::build_reqwest_client_with_custom_ca;
use futures::Stream;
use reqwest::StatusCode;
use reqwest::header::CONTENT_LENGTH;
diff --git a/codex-rs/codex-client/BUILD.bazel b/codex-rs/codex-client/BUILD.bazel
index 3d26819111..dd7e504634 100644
--- a/codex-rs/codex-client/BUILD.bazel
+++ b/codex-rs/codex-client/BUILD.bazel
@@ -2,6 +2,5 @@ load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "codex-client",
- compile_data = glob(["tests/fixtures/**"]),
crate_name = "codex_client",
)
diff --git a/codex-rs/codex-client/Cargo.toml b/codex-rs/codex-client/Cargo.toml
index fcd97b6bda..8e354ebeea 100644
--- a/codex-rs/codex-client/Cargo.toml
+++ b/codex-rs/codex-client/Cargo.toml
@@ -5,47 +5,16 @@ name = "codex-client"
version.workspace = true
[dependencies]
-bytes = { workspace = true }
+codex-http-client = { workspace = true }
eventsource-stream = { workspace = true }
futures = { workspace = true }
http = { workspace = true }
-opentelemetry = { workspace = true }
rand = { workspace = true }
-reqwest = { workspace = true, features = ["json", "rustls-tls-native-roots", "stream"] }
-rustls = { workspace = true }
-rustls-native-certs = { workspace = true }
-rustls-pki-types = { workspace = true }
-serde = { workspace = true, features = ["derive"] }
-serde_json = { workspace = true }
-thiserror = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt", "time", "sync"] }
-tracing = { workspace = true }
-tracing-opentelemetry = { workspace = true }
-codex-utils-rustls-provider = { workspace = true }
-zstd = { workspace = true }
-
-[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
-sha2 = { workspace = true }
-
-[target.'cfg(target_os = "macos")'.dependencies]
-system-configuration = { workspace = true }
-
-[target.'cfg(target_os = "windows")'.dependencies]
-windows-sys = { version = "0.52", features = [
- "Win32_Foundation",
- "Win32_Networking_WinHttp",
-] }
[lints]
workspace = true
-[dev-dependencies]
-codex-utils-cargo-bin = { workspace = true }
-opentelemetry_sdk = { workspace = true }
-pretty_assertions = { workspace = true }
-rcgen = { workspace = true }
-tempfile = { workspace = true }
-tracing-subscriber = { workspace = true }
-
[lib]
doctest = false
+test = false
diff --git a/codex-rs/codex-client/README.md b/codex-rs/codex-client/README.md
index 045ee7b343..1e4073117a 100644
--- a/codex-rs/codex-client/README.md
+++ b/codex-rs/codex-client/README.md
@@ -1,8 +1,8 @@
# codex-client
-Generic transport layer that wraps HTTP requests, retries, and streaming primitives without any Codex/OpenAI awareness.
+Higher-level request policy layered on `codex-http-client` without any Codex/OpenAI API awareness.
-- Defines `HttpTransport` and a default `ReqwestTransport` plus thin `Request`/`Response` types.
- Provides retry utilities (`RetryPolicy`, `RetryOn`, `run_with_retry`, `backoff`) that callers plug into for unary and streaming calls.
- Supplies the `sse_stream` helper to turn byte streams into raw SSE `data:` frames with idle timeouts and surfaced stream errors.
-- Consumed by higher-level crates like `codex-api`; it stays neutral on endpoints, headers, or API-specific error shapes.
+- Defines the request telemetry callback used by higher-level clients.
+- Re-exports the low-level HTTP types temporarily so consumers can migrate to `codex-http-client` incrementally.
diff --git a/codex-rs/codex-client/src/lib.rs b/codex-rs/codex-client/src/lib.rs
index 78daa6c288..4b83ba0dd0 100644
--- a/codex-rs/codex-client/src/lib.rs
+++ b/codex-rs/codex-client/src/lib.rs
@@ -1,49 +1,13 @@
-mod chatgpt_cloudflare_cookies;
-mod chatgpt_hosts;
-mod custom_ca;
-mod default_client;
-mod error;
-mod outbound_proxy;
-mod request;
mod retry;
mod sse;
mod telemetry;
-mod transport;
-pub use crate::chatgpt_cloudflare_cookies::with_chatgpt_cloudflare_cookie_store;
-pub use crate::chatgpt_hosts::is_allowed_chatgpt_host;
-pub use crate::custom_ca::BuildCustomCaTransportError;
-/// Test-only subprocess hook for custom CA coverage.
-///
-/// This stays public only so the `custom_ca_probe` binary target can reuse the shared helper. It
-/// is hidden from normal docs because ordinary callers should use
-/// [`build_reqwest_client_with_custom_ca`] instead.
-#[doc(hidden)]
-pub use crate::custom_ca::build_reqwest_client_for_subprocess_tests;
-pub use crate::custom_ca::build_reqwest_client_with_custom_ca;
-pub use crate::custom_ca::maybe_build_rustls_client_config_with_custom_ca;
-pub use crate::default_client::CodexHttpClient;
-pub use crate::default_client::CodexRequestBuilder;
-pub use crate::error::StreamError;
-pub use crate::error::TransportError;
-pub use crate::outbound_proxy::BuildRouteAwareHttpClientError;
-pub use crate::outbound_proxy::ClientRouteClass;
-pub use crate::outbound_proxy::OutboundProxyConfig;
-pub use crate::outbound_proxy::RouteFailureClass;
-pub use crate::outbound_proxy::build_reqwest_client_for_route;
-pub use crate::request::EncodedJsonBody;
-pub use crate::request::PreparedRequestBody;
-pub use crate::request::Request;
-pub use crate::request::RequestBody;
-pub use crate::request::RequestCompression;
-pub use crate::request::Response;
pub use crate::retry::RetryOn;
pub use crate::retry::RetryPolicy;
pub use crate::retry::backoff;
pub use crate::retry::run_with_retry;
pub use crate::sse::sse_stream;
pub use crate::telemetry::RequestTelemetry;
-pub use crate::transport::ByteStream;
-pub use crate::transport::HttpTransport;
-pub use crate::transport::ReqwestTransport;
-pub use crate::transport::StreamResponse;
+pub use codex_http_client::HttpClient as CodexHttpClient;
+pub use codex_http_client::RequestBuilder as CodexRequestBuilder;
+pub use codex_http_client::*;
diff --git a/codex-rs/codex-client/src/retry.rs b/codex-rs/codex-client/src/retry.rs
index c7bdd34b1e..9eb408878b 100644
--- a/codex-rs/codex-client/src/retry.rs
+++ b/codex-rs/codex-client/src/retry.rs
@@ -1,5 +1,5 @@
-use crate::error::TransportError;
-use crate::request::Request;
+use codex_http_client::Request;
+use codex_http_client::TransportError;
use rand::Rng;
use std::future::Future;
use std::time::Duration;
diff --git a/codex-rs/codex-client/src/sse.rs b/codex-rs/codex-client/src/sse.rs
index f3aba3a2c5..ed1591f6de 100644
--- a/codex-rs/codex-client/src/sse.rs
+++ b/codex-rs/codex-client/src/sse.rs
@@ -1,5 +1,5 @@
-use crate::error::StreamError;
-use crate::transport::ByteStream;
+use codex_http_client::ByteStream;
+use codex_http_client::StreamError;
use eventsource_stream::Eventsource;
use futures::StreamExt;
use tokio::sync::mpsc;
diff --git a/codex-rs/codex-client/src/telemetry.rs b/codex-rs/codex-client/src/telemetry.rs
index 457d47f4fc..b856414d33 100644
--- a/codex-rs/codex-client/src/telemetry.rs
+++ b/codex-rs/codex-client/src/telemetry.rs
@@ -1,4 +1,4 @@
-use crate::error::TransportError;
+use codex_http_client::TransportError;
use http::StatusCode;
use std::time::Duration;
diff --git a/codex-rs/core-plugins/src/manager_tests.rs b/codex-rs/core-plugins/src/manager_tests.rs
index 469a9a7fc5..5e743416f2 100644
--- a/codex-rs/core-plugins/src/manager_tests.rs
+++ b/codex-rs/core-plugins/src/manager_tests.rs
@@ -781,6 +781,7 @@ fn remote_installed_plugin_in_marketplace(
name: name.to_string(),
enabled: true,
install_policy: codex_app_server_protocol::PluginInstallPolicy::Available,
+ install_policy_source: None,
auth_policy: codex_app_server_protocol::PluginAuthPolicy::OnUse,
availability: codex_app_server_protocol::PluginAvailability::Available,
interface: None,
diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs
index 4c9bd2e672..b9b94f4ded 100644
--- a/codex-rs/core-plugins/src/remote.rs
+++ b/codex-rs/core-plugins/src/remote.rs
@@ -6,6 +6,7 @@ use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::PluginAuthPolicy;
use codex_app_server_protocol::PluginAvailability;
use codex_app_server_protocol::PluginInstallPolicy;
+use codex_app_server_protocol::PluginInstallPolicySource;
use codex_app_server_protocol::PluginInterface;
use codex_app_server_protocol::SkillInterface;
use codex_login::CodexAuth;
@@ -152,6 +153,7 @@ pub struct RemoteInstalledPlugin {
pub name: String,
pub enabled: bool,
pub install_policy: PluginInstallPolicy,
+ pub install_policy_source: Option,
pub auth_policy: PluginAuthPolicy,
pub availability: PluginAvailability,
pub interface: Option,
@@ -169,6 +171,7 @@ pub struct RemotePluginSummary {
pub installed: bool,
pub enabled: bool,
pub install_policy: PluginInstallPolicy,
+ pub install_policy_source: Option,
pub auth_policy: PluginAuthPolicy,
pub availability: PluginAvailability,
pub interface: Option,
@@ -528,6 +531,26 @@ struct RemoteAppTemplateResponse {
reason: Option,
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
+enum RemotePluginInstallPolicySource {
+ #[serde(rename = "WORKSPACE_SETTING")]
+ WorkspaceSetting,
+ #[serde(rename = "IMPLICIT_CANONICAL_APP")]
+ ImplicitCanonicalApp,
+ #[serde(other)]
+ Unknown,
+}
+
+impl RemotePluginInstallPolicySource {
+ fn into_protocol(self) -> Option {
+ match self {
+ Self::WorkspaceSetting => Some(PluginInstallPolicySource::WorkspaceSetting),
+ Self::ImplicitCanonicalApp => Some(PluginInstallPolicySource::ImplicitCanonicalApp),
+ Self::Unknown => None,
+ }
+ }
+}
+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
struct RemotePluginDirectoryItem {
id: String,
@@ -544,6 +567,7 @@ struct RemotePluginDirectoryItem {
#[serde(default)]
share_principals: Option>,
installation_policy: PluginInstallPolicy,
+ installation_policy_source: Option,
authentication_policy: PluginAuthPolicy,
#[serde(rename = "status", default)]
availability: PluginAvailability,
@@ -1050,6 +1074,7 @@ pub fn group_remote_installed_plugins_by_marketplaces(
installed: true,
enabled: plugin.enabled,
install_policy: plugin.install_policy,
+ install_policy_source: plugin.install_policy_source,
auth_policy: plugin.auth_policy,
availability: plugin.availability,
interface: plugin.interface.clone(),
@@ -1483,6 +1508,9 @@ fn build_remote_plugin_summary(
installed: installed_plugin.is_some(),
enabled: installed_plugin.is_some_and(|plugin| plugin.enabled),
install_policy: plugin.installation_policy,
+ install_policy_source: plugin
+ .installation_policy_source
+ .and_then(RemotePluginInstallPolicySource::into_protocol),
auth_policy: plugin.authentication_policy,
availability: plugin.availability,
interface: remote_plugin_interface_to_info(plugin),
@@ -1562,6 +1590,9 @@ fn remote_installed_plugin_to_cache_entry(
name: plugin.name.clone(),
enabled: installed_plugin.enabled,
install_policy: plugin.installation_policy,
+ install_policy_source: plugin
+ .installation_policy_source
+ .and_then(RemotePluginInstallPolicySource::into_protocol),
auth_policy: plugin.authentication_policy,
availability: plugin.availability,
interface: remote_plugin_interface_to_info(plugin),
diff --git a/codex-rs/core-plugins/src/remote/share/tests.rs b/codex-rs/core-plugins/src/remote/share/tests.rs
index d7744681b9..e9887370c5 100644
--- a/codex-rs/core-plugins/src/remote/share/tests.rs
+++ b/codex-rs/core-plugins/src/remote/share/tests.rs
@@ -648,6 +648,7 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
installed: false,
enabled: false,
install_policy: PluginInstallPolicy::Available,
+ install_policy_source: None,
auth_policy: PluginAuthPolicy::OnUse,
availability: PluginAvailability::Available,
interface: Some(expected_plugin_interface()),
@@ -687,6 +688,7 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
installed: true,
enabled: true,
install_policy: PluginInstallPolicy::Available,
+ install_policy_source: None,
auth_policy: PluginAuthPolicy::OnUse,
availability: PluginAvailability::Available,
interface: Some(expected_plugin_interface()),
diff --git a/codex-rs/core-plugins/src/remote_tests.rs b/codex-rs/core-plugins/src/remote_tests.rs
index f1d4478d7f..29fe8add19 100644
--- a/codex-rs/core-plugins/src/remote_tests.rs
+++ b/codex-rs/core-plugins/src/remote_tests.rs
@@ -33,6 +33,69 @@ fn build_remote_marketplace_preserves_directory_order_and_appends_installed_only
);
}
+#[test]
+fn installation_policy_source_is_preserved_across_remote_summary_paths() {
+ let mut directory_plugin = directory_plugin("plugin-linear", "linear");
+ directory_plugin.installation_policy_source =
+ Some(RemotePluginInstallPolicySource::ImplicitCanonicalApp);
+ let installed_plugin = RemotePluginInstalledItem {
+ plugin: directory_plugin.clone(),
+ enabled: true,
+ disabled_skill_names: Vec::new(),
+ };
+
+ let marketplace = build_remote_marketplace(
+ REMOTE_GLOBAL_MARKETPLACE_NAME,
+ REMOTE_GLOBAL_MARKETPLACE_DISPLAY_NAME,
+ vec![directory_plugin],
+ vec![installed_plugin.clone()],
+ /*include_installed_only*/ false,
+ )
+ .expect("marketplace should be valid")
+ .expect("marketplace should not be empty");
+ assert_eq!(
+ marketplace
+ .plugins
+ .into_iter()
+ .map(|plugin| plugin.install_policy_source)
+ .collect::>(),
+ vec![Some(PluginInstallPolicySource::ImplicitCanonicalApp)]
+ );
+
+ let mut installed_plugin = installed_plugin;
+ installed_plugin.plugin.installation_policy_source =
+ Some(RemotePluginInstallPolicySource::WorkspaceSetting);
+ let installed_plugin = remote_installed_plugin_to_cache_entry(&installed_plugin)
+ .expect("installed plugin should be valid");
+ let marketplaces = group_remote_installed_plugins_by_marketplaces(
+ &[installed_plugin],
+ &[REMOTE_GLOBAL_MARKETPLACE_NAME],
+ );
+ assert_eq!(
+ marketplaces
+ .into_iter()
+ .flat_map(|marketplace| marketplace.plugins)
+ .map(|plugin| plugin.install_policy_source)
+ .collect::>(),
+ vec![Some(PluginInstallPolicySource::WorkspaceSetting)]
+ );
+}
+
+#[test]
+fn unknown_installation_policy_source_maps_to_none() {
+ let plugin = directory_plugin("plugin-linear", "linear");
+ let mut plugin_json = serde_json::to_value(plugin).expect("plugin should serialize");
+ plugin_json["installation_policy_source"] =
+ serde_json::Value::String("FUTURE_POLICY_SOURCE".to_string());
+ let plugin: RemotePluginDirectoryItem =
+ serde_json::from_value(plugin_json).expect("unknown source should deserialize");
+
+ let summary = build_remote_plugin_summary(&plugin, /*installed_plugin*/ None)
+ .expect("summary should be valid");
+
+ assert_eq!(summary.install_policy_source, None);
+}
+
fn directory_plugin(id: &str, name: &str) -> RemotePluginDirectoryItem {
RemotePluginDirectoryItem {
id: id.to_string(),
@@ -44,6 +107,7 @@ fn directory_plugin(id: &str, name: &str) -> RemotePluginDirectoryItem {
share_url: None,
share_principals: None,
installation_policy: PluginInstallPolicy::Available,
+ installation_policy_source: None,
authentication_policy: PluginAuthPolicy::OnUse,
availability: PluginAvailability::Available,
release: RemotePluginReleaseResponse {
diff --git a/codex-rs/core/src/exec_policy.rs b/codex-rs/core/src/exec_policy.rs
index cbffcd7e43..8f31f150b3 100644
--- a/codex-rs/core/src/exec_policy.rs
+++ b/codex-rs/core/src/exec_policy.rs
@@ -554,12 +554,19 @@ pub fn format_exec_policy_error_with_source(error: &ExecPolicyError) -> String {
}
}
-async fn load_exec_policy_with_warning(
+pub(crate) async fn load_exec_policy_with_warning(
config_stack: &ConfigLayerStack,
) -> Result<(Policy, Option), ExecPolicyError> {
match load_exec_policy(config_stack).await {
Ok(policy) => Ok((policy, None)),
- Err(err @ ExecPolicyError::ParsePolicy { .. }) => Ok((Policy::empty(), Some(err))),
+ Err(err @ ExecPolicyError::ParsePolicy { .. }) => {
+ let policy = config_stack
+ .requirements()
+ .exec_policy
+ .as_deref()
+ .map_or_else(Policy::empty, |policy| policy.as_ref().clone());
+ Ok((policy, Some(err)))
+ }
Err(err) => Err(err),
}
}
diff --git a/codex-rs/core/src/exec_policy_tests.rs b/codex-rs/core/src/exec_policy_tests.rs
index cf17387ef5..95d29e0bac 100644
--- a/codex-rs/core/src/exec_policy_tests.rs
+++ b/codex-rs/core/src/exec_policy_tests.rs
@@ -374,6 +374,42 @@ async fn merges_requirements_exec_policy_network_rules() -> anyhow::Result<()> {
Ok(())
}
+#[tokio::test]
+async fn malformed_custom_rules_preserve_requirements_exec_policy() -> anyhow::Result<()> {
+ let temp_dir = tempdir()?;
+ let policy_dir = temp_dir.path().join(RULES_DIR_NAME);
+ fs::create_dir_all(&policy_dir)?;
+ fs::write(policy_dir.join("broken.rules"), "prefix_rule(")?;
+
+ let mut requirements_exec_policy = Policy::empty();
+ requirements_exec_policy.add_prefix_rule(&["rm".to_string()], Decision::Forbidden)?;
+ let requirements = ConfigRequirements {
+ exec_policy: Some(Sourced::new(
+ RequirementsExecPolicy::new(requirements_exec_policy),
+ RequirementSource::Unknown,
+ )),
+ ..ConfigRequirements::default()
+ };
+ let dot_codex_folder = AbsolutePathBuf::from_absolute_path(temp_dir.path())?;
+ let layer = ConfigLayerEntry::new(
+ ConfigLayerSource::Project { dot_codex_folder },
+ TomlValue::Table(Default::default()),
+ );
+ let config_stack =
+ ConfigLayerStack::new(vec![layer], requirements, ConfigRequirementsToml::default())?;
+
+ let (policy, warning) = load_exec_policy_with_warning(&config_stack).await?;
+
+ assert!(matches!(warning, Some(ExecPolicyError::ParsePolicy { .. })));
+ assert_eq!(
+ policy
+ .check_multiple([vec!["rm".to_string()]].iter(), &|_| Decision::Allow)
+ .decision,
+ Decision::Forbidden
+ );
+ Ok(())
+}
+
#[tokio::test]
async fn preserves_host_executables_when_requirements_overlay_is_present() -> anyhow::Result<()> {
let temp_dir = tempdir()?;
diff --git a/codex-rs/core/src/network_proxy_loader.rs b/codex-rs/core/src/network_proxy_loader.rs
index 0a4fe2d7c2..62ed44ad81 100644
--- a/codex-rs/core/src/network_proxy_loader.rs
+++ b/codex-rs/core/src/network_proxy_loader.rs
@@ -2,9 +2,8 @@ use crate::config::find_codex_home;
use crate::config::is_builtin_permission_profile_name;
use crate::config::reject_unknown_builtin_permission_profile;
use crate::config::resolve_permission_profile;
-use crate::exec_policy::ExecPolicyError;
use crate::exec_policy::format_exec_policy_error_with_source;
-use crate::exec_policy::load_exec_policy;
+use crate::exec_policy::load_exec_policy_with_warning;
use anyhow::Context;
use anyhow::Result;
use codex_config::CONFIG_TOML_FILE;
@@ -64,13 +63,15 @@ async fn build_config_state_with_mtimes() -> Result<(ConfigState, Vec (policy, None),
- Err(err @ ExecPolicyError::ParsePolicy { .. }) => {
- (codex_execpolicy::Policy::empty(), Some(err))
- }
- Err(err) => return Err(err.into()),
- };
+ let layer_mtimes = collect_layer_mtimes(&config_layer_stack);
+ let state = build_config_state_from_layers(&config_layer_stack).await?;
+ Ok((state, layer_mtimes))
+}
+
+async fn build_config_state_from_layers(
+ config_layer_stack: &ConfigLayerStack,
+) -> Result {
+ let (exec_policy, warning) = load_exec_policy_with_warning(config_layer_stack).await?;
if let Some(err) = warning.as_ref() {
tracing::warn!(
"failed to parse execpolicy while building network proxy state: {}",
@@ -78,12 +79,10 @@ async fn build_config_state_with_mtimes() -> Result<(ConfigState, Vec Vec {
diff --git a/codex-rs/core/src/network_proxy_loader_tests.rs b/codex-rs/core/src/network_proxy_loader_tests.rs
index d6c8efc9ae..f7c7c2e452 100644
--- a/codex-rs/core/src/network_proxy_loader_tests.rs
+++ b/codex-rs/core/src/network_proxy_loader_tests.rs
@@ -5,6 +5,9 @@ use codex_config::ConfigLayerSource;
use codex_config::ConfigLayerStack;
use codex_config::ConfigRequirements;
use codex_config::ConfigRequirementsToml;
+use codex_config::RequirementSource;
+use codex_config::RequirementsExecPolicy;
+use codex_config::Sourced;
use codex_config::permissions_toml::NetworkDomainPermissionToml;
use codex_config::permissions_toml::NetworkDomainPermissionsToml;
use codex_execpolicy::Decision;
@@ -12,6 +15,8 @@ use codex_execpolicy::NetworkRuleProtocol;
use codex_execpolicy::Policy;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
+use std::fs;
+use tempfile::tempdir;
#[test]
fn higher_precedence_profile_network_overlays_domain_entries() {
@@ -225,6 +230,51 @@ fn execpolicy_network_rules_overlay_network_lists() {
);
}
+#[tokio::test]
+async fn malformed_custom_rules_preserve_managed_denied_domain() {
+ let temp_dir = tempdir().expect("create temp dir");
+ let policy_dir = temp_dir.path().join("rules");
+ fs::create_dir_all(&policy_dir).expect("create policy dir");
+ fs::write(policy_dir.join("broken.rules"), "prefix_rule(").expect("write malformed policy");
+
+ let mut requirements_exec_policy = Policy::empty();
+ requirements_exec_policy
+ .add_network_rule(
+ "blocked.example.com",
+ NetworkRuleProtocol::Https,
+ Decision::Forbidden,
+ /*justification*/ None,
+ )
+ .expect("managed network rule should be valid");
+ let requirements = ConfigRequirements {
+ exec_policy: Some(Sourced::new(
+ RequirementsExecPolicy::new(requirements_exec_policy),
+ RequirementSource::Unknown,
+ )),
+ ..ConfigRequirements::default()
+ };
+ let dot_codex_folder = AbsolutePathBuf::from_absolute_path(temp_dir.path())
+ .expect("dot codex folder should be absolute");
+ let layers = ConfigLayerStack::new(
+ vec![ConfigLayerEntry::new(
+ ConfigLayerSource::Project { dot_codex_folder },
+ toml::Value::Table(Default::default()),
+ )],
+ requirements,
+ ConfigRequirementsToml::default(),
+ )
+ .expect("layer stack should be valid");
+
+ let state = build_config_state_from_layers(&layers)
+ .await
+ .expect("proxy state should tolerate malformed custom rules");
+
+ assert_eq!(
+ state.config.network.denied_domains(),
+ Some(vec!["blocked.example.com".to_string()])
+ );
+}
+
#[test]
fn apply_network_constraints_includes_allow_all_unix_sockets_flag() {
let config: toml::Value = toml::from_str(
diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs
index ba8eacb395..91a21ef731 100644
--- a/codex-rs/core/src/session/mod.rs
+++ b/codex-rs/core/src/session/mod.rs
@@ -629,11 +629,14 @@ impl Codex {
developer_instructions: None,
},
};
- let service_tier = get_service_tier(
- config.service_tier.clone(),
- config.features.enabled(Feature::FastMode),
+ let fast_mode_enabled = config.features.enabled(Feature::FastMode);
+ let initial_service_tier_warning = unsupported_service_tier_warning(
+ config.service_tier.as_deref(),
+ fast_mode_enabled,
&model_info,
);
+ let service_tier =
+ get_service_tier(config.service_tier.clone(), fast_mode_enabled, &model_info);
let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(),
collaboration_mode,
@@ -706,6 +709,14 @@ impl Codex {
error!("Failed to create session: {e:#}");
map_session_init_error(&e, &config.codex_home)
})?;
+ if let Some(message) = initial_service_tier_warning {
+ session
+ .send_event_raw(Event {
+ id: INITIAL_SUBMIT_ID.to_owned(),
+ msg: EventMsg::Warning(WarningEvent { message }),
+ })
+ .await;
+ }
let thread_id = session.thread_id;
// This task will run until Op::Shutdown is received.
@@ -908,6 +919,22 @@ fn get_service_tier(
})
}
+fn unsupported_service_tier_warning(
+ configured_service_tier: Option<&str>,
+ fast_mode_enabled: bool,
+ model_info: &ModelInfo,
+) -> Option {
+ let service_tier = configured_service_tier.filter(|service_tier| {
+ fast_mode_enabled
+ && *service_tier != SERVICE_TIER_DEFAULT_REQUEST_VALUE
+ && !model_info.supports_service_tier(service_tier)
+ })?;
+ Some(format!(
+ "Configured service tier `{service_tier}` is not advertised as supported for model `{}` and will be omitted from requests.",
+ model_info.slug
+ ))
+}
+
fn session_permission_profile_state_from_config(
config: &Config,
) -> CodexResult {
diff --git a/codex-rs/core/tests/remote_env_windows/BUILD.bazel b/codex-rs/core/tests/remote_env_windows/BUILD.bazel
index 9b5ce52bd9..1f2465ffcb 100644
--- a/codex-rs/core/tests/remote_env_windows/BUILD.bazel
+++ b/codex-rs/core/tests/remote_env_windows/BUILD.bazel
@@ -6,15 +6,10 @@ wine_rust_test(
srcs = ["remote_env_windows_test.rs"],
crate_name = "remote_env_windows_test",
crate_root = "remote_env_windows_test.rs",
- host_binaries = {
- "codex-app-server": "//codex-rs/app-server:codex-app-server",
- },
windows_binaries = {
"wine-windows-exec-server": "//codex-rs/exec-server/testing:windows-exec-server",
},
deps = [
- "//codex-rs/app-server-protocol",
- "//codex-rs/app-server/tests/common",
"//codex-rs/core/tests/common",
"//codex-rs/exec-server",
"//codex-rs/exec-server/testing:wine-exec-server-test-support",
@@ -24,7 +19,6 @@ wine_rust_test(
"@crates//:anyhow",
"@crates//:pretty_assertions",
"@crates//:serde_json",
- "@crates//:tempfile",
"@crates//:tokio",
],
)
diff --git a/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs b/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs
index 04f8914982..ff80316061 100644
--- a/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs
+++ b/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs
@@ -2,20 +2,7 @@
use anyhow::Context;
use anyhow::Result;
-use app_test_support::PathBufExt;
-use app_test_support::TestAppServer;
-use app_test_support::create_mock_responses_server_repeating_assistant;
-use app_test_support::to_response;
-use app_test_support::write_mock_responses_config_toml;
-use codex_app_server_protocol::RequestId;
-use codex_app_server_protocol::ThreadStartParams;
-use codex_app_server_protocol::ThreadStartResponse;
-use codex_app_server_protocol::TurnEnvironmentParams;
-use codex_app_server_protocol::TurnStartParams;
-use codex_app_server_protocol::TurnStartResponse;
-use codex_app_server_protocol::UserInput as V2UserInput;
use codex_exec_server::REMOTE_ENVIRONMENT_ID;
-use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
use codex_features::Feature;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
@@ -35,20 +22,11 @@ use core_test_support::responses::start_mock_server;
use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields;
use core_test_support::wait_for_event;
-use codex_utils_path_uri::LegacyAppPathString;
-use codex_utils_path_uri::PathConvention;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
-use serde_json::Value;
use serde_json::json;
-use std::collections::BTreeMap;
-use std::fs;
-use tempfile::TempDir;
-use tokio::time::timeout;
use wine_exec_server_test_support::WineExecServer;
-const APP_SERVER_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
const CALL_ID: &str = "wine-cmd-smoke";
@@ -244,152 +222,3 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
})
.await
}
-
-#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn app_server_starts_thread_with_windows_environment_native_cwd() -> Result<()> {
- const AGENTS_INSTRUCTIONS: &str = "remote Windows workspace instructions";
- const NATIVE_CWD: &str = r"C:\windows";
-
- WineExecServer
- .scope(|exec_server_url, wine_prefix| async move {
- let agents_path = PathUri::parse("file:///C:/windows/AGENTS.md")?;
- fs::write(
- wine_prefix.join("drive_c").join("windows").join("AGENTS.md"),
- AGENTS_INSTRUCTIONS,
- )?;
-
- let codex_home = TempDir::new()?;
- let server = create_mock_responses_server_repeating_assistant("done").await;
- write_mock_responses_config_toml(
- codex_home.path(),
- &server.uri(),
- &BTreeMap::new(),
- 100_000,
- /*requires_openai_auth*/ None,
- "mock",
- "compact",
- )?;
- let mut app_server = TestAppServer::new_with_env(
- codex_home.path(),
- &[(
- CODEX_EXEC_SERVER_URL_ENV_VAR,
- Some(exec_server_url.as_str()),
- )],
- )
- .await?;
- timeout(APP_SERVER_READ_TIMEOUT, app_server.initialize()).await??;
-
- let request_id = app_server
- .send_thread_start_request(ThreadStartParams {
- environments: Some(vec![TurnEnvironmentParams {
- environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
- cwd: serde_json::from_value::(json!(NATIVE_CWD))?,
- }]),
- ..Default::default()
- })
- .await?;
- let response = timeout(
- APP_SERVER_READ_TIMEOUT,
- app_server.read_stream_until_response_message(RequestId::Integer(request_id)),
- )
- .await??;
- let response: ThreadStartResponse = to_response(response)?;
- assert!(!response.thread.id.is_empty());
- let host_cwd = codex_home.path().to_path_buf().abs();
- // TODO(anp): Return the selected environment's native cwd from thread/start.
- assert_eq!(response.cwd, host_cwd);
- // TODO(anp): Derive runtime workspace roots from the selected remote environment.
- assert_eq!(response.runtime_workspace_roots, vec![host_cwd]);
- assert_eq!(
- response.instruction_sources,
- vec![LegacyAppPathString::from_path_uri(
- &agents_path,
- PathConvention::Windows,
- )?]
- );
- // TODO(anp): Report the implicit built-in permission profile instead of None.
- assert_eq!(response.active_permission_profile, None);
-
- let turn_request_id = app_server
- .send_turn_start_request(TurnStartParams {
- thread_id: response.thread.id,
- client_user_message_id: None,
- input: vec![V2UserInput::Text {
- text: "say done".to_string(),
- text_elements: Vec::new(),
- }],
- ..Default::default()
- })
- .await?;
- let turn_response = timeout(
- APP_SERVER_READ_TIMEOUT,
- app_server.read_stream_until_response_message(RequestId::Integer(turn_request_id)),
- )
- .await??;
- let _: TurnStartResponse = to_response(turn_response)?;
- timeout(
- APP_SERVER_READ_TIMEOUT,
- app_server.read_stream_until_notification_message("turn/completed"),
- )
- .await??;
-
- let requests = server
- .received_requests()
- .await
- .context("failed to fetch received requests")?;
- let first_request = requests
- .iter()
- .find(|request| request.url.path().ends_with("/responses"))
- .context("turn should send a Responses request")?;
- let body = first_request.body_json::()?;
- let remote_instructions = body["input"]
- .as_array()
- .into_iter()
- .flatten()
- .filter(|item| item.get("role").and_then(Value::as_str) == Some("user"))
- .filter_map(|item| item.get("content").and_then(Value::as_array))
- .flatten()
- .filter_map(|content| content.get("text").and_then(Value::as_str))
- .find(|text| text.contains(AGENTS_INSTRUCTIONS))
- .context("remote workspace instructions should be model visible")?;
- assert!(remote_instructions.contains(r"# AGENTS.md instructions for C:\windows"));
- let environment_context = body["input"]
- .as_array()
- .into_iter()
- .flatten()
- .filter(|item| item.get("role").and_then(Value::as_str) == Some("user"))
- .filter_map(|item| item.get("content").and_then(Value::as_array))
- .flatten()
- .filter_map(|content| content.get("text").and_then(Value::as_str))
- .find(|text| text.starts_with(""))
- .context("environment context should be model visible")?;
- // The model should see the remote environment's shell, not the Linux app-server's
- // host shell.
- assert_eq!(
- environment_context
- .lines()
- .find(|line| line.trim_start().starts_with(""))
- .map(str::trim),
- Some("powershell"),
- );
- // The model should see cwd using the remote environment's native path convention, not
- // the Linux app-server's host path convention.
- assert_eq!(
- environment_context
- .lines()
- .find(|line| line.trim_start().starts_with(""))
- .map(str::trim),
- Some(r"C:\windows"),
- );
- let host_workspace_roots = format!(
- "{}",
- codex_home.path().display()
- );
- // TODO(anp): Derive model-visible workspace roots from the selected remote environment
- // and render them using its native path convention.
- assert!(environment_context.contains(&host_workspace_roots));
-
- Ok(())
- })
- .await
-}
diff --git a/codex-rs/core/tests/suite/exec_policy.rs b/codex-rs/core/tests/suite/exec_policy.rs
index 4e64c66b0a..0c73b74ab2 100644
--- a/codex-rs/core/tests/suite/exec_policy.rs
+++ b/codex-rs/core/tests/suite/exec_policy.rs
@@ -1,6 +1,7 @@
#![allow(clippy::unwrap_used)]
use anyhow::Result;
+use codex_config::test_support::CloudConfigBundleFixture;
use codex_features::Feature;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
@@ -17,6 +18,7 @@ use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_sse_once;
use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server;
+use core_test_support::skip_if_target_windows;
use core_test_support::test_codex::local_selections;
use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields;
@@ -255,6 +257,84 @@ async fn execpolicy_blocks_shell_invocation() -> Result<()> {
Ok(())
}
+#[tokio::test]
+async fn malformed_custom_rules_preserve_managed_forbidden_prefix() -> Result<()> {
+ skip_if_target_windows!(
+ Ok(()),
+ "managed prefix fixture uses POSIX executable semantics"
+ );
+
+ let mut builder = test_codex()
+ .with_cloud_config_bundle(
+ CloudConfigBundleFixture::loader_with_enterprise_requirement(
+ r#"
+[rules]
+prefix_rules = [
+ { pattern = [{ token = "echo" }], decision = "forbidden" },
+]
+"#,
+ ),
+ )
+ .with_config(|config| {
+ config
+ .features
+ .enable(Feature::UnifiedExec)
+ .expect("test config should allow feature update");
+ let policy_path = config.codex_home.join("rules").join("broken.rules");
+ fs::create_dir_all(
+ policy_path
+ .parent()
+ .expect("policy directory must have a parent"),
+ )
+ .expect("create policy directory");
+ fs::write(policy_path, "prefix_rule(").expect("write malformed policy file");
+ });
+ let server = start_mock_server().await;
+ let test = builder.build_with_auto_env(&server).await?;
+ let call_id = "managed-shell-forbidden";
+ let args = json!({
+ "cmd": "echo blocked",
+ "yield_time_ms": 1_000,
+ });
+
+ mount_sse_once(
+ &server,
+ sse(vec![
+ ev_response_created("resp-managed-1"),
+ ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?),
+ ev_completed("resp-managed-1"),
+ ]),
+ )
+ .await;
+ let results_mock = mount_sse_once(
+ &server,
+ sse(vec![
+ ev_assistant_message("msg-managed-1", "done"),
+ ev_completed("resp-managed-2"),
+ ]),
+ )
+ .await;
+
+ test.submit_turn_with_approval_and_permission_profile(
+ "run shell command",
+ AskForApproval::Never,
+ PermissionProfile::Disabled,
+ )
+ .await?;
+
+ let output_item = results_mock.single_request().function_call_output(call_id);
+ let output = output_item
+ .get("output")
+ .and_then(Value::as_str)
+ .expect("function call output should include a string output payload");
+ assert!(
+ output.contains("policy forbids commands starting with `echo`"),
+ "unexpected output: {output}"
+ );
+
+ Ok(())
+}
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn shell_command_empty_script_with_collaboration_mode_does_not_panic() -> Result<()> {
let server = start_mock_server().await;
diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs
index b08ca7cc8a..9ef3675b3e 100644
--- a/codex-rs/core/tests/suite/model_switching.rs
+++ b/codex-rs/core/tests/suite/model_switching.rs
@@ -387,6 +387,46 @@ async fn unsupported_service_tier_is_omitted_from_http_turn() -> Result<()> {
Ok(())
}
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn unsupported_configured_service_tier_warns_at_session_start() -> Result<()> {
+ skip_if_no_network!(Ok(()));
+
+ let server = start_mock_server().await;
+ let model_slug = "test-no-tier-model";
+ let model = test_model_info(
+ model_slug,
+ model_slug,
+ "no service tiers",
+ default_input_modalities(),
+ );
+ let mut builder = test_codex()
+ .with_model(model_slug)
+ .with_config(move |config| {
+ config.service_tier = Some(ServiceTier::Flex.request_value().to_string());
+ config.model_catalog = Some(ModelsResponse {
+ models: vec![model],
+ });
+ });
+ let test = builder.build(&server).await?;
+
+ let warning = wait_for_event(&test.codex, |event| {
+ matches!(
+ event,
+ EventMsg::Warning(warning)
+ if warning.message.contains("will be omitted from requests")
+ )
+ })
+ .await;
+ let EventMsg::Warning(warning) = warning else {
+ unreachable!("wait_for_event matched a warning")
+ };
+ assert_eq!(
+ warning.message,
+ "Configured service tier `flex` is not advertised as supported for model `test-no-tier-model` and will be omitted from requests."
+ );
+ Ok(())
+}
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn default_service_tier_override_is_omitted_from_http_turn() -> Result<()> {
skip_if_no_network!(Ok(()));
diff --git a/codex-rs/exec-server/Cargo.toml b/codex-rs/exec-server/Cargo.toml
index 35fa411021..ba9058f0c6 100644
--- a/codex-rs/exec-server/Cargo.toml
+++ b/codex-rs/exec-server/Cargo.toml
@@ -17,7 +17,7 @@ base64 = { workspace = true }
bytes = { workspace = true }
clatter = { workspace = true }
codex-api = { workspace = true }
-codex-client = { workspace = true }
+codex-http-client = { workspace = true }
codex-exec-server-protocol = { workspace = true }
codex-file-system = { workspace = true }
codex-network-proxy = { workspace = true }
diff --git a/codex-rs/exec-server/src/client/reqwest_http_client.rs b/codex-rs/exec-server/src/client/reqwest_http_client.rs
index 7ead01c9ba..d0cffa6a32 100644
--- a/codex-rs/exec-server/src/client/reqwest_http_client.rs
+++ b/codex-rs/exec-server/src/client/reqwest_http_client.rs
@@ -8,9 +8,9 @@
use std::error::Error as StdError;
use std::time::Duration;
-use codex_client::build_reqwest_client_with_custom_ca;
-use codex_client::with_chatgpt_cloudflare_cookie_store;
use codex_exec_server_protocol::JSONRPCErrorError;
+use codex_http_client::build_reqwest_client_with_custom_ca;
+use codex_http_client::with_chatgpt_cloudflare_cookie_store;
use futures::FutureExt;
use futures::StreamExt;
use futures::future::BoxFuture;
diff --git a/codex-rs/http-client/BUILD.bazel b/codex-rs/http-client/BUILD.bazel
new file mode 100644
index 0000000000..6092c05bf2
--- /dev/null
+++ b/codex-rs/http-client/BUILD.bazel
@@ -0,0 +1,7 @@
+load("//:defs.bzl", "codex_rust_crate")
+
+codex_rust_crate(
+ name = "http-client",
+ compile_data = glob(["tests/fixtures/**"]),
+ crate_name = "codex_http_client",
+)
diff --git a/codex-rs/http-client/Cargo.toml b/codex-rs/http-client/Cargo.toml
new file mode 100644
index 0000000000..46afa51ba5
--- /dev/null
+++ b/codex-rs/http-client/Cargo.toml
@@ -0,0 +1,49 @@
+[package]
+edition.workspace = true
+license.workspace = true
+name = "codex-http-client"
+version.workspace = true
+
+[dependencies]
+bytes = { workspace = true }
+codex-utils-rustls-provider = { workspace = true }
+futures = { workspace = true }
+http = { workspace = true }
+opentelemetry = { workspace = true }
+reqwest = { workspace = true, features = ["json", "rustls-tls-native-roots", "stream"] }
+rustls = { workspace = true }
+rustls-native-certs = { workspace = true }
+rustls-pki-types = { workspace = true }
+serde = { workspace = true, features = ["derive"] }
+serde_json = { workspace = true }
+thiserror = { workspace = true }
+tokio = { workspace = true, features = ["macros", "rt", "time", "sync"] }
+tracing = { workspace = true }
+tracing-opentelemetry = { workspace = true }
+zstd = { workspace = true }
+
+[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
+sha2 = { workspace = true }
+
+[target.'cfg(target_os = "macos")'.dependencies]
+system-configuration = { workspace = true }
+
+[target.'cfg(target_os = "windows")'.dependencies]
+windows-sys = { version = "0.52", features = [
+ "Win32_Foundation",
+ "Win32_Networking_WinHttp",
+] }
+
+[lints]
+workspace = true
+
+[dev-dependencies]
+codex-utils-cargo-bin = { workspace = true }
+opentelemetry_sdk = { workspace = true }
+pretty_assertions = { workspace = true }
+rcgen = { workspace = true }
+tempfile = { workspace = true }
+tracing-subscriber = { workspace = true }
+
+[lib]
+doctest = false
diff --git a/codex-rs/http-client/README.md b/codex-rs/http-client/README.md
new file mode 100644
index 0000000000..89e35ac59d
--- /dev/null
+++ b/codex-rs/http-client/README.md
@@ -0,0 +1,9 @@
+# codex-http-client
+
+Low-level HTTP transport shared by Codex crates.
+
+- Defines the request, response, streaming, and transport types used for outbound HTTP calls.
+- Owns the `reqwest` implementation, custom CA handling, and ChatGPT Cloudflare cookie policy.
+- Resolves system, PAC/WPAD, environment, and direct proxy routes for supported clients.
+
+Higher-level retry, SSE, and request telemetry policy remains in `codex-client`.
diff --git a/codex-rs/codex-client/src/bin/custom_ca_probe.rs b/codex-rs/http-client/src/bin/custom_ca_probe.rs
similarity index 91%
rename from codex-rs/codex-client/src/bin/custom_ca_probe.rs
rename to codex-rs/http-client/src/bin/custom_ca_probe.rs
index 81f5ba9bc2..c8db21510d 100644
--- a/codex-rs/codex-client/src/bin/custom_ca_probe.rs
+++ b/codex-rs/http-client/src/bin/custom_ca_probe.rs
@@ -10,9 +10,9 @@
//! - error messages guide users when CA files are invalid.
//! - optional HTTPS probes can complete a request through the constructed client.
//!
-//! The detailed explanation of what "hermetic" means here lives in `codex_client::custom_ca`.
+//! The detailed explanation of what "hermetic" means here lives in `codex_http_client::custom_ca`.
//! This binary exists so the tests can exercise
-//! [`codex_client::build_reqwest_client_for_subprocess_tests`] in a separate process without
+//! [`codex_http_client::build_reqwest_client_for_subprocess_tests`] in a separate process without
//! duplicating client-construction logic.
use std::env;
@@ -69,11 +69,11 @@ fn build_probe_client(
if let Some(proxy_url) = proxy_url {
let proxy = reqwest::Proxy::https(proxy_url)
.map_err(|error| format!("failed to configure probe proxy {proxy_url}: {error}"))?;
- return codex_client::build_reqwest_client_with_custom_ca(builder.proxy(proxy))
+ return codex_http_client::build_reqwest_client_with_custom_ca(builder.proxy(proxy))
.map_err(|error| error.to_string());
}
- codex_client::build_reqwest_client_for_subprocess_tests(builder)
+ codex_http_client::build_reqwest_client_for_subprocess_tests(builder)
.map_err(|error| error.to_string())
}
diff --git a/codex-rs/codex-client/src/chatgpt_cloudflare_cookies.rs b/codex-rs/http-client/src/chatgpt_cloudflare_cookies.rs
similarity index 98%
rename from codex-rs/codex-client/src/chatgpt_cloudflare_cookies.rs
rename to codex-rs/http-client/src/chatgpt_cloudflare_cookies.rs
index 7e5d22057c..f63763dad4 100644
--- a/codex-rs/codex-client/src/chatgpt_cloudflare_cookies.rs
+++ b/codex-rs/http-client/src/chatgpt_cloudflare_cookies.rs
@@ -7,7 +7,7 @@ use reqwest::header::HeaderValue;
use crate::chatgpt_hosts::is_allowed_chatgpt_host;
-// WARNING: this store is process-global and may be shared across auth contexts.
+// WARNING: this HTTP cookie store is process-global and may be shared across auth contexts.
// It must only ever contain Cloudflare infrastructure cookies. Never extend this
// store to persist ChatGPT account, session, auth, or other user-specific cookie
// data.
diff --git a/codex-rs/codex-client/src/chatgpt_hosts.rs b/codex-rs/http-client/src/chatgpt_hosts.rs
similarity index 96%
rename from codex-rs/codex-client/src/chatgpt_hosts.rs
rename to codex-rs/http-client/src/chatgpt_hosts.rs
index dd0b99589c..0426c8bb36 100644
--- a/codex-rs/codex-client/src/chatgpt_hosts.rs
+++ b/codex-rs/http-client/src/chatgpt_hosts.rs
@@ -1,5 +1,5 @@
/// Returns whether `host` is one of the ChatGPT hosts Codex is allowed to treat
-/// as first-party ChatGPT traffic.
+/// as first-party ChatGPT HTTP traffic.
pub fn is_allowed_chatgpt_host(host: &str) -> bool {
const EXACT_HOSTS: &[&str] = &["chatgpt.com", "chat.openai.com", "chatgpt-staging.com"];
const SUBDOMAIN_SUFFIXES: &[&str] = &[".chatgpt.com", ".chatgpt-staging.com"];
diff --git a/codex-rs/codex-client/src/custom_ca.rs b/codex-rs/http-client/src/custom_ca.rs
similarity index 99%
rename from codex-rs/codex-client/src/custom_ca.rs
rename to codex-rs/http-client/src/custom_ca.rs
index 1a211beedf..4cfae8f23d 100644
--- a/codex-rs/codex-client/src/custom_ca.rs
+++ b/codex-rs/http-client/src/custom_ca.rs
@@ -1,4 +1,4 @@
-//! Custom CA handling for Codex outbound HTTP and websocket clients.
+//! Custom CA handling shared by Codex outbound HTTP and websocket clients.
//!
//! Codex constructs outbound reqwest clients and secure websocket connections in a few crates, but
//! they all need the same trust-store policy when enterprise proxies or gateways intercept TLS.
diff --git a/codex-rs/codex-client/src/default_client.rs b/codex-rs/http-client/src/default_client.rs
similarity index 93%
rename from codex-rs/codex-client/src/default_client.rs
rename to codex-rs/http-client/src/default_client.rs
index 56b3ce4b16..b4465e7f8c 100644
--- a/codex-rs/codex-client/src/default_client.rs
+++ b/codex-rs/http-client/src/default_client.rs
@@ -14,47 +14,47 @@ use tracing::Span;
use tracing_opentelemetry::OpenTelemetrySpanExt;
#[derive(Clone, Debug)]
-pub struct CodexHttpClient {
+pub struct HttpClient {
inner: reqwest::Client,
}
-impl CodexHttpClient {
+impl HttpClient {
pub fn new(inner: reqwest::Client) -> Self {
Self { inner }
}
- pub fn get(&self, url: U) -> CodexRequestBuilder
+ pub fn get(&self, url: U) -> RequestBuilder
where
U: IntoUrl,
{
self.request(Method::GET, url)
}
- pub fn post(&self, url: U) -> CodexRequestBuilder
+ pub fn post(&self, url: U) -> RequestBuilder
where
U: IntoUrl,
{
self.request(Method::POST, url)
}
- pub fn request(&self, method: Method, url: U) -> CodexRequestBuilder
+ pub fn request(&self, method: Method, url: U) -> RequestBuilder
where
U: IntoUrl,
{
let url_str = url.as_str().to_string();
- CodexRequestBuilder::new(self.inner.request(method.clone(), url), method, url_str)
+ RequestBuilder::new(self.inner.request(method.clone(), url), method, url_str)
}
}
#[must_use = "requests are not sent unless `send` is awaited"]
#[derive(Debug)]
-pub struct CodexRequestBuilder {
+pub struct RequestBuilder {
builder: reqwest::RequestBuilder,
method: Method,
url: String,
}
-impl CodexRequestBuilder {
+impl RequestBuilder {
fn new(builder: reqwest::RequestBuilder, method: Method, url: String) -> Self {
Self {
builder,
diff --git a/codex-rs/codex-client/src/error.rs b/codex-rs/http-client/src/error.rs
similarity index 91%
rename from codex-rs/codex-client/src/error.rs
rename to codex-rs/http-client/src/error.rs
index fa2bfb4f79..6b49229026 100644
--- a/codex-rs/codex-client/src/error.rs
+++ b/codex-rs/http-client/src/error.rs
@@ -1,3 +1,5 @@
+//! Errors returned by the shared Codex HTTP transport.
+
use http::HeaderMap;
use http::StatusCode;
use thiserror::Error;
diff --git a/codex-rs/http-client/src/lib.rs b/codex-rs/http-client/src/lib.rs
new file mode 100644
index 0000000000..5f90435452
--- /dev/null
+++ b/codex-rs/http-client/src/lib.rs
@@ -0,0 +1,40 @@
+mod chatgpt_cloudflare_cookies;
+mod chatgpt_hosts;
+mod custom_ca;
+mod default_client;
+mod error;
+mod outbound_proxy;
+mod request;
+mod transport;
+
+pub use crate::chatgpt_cloudflare_cookies::with_chatgpt_cloudflare_cookie_store;
+pub use crate::chatgpt_hosts::is_allowed_chatgpt_host;
+pub use crate::custom_ca::BuildCustomCaTransportError;
+/// Test-only subprocess hook for custom CA coverage.
+///
+/// This stays public only so the `custom_ca_probe` binary target can reuse the shared helper. It
+/// is hidden from normal docs because ordinary callers should use
+/// [`build_reqwest_client_with_custom_ca`] instead.
+#[doc(hidden)]
+pub use crate::custom_ca::build_reqwest_client_for_subprocess_tests;
+pub use crate::custom_ca::build_reqwest_client_with_custom_ca;
+pub use crate::custom_ca::maybe_build_rustls_client_config_with_custom_ca;
+pub use crate::default_client::HttpClient;
+pub use crate::default_client::RequestBuilder;
+pub use crate::error::StreamError;
+pub use crate::error::TransportError;
+pub use crate::outbound_proxy::BuildRouteAwareHttpClientError;
+pub use crate::outbound_proxy::ClientRouteClass;
+pub use crate::outbound_proxy::OutboundProxyConfig;
+pub use crate::outbound_proxy::RouteFailureClass;
+pub use crate::outbound_proxy::build_reqwest_client_for_route;
+pub use crate::request::EncodedJsonBody;
+pub use crate::request::PreparedRequestBody;
+pub use crate::request::Request;
+pub use crate::request::RequestBody;
+pub use crate::request::RequestCompression;
+pub use crate::request::Response;
+pub use crate::transport::ByteStream;
+pub use crate::transport::HttpTransport;
+pub use crate::transport::ReqwestTransport;
+pub use crate::transport::StreamResponse;
diff --git a/codex-rs/codex-client/src/outbound_proxy.rs b/codex-rs/http-client/src/outbound_proxy.rs
similarity index 99%
rename from codex-rs/codex-client/src/outbound_proxy.rs
rename to codex-rs/http-client/src/outbound_proxy.rs
index ea6587052e..e643698a8c 100644
--- a/codex-rs/codex-client/src/outbound_proxy.rs
+++ b/codex-rs/http-client/src/outbound_proxy.rs
@@ -1,4 +1,4 @@
-//! Conservative outbound proxy selection for resolver-aware clients.
+//! Conservative outbound proxy selection for resolver-aware HTTP clients.
//!
//! When enabled, platform system discovery is tried first, explicit environment
//! proxies are the fallback, and the final fallback is a direct connection.
diff --git a/codex-rs/codex-client/src/outbound_proxy/macos.rs b/codex-rs/http-client/src/outbound_proxy/macos.rs
similarity index 99%
rename from codex-rs/codex-client/src/outbound_proxy/macos.rs
rename to codex-rs/http-client/src/outbound_proxy/macos.rs
index 3913ecc40b..fe07a6b498 100644
--- a/codex-rs/codex-client/src/outbound_proxy/macos.rs
+++ b/codex-rs/http-client/src/outbound_proxy/macos.rs
@@ -1,3 +1,5 @@
+//! macOS system proxy resolution through SystemConfiguration and CFNetwork.
+
use std::ffi::c_void;
use std::ptr;
use std::time::Duration;
diff --git a/codex-rs/codex-client/src/outbound_proxy/windows.rs b/codex-rs/http-client/src/outbound_proxy/windows.rs
similarity index 99%
rename from codex-rs/codex-client/src/outbound_proxy/windows.rs
rename to codex-rs/http-client/src/outbound_proxy/windows.rs
index 9e64196705..4ff927723a 100644
--- a/codex-rs/codex-client/src/outbound_proxy/windows.rs
+++ b/codex-rs/http-client/src/outbound_proxy/windows.rs
@@ -1,3 +1,5 @@
+//! Windows system proxy resolution through WinHTTP.
+
use std::ffi::c_void;
use std::ptr;
diff --git a/codex-rs/codex-client/src/outbound_proxy/windows_tests.rs b/codex-rs/http-client/src/outbound_proxy/windows_tests.rs
similarity index 93%
rename from codex-rs/codex-client/src/outbound_proxy/windows_tests.rs
rename to codex-rs/http-client/src/outbound_proxy/windows_tests.rs
index 3772c057db..589c06661c 100644
--- a/codex-rs/codex-client/src/outbound_proxy/windows_tests.rs
+++ b/codex-rs/http-client/src/outbound_proxy/windows_tests.rs
@@ -1,3 +1,5 @@
+//! Windows proxy parsing tests.
+
use super::*;
#[test]
diff --git a/codex-rs/codex-client/src/outbound_proxy_tests.rs b/codex-rs/http-client/src/outbound_proxy_tests.rs
similarity index 99%
rename from codex-rs/codex-client/src/outbound_proxy_tests.rs
rename to codex-rs/http-client/src/outbound_proxy_tests.rs
index 46868e8f73..60a1d56396 100644
--- a/codex-rs/codex-client/src/outbound_proxy_tests.rs
+++ b/codex-rs/http-client/src/outbound_proxy_tests.rs
@@ -1,3 +1,5 @@
+//! Shared outbound proxy policy tests.
+
use super::*;
use pretty_assertions::assert_eq;
use std::io::Read;
diff --git a/codex-rs/codex-client/src/request.rs b/codex-rs/http-client/src/request.rs
similarity index 98%
rename from codex-rs/codex-client/src/request.rs
rename to codex-rs/http-client/src/request.rs
index 6d50f7cb57..7608860c3d 100644
--- a/codex-rs/codex-client/src/request.rs
+++ b/codex-rs/http-client/src/request.rs
@@ -1,7 +1,7 @@
use bytes::Bytes;
+use http::HeaderMap;
+use http::HeaderValue;
use http::Method;
-use reqwest::header::HeaderMap;
-use reqwest::header::HeaderValue;
use serde::Serialize;
use serde_json::Value;
use std::time::Duration;
@@ -121,7 +121,7 @@ impl Request {
Some(RequestBody::Json(_) | RequestBody::EncodedJson(_))
);
let trace_bytes = if self.compression != RequestCompression::None
- && tracing::enabled!(target: "codex_client::transport", tracing::Level::TRACE)
+ && tracing::enabled!(target: "codex_http_client::transport", tracing::Level::TRACE)
{
match self.body.as_ref() {
Some(RequestBody::Json(body)) => Some(Bytes::from(
diff --git a/codex-rs/codex-client/src/transport.rs b/codex-rs/http-client/src/transport.rs
similarity index 94%
rename from codex-rs/codex-client/src/transport.rs
rename to codex-rs/http-client/src/transport.rs
index 708b73bf2b..bda6c28ee9 100644
--- a/codex-rs/codex-client/src/transport.rs
+++ b/codex-rs/http-client/src/transport.rs
@@ -1,5 +1,5 @@
-use crate::default_client::CodexHttpClient;
-use crate::default_client::CodexRequestBuilder;
+use crate::default_client::HttpClient;
+use crate::default_client::RequestBuilder;
use crate::error::TransportError;
use crate::request::Request;
use crate::request::RequestBody;
@@ -35,17 +35,17 @@ pub trait HttpTransport: Send + Sync {
#[derive(Clone, Debug)]
pub struct ReqwestTransport {
- client: CodexHttpClient,
+ client: HttpClient,
}
impl ReqwestTransport {
pub fn new(client: reqwest::Client) -> Self {
Self {
- client: CodexHttpClient::new(client),
+ client: HttpClient::new(client),
}
}
- fn build(&self, req: Request) -> Result {
+ fn build(&self, req: Request) -> Result {
let prepared = req.prepare_body_for_send().map_err(TransportError::Build)?;
let Request {
diff --git a/codex-rs/codex-client/tests/ca_env.rs b/codex-rs/http-client/tests/ca_env.rs
similarity index 99%
rename from codex-rs/codex-client/tests/ca_env.rs
rename to codex-rs/http-client/tests/ca_env.rs
index 7ddd138a33..f35225fee7 100644
--- a/codex-rs/codex-client/tests/ca_env.rs
+++ b/codex-rs/http-client/tests/ca_env.rs
@@ -3,7 +3,7 @@
//!
//! These tests intentionally run through `custom_ca_probe` and
//! `build_reqwest_client_for_subprocess_tests` instead of calling the helper in-process. The
-//! detailed explanation of what "hermetic" means here lives in `codex_client::custom_ca`; these
+//! detailed explanation of what "hermetic" means here lives in `codex_http_client::custom_ca`; these
//! tests add the process-level half of that contract by scrubbing inherited CA environment
//! variables before each subprocess launch. Most assertions here cover CA file selection, PEM
//! parsing, and user-facing errors. The HTTPS probes go further and perform real POSTs against
diff --git a/codex-rs/codex-client/tests/fixtures/test-ca-trusted.pem b/codex-rs/http-client/tests/fixtures/test-ca-trusted.pem
similarity index 100%
rename from codex-rs/codex-client/tests/fixtures/test-ca-trusted.pem
rename to codex-rs/http-client/tests/fixtures/test-ca-trusted.pem
diff --git a/codex-rs/codex-client/tests/fixtures/test-ca.pem b/codex-rs/http-client/tests/fixtures/test-ca.pem
similarity index 100%
rename from codex-rs/codex-client/tests/fixtures/test-ca.pem
rename to codex-rs/http-client/tests/fixtures/test-ca.pem
diff --git a/codex-rs/codex-client/tests/fixtures/test-intermediate.pem b/codex-rs/http-client/tests/fixtures/test-intermediate.pem
similarity index 100%
rename from codex-rs/codex-client/tests/fixtures/test-intermediate.pem
rename to codex-rs/http-client/tests/fixtures/test-intermediate.pem
diff --git a/codex-rs/login/Cargo.toml b/codex-rs/login/Cargo.toml
index 04066f5894..3eb253d7dc 100644
--- a/codex-rs/login/Cargo.toml
+++ b/codex-rs/login/Cargo.toml
@@ -11,7 +11,7 @@ workspace = true
base64 = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
codex-agent-identity = { workspace = true }
-codex-client = { workspace = true }
+codex-http-client = { workspace = true }
codex-config = { workspace = true }
codex-keyring-store = { workspace = true }
codex-model-provider-info = { workspace = true }
diff --git a/codex-rs/login/src/auth/default_client.rs b/codex-rs/login/src/auth/default_client.rs
index f72727bfca..1146a89d96 100644
--- a/codex-rs/login/src/auth/default_client.rs
+++ b/codex-rs/login/src/auth/default_client.rs
@@ -1,17 +1,17 @@
//! Default Codex HTTP client: shared `User-Agent`, `originator`, optional residency header, and
-//! reqwest/`CodexHttpClient` construction.
+//! reqwest/`HttpClient` construction.
//!
//! Use [`crate::default_client`] or [`codex_login::default_client`] from other crates in this
//! workspace.
-use codex_client::BuildCustomCaTransportError;
-use codex_client::BuildRouteAwareHttpClientError;
-use codex_client::ClientRouteClass;
-use codex_client::CodexHttpClient;
-pub use codex_client::CodexRequestBuilder;
-use codex_client::build_reqwest_client_for_route;
-use codex_client::build_reqwest_client_with_custom_ca;
-use codex_client::with_chatgpt_cloudflare_cookie_store;
+use codex_http_client::BuildCustomCaTransportError;
+use codex_http_client::BuildRouteAwareHttpClientError;
+use codex_http_client::ClientRouteClass;
+use codex_http_client::HttpClient;
+pub use codex_http_client::RequestBuilder as CodexRequestBuilder;
+use codex_http_client::build_reqwest_client_for_route;
+use codex_http_client::build_reqwest_client_with_custom_ca;
+use codex_http_client::with_chatgpt_cloudflare_cookie_store;
use codex_terminal_detection::user_agent;
use reqwest::header::HeaderMap;
use reqwest::header::HeaderValue;
@@ -197,9 +197,9 @@ fn sanitize_user_agent(candidate: String, fallback: &str) -> String {
///
/// This supported default path preserves reqwest's existing proxy behavior and does not opt into
/// Codex's route-aware system/PAC resolution.
-pub fn create_client() -> CodexHttpClient {
+pub fn create_client() -> HttpClient {
let inner = build_reqwest_client();
- CodexHttpClient::new(inner)
+ HttpClient::new(inner)
}
/// Builds the default reqwest client used for ordinary Codex HTTP traffic.
@@ -282,8 +282,8 @@ pub(crate) fn build_default_auth_reqwest_client(
pub(crate) fn create_default_auth_client(
endpoint: &str,
auth_route_config: Option<&AuthRouteConfig>,
-) -> Result {
- build_default_auth_reqwest_client(endpoint, auth_route_config).map(CodexHttpClient::new)
+) -> Result {
+ build_default_auth_reqwest_client(endpoint, auth_route_config).map(HttpClient::new)
}
pub fn default_headers() -> HeaderMap {
diff --git a/codex-rs/login/src/auth/manager.rs b/codex-rs/login/src/auth/manager.rs
index 844410874d..a2d005dfa6 100644
--- a/codex-rs/login/src/auth/manager.rs
+++ b/codex-rs/login/src/auth/manager.rs
@@ -55,8 +55,8 @@ use crate::outbound_proxy::AuthRouteConfig;
use crate::token_data::TokenData;
use crate::token_data::parse_chatgpt_jwt_claims;
use crate::token_data::parse_jwt_expiration;
-use codex_client::CodexHttpClient;
use codex_config::types::AuthCredentialsStoreMode;
+use codex_http_client::HttpClient;
use codex_protocol::account::PlanType as AccountPlanType;
use codex_protocol::auth::PlanType as InternalPlanType;
use codex_protocol::auth::RefreshTokenFailedError;
@@ -171,7 +171,7 @@ pub struct ChatgptAuthTokens {
#[derive(Debug, Clone)]
struct ChatgptAuthState {
auth_dot_json: Arc>>,
- client: CodexHttpClient,
+ client: HttpClient,
}
const TOKEN_REFRESH_INTERVAL: i64 = 8;
@@ -805,7 +805,7 @@ impl ChatgptAuth {
&self.storage
}
- fn client(&self) -> &CodexHttpClient {
+ fn client(&self) -> &HttpClient {
&self.state.client
}
@@ -1331,7 +1331,7 @@ fn persist_tokens(
// The caller is responsible for persisting any returned tokens.
async fn request_chatgpt_token_refresh(
refresh_token: String,
- client: &CodexHttpClient,
+ client: &HttpClient,
) -> Result {
let refresh_request = RefreshRequest {
client_id: oauth_client_id(),
diff --git a/codex-rs/login/src/auth/personal_access_token.rs b/codex-rs/login/src/auth/personal_access_token.rs
index ea75648d1b..09c6d5d2b3 100644
--- a/codex-rs/login/src/auth/personal_access_token.rs
+++ b/codex-rs/login/src/auth/personal_access_token.rs
@@ -1,4 +1,4 @@
-use codex_client::CodexHttpClient;
+use codex_http_client::HttpClient;
use codex_protocol::account::PlanType as AccountPlanType;
use codex_protocol::auth::PlanType as InternalPlanType;
use serde::Deserialize;
@@ -77,7 +77,7 @@ impl PersonalAccessTokenAuth {
}
async fn hydrate_personal_access_token(
- client: &CodexHttpClient,
+ client: &HttpClient,
endpoint: &str,
access_token: &str,
) -> std::io::Result {
diff --git a/codex-rs/login/src/auth/revoke.rs b/codex-rs/login/src/auth/revoke.rs
index 076b8b53da..ca22adb571 100644
--- a/codex-rs/login/src/auth/revoke.rs
+++ b/codex-rs/login/src/auth/revoke.rs
@@ -7,7 +7,7 @@
use serde::Serialize;
use std::time::Duration;
-use codex_client::CodexHttpClient;
+use codex_http_client::HttpClient;
use codex_protocol::auth::AuthMode;
use super::manager::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
@@ -95,7 +95,7 @@ fn resolved_auth_mode(auth_dot_json: &AuthDotJson) -> AuthMode {
}
async fn revoke_oauth_token(
- client: &CodexHttpClient,
+ client: &HttpClient,
endpoint: &str,
token: &str,
kind: RevokeTokenKind,
@@ -181,7 +181,7 @@ mod tests {
.mount(&server)
.await;
- let client = CodexHttpClient::new(reqwest::Client::new());
+ let client = HttpClient::new(reqwest::Client::new());
let endpoint = format!("{}/oauth/revoke", server.uri());
let error = revoke_oauth_token(
&client,
diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs
index 44c2f1c4d3..7ea415fcc5 100644
--- a/codex-rs/login/src/lib.rs
+++ b/codex-rs/login/src/lib.rs
@@ -7,8 +7,8 @@ mod outbound_proxy;
mod pkce;
mod server;
-pub use codex_client::BuildCustomCaTransportError as BuildLoginHttpClientError;
pub use codex_config::types::AuthCredentialsStoreMode;
+pub use codex_http_client::BuildCustomCaTransportError as BuildLoginHttpClientError;
pub use device_code_auth::DeviceCode;
pub use device_code_auth::complete_device_code_login;
pub use device_code_auth::request_device_code;
diff --git a/codex-rs/login/src/outbound_proxy.rs b/codex-rs/login/src/outbound_proxy.rs
index bf7dfbeb4d..66441e3637 100644
--- a/codex-rs/login/src/outbound_proxy.rs
+++ b/codex-rs/login/src/outbound_proxy.rs
@@ -1,4 +1,4 @@
-use codex_client::OutboundProxyConfig;
+use codex_http_client::OutboundProxyConfig;
/// Auth-layer adapter around client-owned proxy policy.
///
diff --git a/codex-rs/model-provider/Cargo.toml b/codex-rs/model-provider/Cargo.toml
index 29599c6c7e..b6ee6acbfc 100644
--- a/codex-rs/model-provider/Cargo.toml
+++ b/codex-rs/model-provider/Cargo.toml
@@ -16,7 +16,7 @@ workspace = true
codex-api = { workspace = true }
codex-agent-identity = { workspace = true }
codex-aws-auth = { workspace = true }
-codex-client = { workspace = true }
+codex-http-client = { workspace = true }
codex-feedback = { workspace = true }
codex-login = { workspace = true }
codex-model-provider-info = { workspace = true }
diff --git a/codex-rs/model-provider/src/amazon_bedrock/auth.rs b/codex-rs/model-provider/src/amazon_bedrock/auth.rs
index 6dd897050a..b9b318d9b1 100644
--- a/codex-rs/model-provider/src/amazon_bedrock/auth.rs
+++ b/codex-rs/model-provider/src/amazon_bedrock/auth.rs
@@ -6,9 +6,9 @@ use codex_api::SharedAuthProvider;
use codex_aws_auth::AwsAuthContext;
use codex_aws_auth::AwsAuthError;
use codex_aws_auth::AwsRequestToSign;
-use codex_client::Request;
-use codex_client::RequestBody;
-use codex_client::RequestCompression;
+use codex_http_client::Request;
+use codex_http_client::RequestBody;
+use codex_http_client::RequestCompression;
use codex_login::auth::BedrockApiKeyAuth;
use codex_model_provider_info::ModelProviderAwsAuthInfo;
use codex_protocol::error::CodexErr;
diff --git a/codex-rs/tui/src/app/plugin_mentions.rs b/codex-rs/tui/src/app/plugin_mentions.rs
index ae2d25a4dc..cae0caa8f0 100644
--- a/codex-rs/tui/src/app/plugin_mentions.rs
+++ b/codex-rs/tui/src/app/plugin_mentions.rs
@@ -173,6 +173,7 @@ mod tests {
installed: true,
enabled: true,
install_policy: PluginInstallPolicy::Available,
+ install_policy_source: None,
auth_policy: PluginAuthPolicy::OnInstall,
availability: PluginAvailability::Available,
interface: None,
diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs
index 1f9374bbc3..5f890ab000 100644
--- a/codex-rs/tui/src/chatwidget/tests/helpers.rs
+++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs
@@ -1341,6 +1341,7 @@ pub(super) fn plugins_test_summary(
installed,
enabled,
install_policy,
+ install_policy_source: None,
auth_policy: PluginAuthPolicy::OnInstall,
availability: PluginAvailability::Available,
interface: Some(plugins_test_interface(
@@ -1370,6 +1371,7 @@ pub(super) fn plugins_test_remote_summary(
installed,
enabled: true,
install_policy: PluginInstallPolicy::Available,
+ install_policy_source: None,
auth_policy: PluginAuthPolicy::OnInstall,
availability: PluginAvailability::Available,
interface: Some(plugins_test_interface(
diff --git a/scripts/codex_package/README.md b/scripts/codex_package/README.md
index b76c18c3c2..7850bbd354 100644
--- a/scripts/codex_package/README.md
+++ b/scripts/codex_package/README.md
@@ -10,7 +10,8 @@ The builder creates a canonical Codex package directory:
.
├── codex-package.json
├── bin
-│ └── [.exe]
+│ ├── [.exe]
+│ └── codex-code-mode-host[.exe]
├── codex-resources
│ ├── bwrap # Linux only
│ ├── zsh/bin/zsh # supported Unix targets only
@@ -40,6 +41,7 @@ grouped `cargo build` command per package when they are needed and no prebuilt
override was provided:
- all targets: the selected entrypoint, unless `--entrypoint-bin` is provided
+- all targets: `codex-code-mode-host`, unless `--code-mode-host-bin` is provided
- Linux targets: `bwrap`, unless `--bwrap-bin` is provided
- Windows targets: `codex-command-runner` and `codex-windows-sandbox-setup`,
unless the corresponding prebuilt helper flags are provided
@@ -50,6 +52,9 @@ explicit target. Release jobs that already built and signed/notarized the
entrypoint should pass `--entrypoint-bin` so the package contains that exact
binary instead of rebuilding it.
+Release jobs should likewise pass `--code-mode-host-bin` so the package contains
+the signed host executable beside the signed entrypoint.
+
Release jobs that already built package resource binaries should also pass the
corresponding resource flags: `--bwrap-bin` for Linux packages, and
`--codex-command-runner-bin` plus `--codex-windows-sandbox-setup-bin` for
diff --git a/scripts/codex_package/cargo.py b/scripts/codex_package/cargo.py
index 208d85d174..f759e44762 100644
--- a/scripts/codex_package/cargo.py
+++ b/scripts/codex_package/cargo.py
@@ -17,6 +17,7 @@ CODEX_RS_ROOT = REPO_ROOT / "codex-rs"
@dataclass(frozen=True)
class SourceBuildOutputs:
entrypoint_bin: Path
+ code_mode_host_bin: Path
bwrap_bin: Path | None
codex_command_runner_bin: Path | None
codex_windows_sandbox_setup_bin: Path | None
@@ -29,6 +30,7 @@ def build_source_binaries(
cargo: str,
profile: str,
entrypoint_bin: Path | None,
+ code_mode_host_bin: Path | None,
bwrap_bin: Path | None,
codex_command_runner_bin: Path | None,
codex_windows_sandbox_setup_bin: Path | None,
@@ -43,6 +45,7 @@ def build_source_binaries(
spec,
variant,
build_entrypoint=entrypoint_bin is None,
+ build_code_mode_host=code_mode_host_bin is None,
build_bwrap=spec.is_linux and bwrap_bin is None,
build_codex_command_runner=spec.is_windows and codex_command_runner_bin is None,
build_codex_windows_sandbox_setup=spec.is_windows
@@ -61,7 +64,7 @@ def build_source_binaries(
cmd.extend(["--bin", binary])
cargo_env = None
- if entrypoint_bin is None:
+ if entrypoint_bin is None or code_mode_host_bin is None:
codex_v8_env = resolve_codex_v8_cargo_env(spec)
if codex_v8_env:
cargo_env = {**os.environ, **codex_v8_env}
@@ -80,6 +83,11 @@ def build_source_binaries(
entrypoint_bin,
output_dir / variant.entrypoint_name(spec),
),
+ code_mode_host_bin=(
+ code_mode_host_bin.resolve()
+ if code_mode_host_bin is not None
+ else output_dir / f"codex-code-mode-host{spec.exe_suffix}"
+ ),
bwrap_bin=resolve_output_path(
bwrap_bin,
output_dir / "bwrap" if spec.is_linux else None,
@@ -102,6 +110,7 @@ def source_binaries_for_target(
variant: PackageVariant,
*,
build_entrypoint: bool,
+ build_code_mode_host: bool,
build_bwrap: bool,
build_codex_command_runner: bool,
build_codex_windows_sandbox_setup: bool,
@@ -109,6 +118,8 @@ def source_binaries_for_target(
binaries = []
if build_entrypoint:
binaries.append(variant.cargo_bin)
+ if build_code_mode_host:
+ binaries.append("codex-code-mode-host")
if build_bwrap:
binaries.append("bwrap")
if build_codex_command_runner:
@@ -174,6 +185,7 @@ def cargo_profile_dirname(profile: str) -> str:
def validate_source_outputs(outputs: SourceBuildOutputs) -> None:
for path in [
outputs.entrypoint_bin,
+ outputs.code_mode_host_bin,
outputs.bwrap_bin,
outputs.codex_command_runner_bin,
outputs.codex_windows_sandbox_setup_bin,
diff --git a/scripts/codex_package/cli.py b/scripts/codex_package/cli.py
index 1dd276827e..f461d0df43 100644
--- a/scripts/codex_package/cli.py
+++ b/scripts/codex_package/cli.py
@@ -82,6 +82,14 @@ def parse_args() -> argparse.Namespace:
"variant. If omitted, the entrypoint is built with Cargo."
),
)
+ parser.add_argument(
+ "--code-mode-host-bin",
+ type=Path,
+ help=(
+ "Optional prebuilt codex-code-mode-host executable. If omitted, "
+ "the host is built with Cargo."
+ ),
+ )
parser.add_argument(
"--bwrap-bin",
type=Path,
@@ -148,6 +156,11 @@ def main() -> int:
"prebuilt entrypoint executable",
"--entrypoint-bin",
),
+ code_mode_host_bin=resolve_optional_input_path(
+ args.code_mode_host_bin,
+ "prebuilt code-mode host executable",
+ "--code-mode-host-bin",
+ ),
bwrap_bin=resolve_optional_input_path(
args.bwrap_bin,
"prebuilt Linux bwrap executable",
@@ -167,6 +180,7 @@ def main() -> int:
version = read_workspace_version()
inputs = PackageInputs(
entrypoint_bin=source_outputs.entrypoint_bin,
+ code_mode_host_bin=source_outputs.code_mode_host_bin,
rg_bin=resolve_rg_bin(spec, args.rg_bin),
zsh_bin=resolve_zsh_bin(spec, args.zsh_manifest),
bwrap_bin=source_outputs.bwrap_bin,
diff --git a/scripts/codex_package/layout.py b/scripts/codex_package/layout.py
index 63598672ea..f4fef6912c 100644
--- a/scripts/codex_package/layout.py
+++ b/scripts/codex_package/layout.py
@@ -51,6 +51,11 @@ def build_package_dir(
bin_dir / entrypoint_name,
is_windows=spec.is_windows,
)
+ copy_executable(
+ inputs.code_mode_host_bin,
+ bin_dir / f"codex-code-mode-host{spec.exe_suffix}",
+ is_windows=spec.is_windows,
+ )
copy_executable(inputs.rg_bin, path_dir / spec.rg_name, is_windows=spec.is_windows)
if inputs.zsh_bin is not None:
@@ -130,6 +135,7 @@ def validate_package_dir(
required_files = [
Path("bin") / variant.entrypoint_name(spec),
+ Path("bin") / f"codex-code-mode-host{spec.exe_suffix}",
Path("codex-path") / spec.rg_name,
]
executable_files = list(required_files)
diff --git a/scripts/codex_package/targets.py b/scripts/codex_package/targets.py
index 8307a3e630..016202cf5f 100644
--- a/scripts/codex_package/targets.py
+++ b/scripts/codex_package/targets.py
@@ -39,6 +39,7 @@ class PackageVariant:
@dataclass(frozen=True)
class PackageInputs:
entrypoint_bin: Path
+ code_mode_host_bin: Path
rg_bin: Path
zsh_bin: Path | None
bwrap_bin: Path | None
diff --git a/scripts/codex_package/test_cargo.py b/scripts/codex_package/test_cargo.py
index 6185f8b8b3..1a4a572b9b 100644
--- a/scripts/codex_package/test_cargo.py
+++ b/scripts/codex_package/test_cargo.py
@@ -20,6 +20,7 @@ class SourceBinariesForTargetTest(unittest.TestCase):
TARGET_SPECS["aarch64-apple-darwin"],
PACKAGE_VARIANTS["codex"],
build_entrypoint=False,
+ build_code_mode_host=False,
build_bwrap=False,
build_codex_command_runner=False,
build_codex_windows_sandbox_setup=False,
@@ -35,6 +36,7 @@ class SourceBinariesForTargetTest(unittest.TestCase):
TARGET_SPECS["x86_64-unknown-linux-musl"],
PACKAGE_VARIANTS["codex"],
build_entrypoint=False,
+ build_code_mode_host=False,
build_bwrap=False,
build_codex_command_runner=False,
build_codex_windows_sandbox_setup=False,
@@ -50,6 +52,7 @@ class SourceBinariesForTargetTest(unittest.TestCase):
TARGET_SPECS["x86_64-pc-windows-msvc"],
PACKAGE_VARIANTS["codex"],
build_entrypoint=False,
+ build_code_mode_host=False,
build_bwrap=False,
build_codex_command_runner=False,
build_codex_windows_sandbox_setup=False,
@@ -63,6 +66,7 @@ class SourceBinariesForTargetTest(unittest.TestCase):
TARGET_SPECS["x86_64-pc-windows-msvc"],
PACKAGE_VARIANTS["codex"],
build_entrypoint=False,
+ build_code_mode_host=False,
build_bwrap=False,
build_codex_command_runner=True,
build_codex_windows_sandbox_setup=True,
@@ -70,10 +74,25 @@ class SourceBinariesForTargetTest(unittest.TestCase):
["codex-command-runner", "codex-windows-sandbox-setup"],
)
+ def test_missing_code_mode_host_is_built_for_app_server(self) -> None:
+ self.assertEqual(
+ source_binaries_for_target(
+ TARGET_SPECS["aarch64-apple-darwin"],
+ PACKAGE_VARIANTS["codex-app-server"],
+ build_entrypoint=False,
+ build_code_mode_host=True,
+ build_bwrap=False,
+ build_codex_command_runner=False,
+ build_codex_windows_sandbox_setup=False,
+ ),
+ ["codex-code-mode-host"],
+ )
+
def test_build_uses_prebuilt_windows_helpers_without_running_cargo(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
entrypoint = touch_file(root / "codex.exe")
+ code_mode_host = touch_file(root / "codex-code-mode-host.exe")
command_runner = touch_file(root / "codex-command-runner.exe")
sandbox_setup = touch_file(root / "codex-windows-sandbox-setup.exe")
@@ -83,12 +102,14 @@ class SourceBinariesForTargetTest(unittest.TestCase):
cargo=str(root / "cargo-that-should-not-run"),
profile="release",
entrypoint_bin=entrypoint,
+ code_mode_host_bin=code_mode_host,
bwrap_bin=None,
codex_command_runner_bin=command_runner,
codex_windows_sandbox_setup_bin=sandbox_setup,
)
self.assertEqual(outputs.entrypoint_bin, entrypoint)
+ self.assertEqual(outputs.code_mode_host_bin, code_mode_host)
self.assertEqual(outputs.codex_command_runner_bin, command_runner)
self.assertEqual(outputs.codex_windows_sandbox_setup_bin, sandbox_setup)
diff --git a/scripts/codex_package/test_layout.py b/scripts/codex_package/test_layout.py
new file mode 100644
index 0000000000..157bce5e9c
--- /dev/null
+++ b/scripts/codex_package/test_layout.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+
+from pathlib import Path
+import sys
+import tempfile
+import unittest
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
+
+from codex_package.layout import build_package_dir
+from codex_package.layout import validate_package_dir
+from codex_package.targets import PACKAGE_VARIANTS
+from codex_package.targets import PackageInputs
+from codex_package.targets import TARGET_SPECS
+
+
+class PackageLayoutTest(unittest.TestCase):
+ def test_app_server_package_places_code_mode_host_beside_entrypoint(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ root = Path(temp_dir)
+ package_dir = root / "package"
+ package_dir.mkdir()
+ inputs = PackageInputs(
+ entrypoint_bin=touch_executable(root / "codex-app-server"),
+ code_mode_host_bin=touch_executable(root / "codex-code-mode-host"),
+ rg_bin=touch_executable(root / "rg"),
+ zsh_bin=None,
+ bwrap_bin=touch_executable(root / "bwrap"),
+ codex_command_runner_bin=None,
+ codex_windows_sandbox_setup_bin=None,
+ )
+
+ build_package_dir(
+ package_dir,
+ "1.2.3",
+ PACKAGE_VARIANTS["codex-app-server"],
+ TARGET_SPECS["x86_64-unknown-linux-musl"],
+ inputs,
+ )
+ validate_package_dir(
+ package_dir,
+ PACKAGE_VARIANTS["codex-app-server"],
+ TARGET_SPECS["x86_64-unknown-linux-musl"],
+ include_zsh=False,
+ )
+
+ self.assertTrue((package_dir / "bin" / "codex-code-mode-host").is_file())
+
+
+def touch_executable(path: Path) -> Path:
+ path.touch(mode=0o755)
+ return path
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/scripts/install/install.ps1 b/scripts/install/install.ps1
index 986f6c6566..e27d34b223 100644
--- a/scripts/install/install.ps1
+++ b/scripts/install/install.ps1
@@ -538,6 +538,7 @@ function Test-PackageContentsAreComplete {
$expectedFiles = @(
"codex-package.json",
"bin\codex.exe",
+ "bin\codex-code-mode-host.exe",
"codex-path\rg.exe",
"codex-resources\codex-command-runner.exe",
"codex-resources\codex-windows-sandbox-setup.exe"
diff --git a/scripts/install/install.sh b/scripts/install/install.sh
index d44b353860..12b66f958a 100755
--- a/scripts/install/install.sh
+++ b/scripts/install/install.sh
@@ -680,7 +680,10 @@ install_package_release() {
rm -rf "$stage_release"
mkdir -p "$stage_release"
tar -xzf "$archive_path" -C "$stage_release"
- chmod 0755 "$stage_release/bin/codex" "$stage_release/codex-path/rg"
+ chmod 0755 \
+ "$stage_release/bin/codex" \
+ "$stage_release/bin/codex-code-mode-host" \
+ "$stage_release/codex-path/rg"
if [ -f "$stage_release/codex-resources/bwrap" ]; then
chmod 0755 "$stage_release/codex-resources/bwrap"
fi
@@ -733,6 +736,7 @@ release_dir_is_complete() {
package)
[ -f "$release_dir/codex-package.json" ] &&
[ -x "$release_dir/bin/codex" ] &&
+ [ -x "$release_dir/bin/codex-code-mode-host" ] &&
[ -x "$release_dir/codex" ] &&
[ -x "$release_dir/codex-path/rg" ] ||
return 1
diff --git a/sdk/python/scripts/update_sdk_artifacts.py b/sdk/python/scripts/update_sdk_artifacts.py
index f3b97c6b6f..1c6ec59402 100755
--- a/sdk/python/scripts/update_sdk_artifacts.py
+++ b/sdk/python/scripts/update_sdk_artifacts.py
@@ -53,6 +53,10 @@ def runtime_binary_name() -> str:
return "codex.exe" if _is_windows() else "codex"
+def runtime_code_mode_host_name() -> str:
+ return "codex-code-mode-host.exe" if _is_windows() else "codex-code-mode-host"
+
+
def staged_runtime_package_root(root: Path) -> Path:
return root / RUNTIME_PACKAGE_ROOT
@@ -276,6 +280,9 @@ def _validate_codex_package_layout(package_dir: Path, package_archive: Path) ->
package_binary = package_dir / "bin" / runtime_binary_name()
if not package_binary.is_file():
missing_entries.append(str(Path("bin") / runtime_binary_name()))
+ code_mode_host = package_dir / "bin" / runtime_code_mode_host_name()
+ if not code_mode_host.is_file():
+ missing_entries.append(str(Path("bin") / runtime_code_mode_host_name()))
if missing_entries:
missing = ", ".join(missing_entries)
raise RuntimeError(f"Missing Codex package layout entries in {package_archive}: {missing}")
diff --git a/sdk/python/tests/test_artifact_workflow_and_binaries.py b/sdk/python/tests/test_artifact_workflow_and_binaries.py
index 301a75abac..ad1262e836 100644
--- a/sdk/python/tests/test_artifact_workflow_and_binaries.py
+++ b/sdk/python/tests/test_artifact_workflow_and_binaries.py
@@ -57,6 +57,7 @@ def _write_fake_codex_package(package_dir: Path, script) -> Path:
(package_dir / "codex-path").mkdir()
(package_dir / "codex-package.json").write_text('{"variant":"codex"}\n')
(package_dir / "bin" / script.runtime_binary_name()).write_text("fake codex\n")
+ (package_dir / "bin" / script.runtime_code_mode_host_name()).write_text("fake code mode host\n")
(package_dir / "codex-resources" / "bwrap").write_text("fake bwrap\n")
(package_dir / "codex-path" / "rg").write_text("fake rg\n")
return package_dir
@@ -682,11 +683,13 @@ def test_stage_runtime_release_copies_package_layout_and_sets_version(
assert {
"metadata": (package_root / "codex-package.json").read_text(),
"codex": (package_root / "bin" / script.runtime_binary_name()).read_text(),
+ "code_mode_host": (package_root / "bin" / script.runtime_code_mode_host_name()).read_text(),
"bwrap": (package_root / "codex-resources" / "bwrap").read_text(),
"rg": (package_root / "codex-path" / "rg").read_text(),
} == {
"metadata": '{"variant":"codex"}\n',
"codex": "fake codex\n",
+ "code_mode_host": "fake code mode host\n",
"bwrap": "fake bwrap\n",
"rg": "fake rg\n",
}