mirror of
https://github.com/openai/codex.git
synced 2026-10-23 18:08:45 +00:00
Merge branch 'main' into fbauer/mcp-resource-origin-call
This commit is contained in:
@@ -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' }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+38
-27
@@ -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",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
+17
@@ -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"
|
||||
},
|
||||
|
||||
+17
@@ -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"
|
||||
},
|
||||
|
||||
+5
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(()));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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
|
||||
@@ -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`.
|
||||
+4
-4
@@ -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())
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
-1
@@ -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.
|
||||
+8
-8
@@ -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;
|
||||
@@ -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
-1
@@ -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.
|
||||
+2
@@ -1,3 +1,5 @@
|
||||
//! macOS system proxy resolution through SystemConfiguration and CFNetwork.
|
||||
|
||||
use std::ffi::c_void;
|
||||
use std::ptr;
|
||||
use std::time::Duration;
|
||||
+2
@@ -1,3 +1,5 @@
|
||||
//! Windows system proxy resolution through WinHTTP.
|
||||
|
||||
use std::ffi::c_void;
|
||||
use std::ptr;
|
||||
|
||||
+2
@@ -1,3 +1,5 @@
|
||||
//! Windows proxy parsing tests.
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
+2
@@ -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
|
||||
Vendored
@@ -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 }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
use codex_client::OutboundProxyConfig;
|
||||
use codex_http_client::OutboundProxyConfig;
|
||||
|
||||
/// Auth-layer adapter around client-owned proxy policy.
|
||||
///
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user