Merge branch 'main' into fbauer/mcp-resource-origin-call

This commit is contained in:
fbauer33
2026-07-06 19:39:18 -07:00
committed by GitHub
111 changed files with 1345 additions and 506 deletions
+4
View File
@@ -2,6 +2,10 @@
version = 1
name = "codex"
# TODO(anp) make it optional to specify this field
[setup]
script = ""
[[actions]]
name = "Run"
icon = "run"
@@ -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
@@ -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' }}
+2 -25
View File
@@ -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
+39
View File
@@ -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
@@ -9,6 +9,7 @@ Usage: build-codex-package-archive.sh \
--entrypoint-dir <dir> \
--archive-dir <dir> \
[--bwrap-bin <path>] \
[--code-mode-host-bin <path>] \
[--zsh-manifest <path>] \
[--codex-command-runner-bin <path>] \
[--codex-windows-sandbox-setup-bin <path>] \
@@ -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}"
@@ -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(
+13 -2
View File
@@ -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
+5 -12
View File
@@ -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:
+2 -5
View File
@@ -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
+2 -5
View File
@@ -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
@@ -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 }}
+4 -11
View File
@@ -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
+3 -8
View File
@@ -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
+9 -11
View File
@@ -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: |
+43 -22
View File
@@ -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
+2 -5
View File
@@ -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
+38 -27
View File
@@ -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",
+2
View File
@@ -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" }
@@ -13430,6 +13430,13 @@
],
"type": "string"
},
"PluginInstallPolicySource": {
"enum": [
"WORKSPACE_SETTING",
"IMPLICIT_CANONICAL_APP"
],
"type": "string"
},
"PluginInstallResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -14313,6 +14320,16 @@
"installPolicy": {
"$ref": "#/definitions/v2/PluginInstallPolicy"
},
"installPolicySource": {
"anyOf": [
{
"$ref": "#/definitions/v2/PluginInstallPolicySource"
},
{
"type": "null"
}
]
},
"installed": {
"type": "boolean"
},
@@ -9834,6 +9834,13 @@
],
"type": "string"
},
"PluginInstallPolicySource": {
"enum": [
"WORKSPACE_SETTING",
"IMPLICIT_CANONICAL_APP"
],
"type": "string"
},
"PluginInstallResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -10717,6 +10724,16 @@
"installPolicy": {
"$ref": "#/definitions/PluginInstallPolicy"
},
"installPolicySource": {
"anyOf": [
{
"$ref": "#/definitions/PluginInstallPolicySource"
},
{
"type": "null"
}
]
},
"installed": {
"type": "boolean"
},
@@ -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"
},
@@ -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"
},
@@ -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"
},
@@ -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"
},
@@ -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";
@@ -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.
*/
@@ -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";
@@ -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<PluginInstallPolicySource>,
pub auth_policy: PluginAuthPolicy,
/// Availability state for installing and using the plugin.
#[serde(default)]
@@ -3663,6 +3663,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,
@@ -3685,6 +3686,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,
+2 -2
View File
@@ -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.
@@ -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,
@@ -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<TurnEnvironmentParams> {
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<TurnCompletedNotification> {
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::<TurnCompletedNotification>(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,
@@ -61,6 +61,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;
@@ -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<_>>(),
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}",
@@ -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
@@ -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()),
@@ -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::<std::io::Result<Vec<_>>>()?;
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<INSTRUCTIONS>\n{AGENTS_INSTRUCTIONS}\n</INSTRUCTIONS>",
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("<environment_context>"))
.context("selected environment context should be model visible")?;
let shell = environment_context
.lines()
.find(|line| line.trim_start().starts_with("<shell>"))
.map(str::trim)
.map(str::to_string);
let cwd = environment_context
.lines()
.find(|line| line.trim_start().starts_with("<cwd>"))
.map(str::trim)
.map(str::to_string);
assert_eq!(
(shell, cwd),
(
Some(format!("<shell>{environment_shell}</shell>")),
Some(format!(
"<cwd>{}</cwd>",
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!(
"<workspace_roots><root>{}</root></workspace_roots>",
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(())
}
+1 -1
View File
@@ -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 }
+2 -2
View File
@@ -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;
+1 -1
View File
@@ -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 }
+1 -1
View File
@@ -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;
+1
View File
@@ -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 }
@@ -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;
@@ -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;
+1 -1
View File
@@ -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;
-1
View File
@@ -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",
)
+2 -33
View File
@@ -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
+3 -3
View File
@@ -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.
+3 -39
View File
@@ -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::*;
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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;
+1 -1
View File
@@ -1,4 +1,4 @@
use crate::error::TransportError;
use codex_http_client::TransportError;
use http::StatusCode;
use std::time::Duration;
@@ -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,
+31
View File
@@ -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<PluginInstallPolicySource>,
pub auth_policy: PluginAuthPolicy,
pub availability: PluginAvailability,
pub interface: Option<PluginInterface>,
@@ -169,6 +171,7 @@ pub struct RemotePluginSummary {
pub installed: bool,
pub enabled: bool,
pub install_policy: PluginInstallPolicy,
pub install_policy_source: Option<PluginInstallPolicySource>,
pub auth_policy: PluginAuthPolicy,
pub availability: PluginAvailability,
pub interface: Option<PluginInterface>,
@@ -528,6 +531,26 @@ struct RemoteAppTemplateResponse {
reason: Option<RemoteAppTemplateUnavailableReason>,
}
#[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<PluginInstallPolicySource> {
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<Vec<RemotePluginDirectorySharePrincipal>>,
installation_policy: PluginInstallPolicy,
installation_policy_source: Option<RemotePluginInstallPolicySource>,
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),
@@ -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()),
+64
View File
@@ -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<_>>(),
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<_>>(),
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 {
+9 -2
View File
@@ -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>), 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),
}
}
+36
View File
@@ -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()?;
+13 -14
View File
@@ -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<LayerMtime
.await
.context("failed to load Codex config")?;
let (exec_policy, warning) = match load_exec_policy(&config_layer_stack).await {
Ok(policy) => (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<ConfigState> {
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<LayerMtime
);
}
let config = config_from_layers(&config_layer_stack, &exec_policy)?;
let config = config_from_layers(config_layer_stack, &exec_policy)?;
let constraints = enforce_trusted_constraints(&config_layer_stack, &config)?;
let layer_mtimes = collect_layer_mtimes(&config_layer_stack);
let state = build_config_state(config, constraints)?;
Ok((state, layer_mtimes))
let constraints = enforce_trusted_constraints(config_layer_stack, &config)?;
build_config_state(config, constraints)
}
fn collect_layer_mtimes(stack: &ConfigLayerStack) -> Vec<LayerMtime> {
@@ -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(
+30 -3
View File
@@ -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<String> {
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<PermissionProfileState> {
@@ -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",
],
)
@@ -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::<LegacyAppPathString>(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::<Value>()?;
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("<environment_context>"))
.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("<shell>"))
.map(str::trim),
Some("<shell>powershell</shell>"),
);
// 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("<cwd>"))
.map(str::trim),
Some(r"<cwd>C:\windows</cwd>"),
);
let host_workspace_roots = format!(
"<workspace_roots><root>{}</root></workspace_roots>",
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
}
+80
View File
@@ -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;
@@ -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(()));
+1 -1
View File
@@ -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 }
@@ -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;
+7
View File
@@ -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",
)
+49
View File
@@ -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
+9
View File
@@ -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`.
@@ -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())
}
@@ -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.
@@ -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"];
@@ -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.
@@ -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<U>(&self, url: U) -> CodexRequestBuilder
pub fn get<U>(&self, url: U) -> RequestBuilder
where
U: IntoUrl,
{
self.request(Method::GET, url)
}
pub fn post<U>(&self, url: U) -> CodexRequestBuilder
pub fn post<U>(&self, url: U) -> RequestBuilder
where
U: IntoUrl,
{
self.request(Method::POST, url)
}
pub fn request<U>(&self, method: Method, url: U) -> CodexRequestBuilder
pub fn request<U>(&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,
@@ -1,3 +1,5 @@
//! Errors returned by the shared Codex HTTP transport.
use http::HeaderMap;
use http::StatusCode;
use thiserror::Error;
+40
View File
@@ -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;
@@ -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.
@@ -1,3 +1,5 @@
//! macOS system proxy resolution through SystemConfiguration and CFNetwork.
use std::ffi::c_void;
use std::ptr;
use std::time::Duration;
@@ -1,3 +1,5 @@
//! Windows system proxy resolution through WinHTTP.
use std::ffi::c_void;
use std::ptr;
@@ -1,3 +1,5 @@
//! Windows proxy parsing tests.
use super::*;
#[test]
@@ -1,3 +1,5 @@
//! Shared outbound proxy policy tests.
use super::*;
use pretty_assertions::assert_eq;
use std::io::Read;
@@ -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(
@@ -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<CodexRequestBuilder, TransportError> {
fn build(&self, req: Request) -> Result<RequestBuilder, TransportError> {
let prepared = req.prepare_body_for_send().map_err(TransportError::Build)?;
let Request {
@@ -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
+1 -1
View File
@@ -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 }
+13 -13
View File
@@ -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<CodexHttpClient, BuildRouteAwareHttpClientError> {
build_default_auth_reqwest_client(endpoint, auth_route_config).map(CodexHttpClient::new)
) -> Result<HttpClient, BuildRouteAwareHttpClientError> {
build_default_auth_reqwest_client(endpoint, auth_route_config).map(HttpClient::new)
}
pub fn default_headers() -> HeaderMap {
+4 -4
View File
@@ -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<Mutex<Option<AuthDotJson>>>,
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<RefreshResponse, RefreshTokenError> {
let refresh_request = RefreshRequest {
client_id: oauth_client_id(),
@@ -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<PersonalAccessTokenAuth> {
+3 -3
View File
@@ -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,
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -1,4 +1,4 @@
use codex_client::OutboundProxyConfig;
use codex_http_client::OutboundProxyConfig;
/// Auth-layer adapter around client-owned proxy policy.
///
+1 -1
View File
@@ -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 }
@@ -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;
+1
View File
@@ -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,
@@ -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(

Some files were not shown because too many files have changed in this diff Show More