mirror of
https://github.com/openai/codex.git
synced 2026-05-01 09:56:37 +00:00
Compare commits
20 Commits
rust-v0.80
...
dev/sayan/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cf0f06ef5 | ||
|
|
6b4f6d2e7f | ||
|
|
4b52d37e88 | ||
|
|
31125c4f9e | ||
|
|
15d1235df9 | ||
|
|
fcb4398a19 | ||
|
|
eede02a5ef | ||
|
|
d47b85d9fe | ||
|
|
cf515142b0 | ||
|
|
74b2238931 | ||
|
|
cc0b5e8504 | ||
|
|
8e49a2c0d1 | ||
|
|
af1ed2685e | ||
|
|
1a0e2e612b | ||
|
|
acfd94f625 | ||
|
|
cabf85aa18 | ||
|
|
bc284669c2 | ||
|
|
fbe883318d | ||
|
|
2a06d64bc9 | ||
|
|
7daaabc795 |
3
.bazelignore
Normal file
3
.bazelignore
Normal file
@@ -0,0 +1,3 @@
|
||||
# Without this, Bazel will consider BUILD.bazel files in
|
||||
# .git/sl/origbackups (which can be populated by Sapling SCM).
|
||||
.git
|
||||
45
.bazelrc
Normal file
45
.bazelrc
Normal file
@@ -0,0 +1,45 @@
|
||||
common --repo_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1
|
||||
common --repo_env=BAZEL_NO_APPLE_CPP_TOOLCHAIN=1
|
||||
|
||||
common --disk_cache=~/.cache/bazel-disk-cache
|
||||
common --repo_contents_cache=~/.cache/bazel-repo-contents-cache
|
||||
common --repository_cache=~/.cache/bazel-repo-cache
|
||||
|
||||
common --experimental_platform_in_output_dir
|
||||
|
||||
common --enable_platform_specific_config
|
||||
# TODO(zbarsky): We need to untangle these libc constraints to get linux remote builds working.
|
||||
common:linux --host_platform=//:local
|
||||
common --@rules_cc//cc/toolchains/args/archiver_flags:use_libtool_on_macos=False
|
||||
common --@toolchains_llvm_bootstrapped//config:experimental_stub_libgcc_s
|
||||
|
||||
# We need to use the sh toolchain on windows so we don't send host bash paths to the linux executor.
|
||||
common:windows --@rules_rust//rust/settings:experimental_use_sh_toolchain_for_bootstrap_process_wrapper
|
||||
|
||||
# TODO(zbarsky): rules_rust doesn't implement this flag properly with remote exec...
|
||||
# common --@rules_rust//rust/settings:pipelined_compilation
|
||||
|
||||
common --incompatible_strict_action_env
|
||||
# Not ideal, but We need to allow dotslash to be found
|
||||
common --test_env=PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
|
||||
|
||||
common --test_output=errors
|
||||
common --bes_results_url=https://app.buildbuddy.io/invocation/
|
||||
common --bes_backend=grpcs://remote.buildbuddy.io
|
||||
common --remote_cache=grpcs://remote.buildbuddy.io
|
||||
common --remote_download_toplevel
|
||||
common --nobuild_runfile_links
|
||||
common --remote_timeout=3600
|
||||
common --noexperimental_throttle_remote_action_building
|
||||
common --experimental_remote_execution_keepalive
|
||||
common --grpc_keepalive_time=30s
|
||||
|
||||
# This limits both in-flight executions and concurrent downloads. Even with high number
|
||||
# of jobs execution will still be limited by CPU cores, so this just pays a bit of
|
||||
# memory in exchange for higher download concurrency.
|
||||
common --jobs=30
|
||||
|
||||
common:remote --extra_execution_platforms=//:rbe
|
||||
common:remote --remote_executor=grpcs://remote.buildbuddy.io
|
||||
common:remote --jobs=800
|
||||
|
||||
20
.github/workflows/Dockerfile.bazel
vendored
Normal file
20
.github/workflows/Dockerfile.bazel
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM ubuntu:24.04
|
||||
|
||||
# TODO(mbolin): Published to docker.io/mbolin491/codex-bazel:latest for
|
||||
# initial debugging, but we should publish to a more proper location.
|
||||
#
|
||||
# docker buildx create --use
|
||||
# docker buildx build --platform linux/amd64 -f .github/workflows/Dockerfile.bazel -t mbolin491/codex-bazel:latest --push .
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
curl git python3 ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install dotslash.
|
||||
RUN curl -LSfs "https://github.com/facebook/dotslash/releases/download/v0.5.8/dotslash-ubuntu-22.04.$(uname -m).tar.gz" | tar fxz - -C /usr/local/bin
|
||||
|
||||
# Ubuntu 24.04 ships with user 'ubuntu' already created with UID 1000.
|
||||
USER ubuntu
|
||||
|
||||
WORKDIR /workspace
|
||||
131
.github/workflows/bazel.yml
vendored
Normal file
131
.github/workflows/bazel.yml
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
name: Bazel (experimental)
|
||||
|
||||
# Note this workflow was originally derived from:
|
||||
# https://github.com/cerisier/toolchains_llvm_bootstrapped/blob/main/.github/workflows/ci.yaml
|
||||
|
||||
on:
|
||||
pull_request: {}
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
# Cancel previous actions from the same PR or branch except 'main' branch.
|
||||
# See https://docs.github.com/en/actions/using-jobs/using-concurrency and https://docs.github.com/en/actions/learn-github-actions/contexts for more info.
|
||||
group: concurrency-group::${{ github.workflow }}::${{ github.event.pull_request.number > 0 && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}${{ github.ref_name == 'main' && format('::{0}', github.run_id) || ''}}
|
||||
cancel-in-progress: ${{ github.ref_name != 'main' }}
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# macOS
|
||||
- os: macos-15-xlarge
|
||||
target: aarch64-apple-darwin
|
||||
- os: macos-15-xlarge
|
||||
target: x86_64-apple-darwin
|
||||
|
||||
# Linux
|
||||
- os: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-gnu
|
||||
- os: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- os: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
- os: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
# TODO: Enable Windows once we fix the toolchain issues there.
|
||||
#- os: windows-latest
|
||||
# target: x86_64-pc-windows-gnullvm
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
# Configure a human readable name for each job
|
||||
name: Local Bazel build on ${{ matrix.os }} for ${{ matrix.target }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
# Some integration tests rely on DotSlash being installed.
|
||||
# See https://github.com/openai/codex/pull/7617.
|
||||
- name: Install DotSlash
|
||||
uses: facebook/install-dotslash@v2
|
||||
|
||||
- name: Make DotSlash available in PATH (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
run: cp "$(which dotslash)" /usr/local/bin
|
||||
|
||||
- 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"
|
||||
|
||||
# Install Bazel via Bazelisk
|
||||
- name: Set up Bazel
|
||||
uses: bazelbuild/setup-bazelisk@v3
|
||||
|
||||
# TODO(mbolin): Bring this back once we have caching working. Currently,
|
||||
# we never seem to get a cache hit but we still end up paying the cost of
|
||||
# uploading at the end of the build, which takes over a minute!
|
||||
#
|
||||
# Cache build and external artifacts so that the next ci build is incremental.
|
||||
# Because github action caches cannot be updated after a build, we need to
|
||||
# store the contents of each build in a unique cache key, then fall back to loading
|
||||
# it on the next ci run. We use hashFiles(...) in the key and restore-keys- with
|
||||
# the prefix to load the most recent cache for the branch on a cache miss. You
|
||||
# should customize the contents of hashFiles to capture any bazel input sources,
|
||||
# although this doesn't need to be perfect. If none of the input sources change
|
||||
# then a cache hit will load an existing cache and bazel won't have to do any work.
|
||||
# In the case of a cache miss, you want the fallback cache to contain most of the
|
||||
# previously built artifacts to minimize build time. The more precise you are with
|
||||
# hashFiles sources the less work bazel will have to do.
|
||||
# - name: Mount bazel caches
|
||||
# uses: actions/cache@v4
|
||||
# with:
|
||||
# path: |
|
||||
# ~/.cache/bazel-repo-cache
|
||||
# ~/.cache/bazel-repo-contents-cache
|
||||
# key: bazel-cache-${{ matrix.os }}-${{ hashFiles('**/BUILD.bazel', '**/*.bzl', 'MODULE.bazel') }}
|
||||
# restore-keys: |
|
||||
# bazel-cache-${{ matrix.os }}
|
||||
|
||||
- name: Configure Bazel startup args (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Use a very short path to reduce argv/path length issues.
|
||||
"BAZEL_STARTUP_ARGS=--output_user_root=C:\" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
|
||||
- name: bazel test //...
|
||||
env:
|
||||
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
||||
shell: bash
|
||||
run: |
|
||||
bazel $BAZEL_STARTUP_ARGS --bazelrc=.github/workflows/ci.bazelrc test //... \
|
||||
--build_metadata=REPO_URL=https://github.com/openai/codex.git \
|
||||
--build_metadata=COMMIT_SHA=$(git rev-parse HEAD) \
|
||||
--build_metadata=ROLE=CI \
|
||||
--build_metadata=VISIBILITY=PUBLIC \
|
||||
"--remote_header=x-buildbuddy-api-key=$BUILDBUDDY_API_KEY"
|
||||
|
||||
cloud-build:
|
||||
name: just bazel-remote-test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Bazel
|
||||
uses: bazelbuild/setup-bazelisk@v3
|
||||
- name: bazel test //... --config=remote
|
||||
env:
|
||||
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bazel test //... \
|
||||
--build_metadata=REPO_URL=https://github.com/openai/codex.git \
|
||||
--build_metadata=COMMIT_SHA=$(git rev-parse HEAD) \
|
||||
--build_metadata=ROLE=CI \
|
||||
--build_metadata=VISIBILITY=PUBLIC \
|
||||
"--remote_header=x-buildbuddy-api-key=$BUILDBUDDY_API_KEY" \
|
||||
--config=remote --platforms=//:rbe --keep_going
|
||||
15
.github/workflows/ci.bazelrc
vendored
Normal file
15
.github/workflows/ci.bazelrc
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
common --remote_download_minimal
|
||||
common --nobuild_runfile_links
|
||||
common --keep_going
|
||||
|
||||
# Prefer to run the build actions entirely remotely so we can dial up the concurrency.
|
||||
# Currently remote builds only work on Mac hosts, until we untangle the libc constraints mess on linux.
|
||||
common:macos --config=remote
|
||||
common:macos --strategy=remote
|
||||
|
||||
# We have platform-specific tests, so execute the tests locally using the strongest sandboxing available on each platform.
|
||||
common:macos --strategy=TestRunner=darwin-sandbox,local
|
||||
# Note: linux-sandbox is stronger, but not available in GHA.
|
||||
common:linux --strategy=TestRunner=processwrapper-sandbox,local
|
||||
common:windows --strategy=TestRunner=local
|
||||
|
||||
4
.github/workflows/rust-ci.yml
vendored
4
.github/workflows/rust-ci.yml
vendored
@@ -66,6 +66,10 @@ jobs:
|
||||
run: cargo fmt -- --config imports_granularity=Item --check
|
||||
- name: Verify codegen for mcp-types
|
||||
run: ./mcp-types/check_lib_rs.py
|
||||
- name: Verify config schema fixture
|
||||
run: |
|
||||
cargo run -p codex-core --bin codex-write-config-schema
|
||||
git diff --exit-code core/config.schema.json
|
||||
|
||||
cargo_shear:
|
||||
name: cargo shear
|
||||
|
||||
@@ -13,6 +13,7 @@ In the codex-rs folder where the rust code lives:
|
||||
- Use method references over closures when possible per https://rust-lang.github.io/rust-clippy/master/index.html#redundant_closure_for_method_calls
|
||||
- When writing tests, prefer comparing the equality of entire objects over fields one by one.
|
||||
- When making a change that adds or changes an API, ensure that the documentation in the `docs/` folder is up to date if applicable.
|
||||
- If you change `ConfigToml` or nested config types, run `just write-config-schema` to update `codex-rs/core/config.schema.json`.
|
||||
|
||||
Run `just fmt` (in `codex-rs` directory) automatically after making Rust code changes; do not ask for approval to run it. Before finalizing a change to `codex-rs`, run `just fix -p <project>` (in `codex-rs` directory) to fix any linter issues in the code. Prefer scoping with `-p` to avoid slow workspace‑wide Clippy builds; only run `just fix` without `-p` if you changed shared crates. Additionally, run the tests:
|
||||
|
||||
|
||||
31
BUILD.bazel
Normal file
31
BUILD.bazel
Normal file
@@ -0,0 +1,31 @@
|
||||
# We mark the local platform as glibc-compatible so that rust can grab a toolchain for us.
|
||||
# TODO(zbarsky): Upstream a better libc constraint into rules_rust.
|
||||
# We only enable this on linux though for sanity, and because it breaks remote execution.
|
||||
platform(
|
||||
name = "local",
|
||||
constraint_values = [
|
||||
"@toolchains_llvm_bootstrapped//constraints/libc:gnu.2.28",
|
||||
],
|
||||
parents = [
|
||||
"@platforms//host",
|
||||
],
|
||||
)
|
||||
|
||||
platform(
|
||||
name = "rbe",
|
||||
constraint_values = [
|
||||
"@platforms//cpu:x86_64",
|
||||
"@platforms//os:linux",
|
||||
"@bazel_tools//tools/cpp:clang",
|
||||
"@toolchains_llvm_bootstrapped//constraints/libc:gnu.2.28",
|
||||
],
|
||||
exec_properties = {
|
||||
# Ubuntu-based image that includes git, python3, dotslash, and other
|
||||
# tools that various integration tests need.
|
||||
# Verify at https://hub.docker.com/layers/mbolin491/codex-bazel/latest/images/sha256:8c9ff94187ea7c08a31e9a81f5fe8046ea3972a6768983c955c4079fa30567fb
|
||||
"container-image": "docker://docker.io/mbolin491/codex-bazel@sha256:8c9ff94187ea7c08a31e9a81f5fe8046ea3972a6768983c955c4079fa30567fb",
|
||||
"OSFamily": "Linux",
|
||||
},
|
||||
)
|
||||
|
||||
exports_files(["AGENTS.md"])
|
||||
122
MODULE.bazel
Normal file
122
MODULE.bazel
Normal file
@@ -0,0 +1,122 @@
|
||||
bazel_dep(name = "platforms", version = "1.0.0")
|
||||
bazel_dep(name = "toolchains_llvm_bootstrapped", version = "0.3.1")
|
||||
archive_override(
|
||||
module_name = "toolchains_llvm_bootstrapped",
|
||||
integrity = "sha256-9ks21bgEqbQWmwUIvqeLA64+Jk6o4ZVjC8KxjVa2Vw8=",
|
||||
strip_prefix = "toolchains_llvm_bootstrapped-e3775e66a7b6d287c705ca0cd24497ef4a77c503",
|
||||
urls = ["https://github.com/cerisier/toolchains_llvm_bootstrapped/archive/e3775e66a7b6d287c705ca0cd24497ef4a77c503/master.tar.gz"],
|
||||
patch_strip = 1,
|
||||
patches = [
|
||||
"//patches:llvm_toolchain_archive_params.patch",
|
||||
],
|
||||
)
|
||||
|
||||
osx = use_extension("@toolchains_llvm_bootstrapped//toolchain/extension:osx.bzl", "osx")
|
||||
osx.framework(name = "ApplicationServices")
|
||||
osx.framework(name = "AppKit")
|
||||
osx.framework(name = "ColorSync")
|
||||
osx.framework(name = "CoreFoundation")
|
||||
osx.framework(name = "CoreGraphics")
|
||||
osx.framework(name = "CoreServices")
|
||||
osx.framework(name = "CoreText")
|
||||
osx.framework(name = "CFNetwork")
|
||||
osx.framework(name = "Foundation")
|
||||
osx.framework(name = "ImageIO")
|
||||
osx.framework(name = "Kernel")
|
||||
osx.framework(name = "OSLog")
|
||||
osx.framework(name = "Security")
|
||||
osx.framework(name = "SystemConfiguration")
|
||||
|
||||
register_toolchains(
|
||||
"@toolchains_llvm_bootstrapped//toolchain:all",
|
||||
)
|
||||
|
||||
bazel_dep(name = "rules_cc", version = "0.2.16")
|
||||
bazel_dep(name = "rules_platform", version = "0.1.0")
|
||||
bazel_dep(name = "rules_rust", version = "0.68.1")
|
||||
single_version_override(
|
||||
module_name = "rules_rust",
|
||||
patch_strip = 1,
|
||||
patches = [
|
||||
"//patches:rules_rust.patch",
|
||||
"//patches:rules_rust_windows_gnu.patch",
|
||||
"//patches:rules_rust_musl.patch",
|
||||
],
|
||||
)
|
||||
|
||||
RUST_TRIPLES = [
|
||||
"aarch64-unknown-linux-musl",
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-pc-windows-gnullvm",
|
||||
"x86_64-unknown-linux-musl",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-pc-windows-gnullvm",
|
||||
]
|
||||
|
||||
rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
|
||||
rust.toolchain(
|
||||
edition = "2024",
|
||||
extra_target_triples = RUST_TRIPLES,
|
||||
versions = ["1.90.0"],
|
||||
)
|
||||
use_repo(rust, "rust_toolchains")
|
||||
|
||||
register_toolchains("@rust_toolchains//:all")
|
||||
|
||||
bazel_dep(name = "rules_rs", version = "0.0.23")
|
||||
|
||||
crate = use_extension("@rules_rs//rs:extensions.bzl", "crate")
|
||||
crate.from_cargo(
|
||||
cargo_lock = "//codex-rs:Cargo.lock",
|
||||
cargo_toml = "//codex-rs:Cargo.toml",
|
||||
platform_triples = RUST_TRIPLES,
|
||||
)
|
||||
|
||||
bazel_dep(name = "openssl", version = "3.5.4.bcr.0")
|
||||
|
||||
crate.annotation(
|
||||
build_script_data = [
|
||||
"@openssl//:gen_dir",
|
||||
],
|
||||
build_script_env = {
|
||||
"OPENSSL_DIR": "$(execpath @openssl//:gen_dir)",
|
||||
"OPENSSL_NO_VENDOR": "1",
|
||||
"OPENSSL_STATIC": "1",
|
||||
},
|
||||
crate = "openssl-sys",
|
||||
data = ["@openssl//:gen_dir"],
|
||||
)
|
||||
|
||||
inject_repo(crate, "openssl")
|
||||
|
||||
# Fix readme inclusions
|
||||
crate.annotation(
|
||||
crate = "windows-link",
|
||||
patch_args = ["-p1"],
|
||||
patches = [
|
||||
"//patches:windows-link.patch"
|
||||
],
|
||||
)
|
||||
|
||||
WINDOWS_IMPORT_LIB = """
|
||||
load("@rules_cc//cc:defs.bzl", "cc_import")
|
||||
|
||||
cc_import(
|
||||
name = "windows_import_lib",
|
||||
static_library = glob(["lib/*.a"])[0],
|
||||
)
|
||||
"""
|
||||
|
||||
crate.annotation(
|
||||
additive_build_file_content = WINDOWS_IMPORT_LIB,
|
||||
crate = "windows_x86_64_gnullvm",
|
||||
gen_build_script = "off",
|
||||
deps = [":windows_import_lib"],
|
||||
)
|
||||
crate.annotation(
|
||||
additive_build_file_content = WINDOWS_IMPORT_LIB,
|
||||
crate = "windows_aarch64_gnullvm",
|
||||
gen_build_script = "off",
|
||||
deps = [":windows_import_lib"],
|
||||
)
|
||||
use_repo(crate, "crates")
|
||||
1095
MODULE.bazel.lock
generated
Normal file
1095
MODULE.bazel.lock
generated
Normal file
File diff suppressed because one or more lines are too long
@@ -1,16 +0,0 @@
|
||||
# Example announcement tips for Codex TUI.
|
||||
# Each [[announcements]] entry is evaluated in order; the last matching one is shown.
|
||||
# Dates are UTC, formatted as YYYY-MM-DD. The from_date is inclusive and the to_date is exclusive.
|
||||
# version_regex matches against the CLI version (env!("CARGO_PKG_VERSION")); omit to apply to all versions.
|
||||
# target_app specify which app should display the announcement (cli, vsce, ...).
|
||||
|
||||
[[announcements]]
|
||||
content = "Welcome to Codex! Check out the new onboarding flow."
|
||||
from_date = "2024-10-01"
|
||||
to_date = "2024-10-15"
|
||||
target_app = "cli"
|
||||
|
||||
[[announcements]]
|
||||
content = "This is a test announcement"
|
||||
version_regex = "^0\\.0\\.0$"
|
||||
to_date = "2026-01-10"
|
||||
@@ -95,7 +95,6 @@ function detectPackageManager() {
|
||||
return "bun";
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
__dirname.includes(".bun/install/global") ||
|
||||
__dirname.includes(".bun\\install\\global")
|
||||
|
||||
1
codex-rs/BUILD.bazel
Normal file
1
codex-rs/BUILD.bazel
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
40
codex-rs/Cargo.lock
generated
40
codex-rs/Cargo.lock
generated
@@ -1036,7 +1036,7 @@ dependencies = [
|
||||
"codex-utils-absolute-path",
|
||||
"mcp-types",
|
||||
"pretty_assertions",
|
||||
"schemars 0.8.22",
|
||||
"schemars 1.0.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum_macros 0.27.2",
|
||||
@@ -1320,6 +1320,7 @@ dependencies = [
|
||||
"regex",
|
||||
"regex-lite",
|
||||
"reqwest",
|
||||
"schemars 1.0.4",
|
||||
"seccompiler",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1494,7 +1495,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"pretty_assertions",
|
||||
"regex",
|
||||
"schemars 0.8.22",
|
||||
"schemars 1.0.4",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"thiserror 2.0.17",
|
||||
@@ -1576,7 +1577,7 @@ dependencies = [
|
||||
"mcp_test_support",
|
||||
"os_info",
|
||||
"pretty_assertions",
|
||||
"schemars 0.8.22",
|
||||
"schemars 1.0.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shlex",
|
||||
@@ -1653,7 +1654,7 @@ dependencies = [
|
||||
"mcp-types",
|
||||
"mime_guess",
|
||||
"pretty_assertions",
|
||||
"schemars 0.8.22",
|
||||
"schemars 1.0.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
@@ -1699,6 +1700,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"reqwest",
|
||||
"rmcp",
|
||||
"schemars 1.0.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serial_test",
|
||||
@@ -1871,7 +1873,7 @@ name = "codex-utils-absolute-path"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"path-absolutize",
|
||||
"schemars 0.8.22",
|
||||
"schemars 1.0.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
@@ -4218,7 +4220,7 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
name = "mcp-types"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"schemars 0.8.22",
|
||||
"schemars 1.0.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ts-rs",
|
||||
@@ -5893,18 +5895,6 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"schemars_derive 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.9.0"
|
||||
@@ -5926,23 +5916,11 @@ dependencies = [
|
||||
"chrono",
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
"schemars_derive 1.0.4",
|
||||
"schemars_derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "1.0.4"
|
||||
|
||||
@@ -182,7 +182,7 @@ regex = "1.12.2"
|
||||
regex-lite = "0.1.8"
|
||||
reqwest = "0.12"
|
||||
rmcp = { version = "0.12.0", default-features = false }
|
||||
schemars = "0.8.22"
|
||||
schemars = "1.0.4"
|
||||
seccompiler = "0.5.0"
|
||||
sentry = "0.46.0"
|
||||
serde = "1"
|
||||
|
||||
6
codex-rs/ansi-escape/BUILD.bazel
Normal file
6
codex-rs/ansi-escape/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "ansi-escape",
|
||||
crate_name = "codex_ansi_escape",
|
||||
)
|
||||
6
codex-rs/app-server-protocol/BUILD.bazel
Normal file
6
codex-rs/app-server-protocol/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "app-server-protocol",
|
||||
crate_name = "codex_app_server_protocol",
|
||||
)
|
||||
6
codex-rs/app-server-test-client/BUILD.bazel
Normal file
6
codex-rs/app-server-test-client/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "codex-app-server-test-client",
|
||||
crate_name = "codex_app_server_test_client",
|
||||
)
|
||||
8
codex-rs/app-server/BUILD.bazel
Normal file
8
codex-rs/app-server/BUILD.bazel
Normal file
@@ -0,0 +1,8 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "app-server",
|
||||
crate_name = "codex_app_server",
|
||||
integration_deps_extra = ["//codex-rs/app-server/tests/common:common"],
|
||||
test_tags = ["no-sandbox"],
|
||||
)
|
||||
@@ -52,6 +52,10 @@ Clients must send a single `initialize` request before invoking any other method
|
||||
|
||||
Applications building on top of `codex app-server` should identify themselves via the `clientInfo` parameter.
|
||||
|
||||
**Important**: `clientInfo.name` is used to identify the client for the OpenAI Compliance Logs Platform. If
|
||||
you are developing a new Codex integration that is intended for enterprise use, please contact us to get it
|
||||
added to a known clients list. For more context: https://chatgpt.com/admin/api-reference#tag/Logs:-Codex
|
||||
|
||||
Example (from OpenAI's official VSCode extension):
|
||||
|
||||
```json
|
||||
@@ -60,7 +64,7 @@ Example (from OpenAI's official VSCode extension):
|
||||
"id": 0,
|
||||
"params": {
|
||||
"clientInfo": {
|
||||
"name": "codex-vscode",
|
||||
"name": "codex_vscode",
|
||||
"title": "Codex VS Code Extension",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
|
||||
@@ -92,13 +92,18 @@ pub async fn run_main(
|
||||
|
||||
let feedback = CodexFeedback::new();
|
||||
|
||||
let otel = codex_core::otel_init::build_provider(&config, env!("CARGO_PKG_VERSION"), false)
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!("error loading otel config: {e}"),
|
||||
)
|
||||
})?;
|
||||
let otel = codex_core::otel_init::build_provider(
|
||||
&config,
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
Some("codex_app_server"),
|
||||
false,
|
||||
)
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!("error loading otel config: {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Install a simple subscriber so `tracing` output is visible. Users can
|
||||
// control the log level with `RUST_LOG`.
|
||||
|
||||
@@ -21,8 +21,10 @@ use codex_core::AuthManager;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
use codex_core::default_client::SetOriginatorError;
|
||||
use codex_core::default_client::USER_AGENT_SUFFIX;
|
||||
use codex_core::default_client::get_codex_user_agent;
|
||||
use codex_core::default_client::set_default_originator;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use toml::Value as TomlValue;
|
||||
@@ -121,6 +123,27 @@ impl MessageProcessor {
|
||||
title: _title,
|
||||
version,
|
||||
} = params.client_info;
|
||||
if let Err(error) = set_default_originator(name.clone()) {
|
||||
match error {
|
||||
SetOriginatorError::InvalidHeaderValue => {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!(
|
||||
"Invalid clientInfo.name: '{name}'. Must be a valid HTTP header value."
|
||||
),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
SetOriginatorError::AlreadyInitialized => {
|
||||
// No-op. This is expected to happen if the originator is already set via env var.
|
||||
// TODO(owen): Once we remove support for CODEX_INTERNAL_ORIGINATOR_OVERRIDE,
|
||||
// this will be an unexpected state and we can return a JSON-RPC error indicating
|
||||
// internal server error.
|
||||
}
|
||||
}
|
||||
}
|
||||
let user_agent_suffix = format!("{name}; {version}");
|
||||
if let Ok(mut suffix) = USER_AGENT_SUFFIX.lock() {
|
||||
*suffix = Some(user_agent_suffix);
|
||||
|
||||
7
codex-rs/app-server/tests/common/BUILD.bazel
Normal file
7
codex-rs/app-server/tests/common/BUILD.bazel
Normal file
@@ -0,0 +1,7 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "common",
|
||||
crate_name = "app_test_support",
|
||||
crate_srcs = glob(["*.rs"]),
|
||||
)
|
||||
@@ -17,6 +17,7 @@ pub use core_test_support::format_with_current_shell_non_login;
|
||||
pub use core_test_support::test_path_buf_with_windows;
|
||||
pub use core_test_support::test_tmp_path;
|
||||
pub use core_test_support::test_tmp_path_buf;
|
||||
pub use mcp_process::DEFAULT_CLIENT_NAME;
|
||||
pub use mcp_process::McpProcess;
|
||||
pub use mock_model_server::create_mock_responses_server_repeating_assistant;
|
||||
pub use mock_model_server::create_mock_responses_server_sequence;
|
||||
|
||||
@@ -66,6 +66,8 @@ pub struct McpProcess {
|
||||
pending_messages: VecDeque<JSONRPCMessage>,
|
||||
}
|
||||
|
||||
pub const DEFAULT_CLIENT_NAME: &str = "codex-app-server-tests";
|
||||
|
||||
impl McpProcess {
|
||||
pub async fn new(codex_home: &Path) -> anyhow::Result<Self> {
|
||||
Self::new_with_env(codex_home, &[]).await
|
||||
@@ -136,33 +138,62 @@ impl McpProcess {
|
||||
|
||||
/// Performs the initialization handshake with the MCP server.
|
||||
pub async fn initialize(&mut self) -> anyhow::Result<()> {
|
||||
let params = Some(serde_json::to_value(InitializeParams {
|
||||
client_info: ClientInfo {
|
||||
name: "codex-app-server-tests".to_string(),
|
||||
let initialized = self
|
||||
.initialize_with_client_info(ClientInfo {
|
||||
name: DEFAULT_CLIENT_NAME.to_string(),
|
||||
title: None,
|
||||
version: "0.1.0".to_string(),
|
||||
},
|
||||
})?);
|
||||
let req_id = self.send_request("initialize", params).await?;
|
||||
let initialized = self.read_jsonrpc_message().await?;
|
||||
let JSONRPCMessage::Response(response) = initialized else {
|
||||
})
|
||||
.await?;
|
||||
let JSONRPCMessage::Response(_) = initialized else {
|
||||
unreachable!("expected JSONRPCMessage::Response for initialize, got {initialized:?}");
|
||||
};
|
||||
if response.id != RequestId::Integer(req_id) {
|
||||
anyhow::bail!(
|
||||
"initialize response id mismatch: expected {}, got {:?}",
|
||||
req_id,
|
||||
response.id
|
||||
);
|
||||
}
|
||||
|
||||
// Send notifications/initialized to ack the response.
|
||||
self.send_notification(ClientNotification::Initialized)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends initialize with the provided client info and returns the response/error message.
|
||||
pub async fn initialize_with_client_info(
|
||||
&mut self,
|
||||
client_info: ClientInfo,
|
||||
) -> anyhow::Result<JSONRPCMessage> {
|
||||
let params = Some(serde_json::to_value(InitializeParams { client_info })?);
|
||||
let request_id = self.send_request("initialize", params).await?;
|
||||
let message = self.read_jsonrpc_message().await?;
|
||||
match message {
|
||||
JSONRPCMessage::Response(response) => {
|
||||
if response.id != RequestId::Integer(request_id) {
|
||||
anyhow::bail!(
|
||||
"initialize response id mismatch: expected {}, got {:?}",
|
||||
request_id,
|
||||
response.id
|
||||
);
|
||||
}
|
||||
|
||||
// Send notifications/initialized to ack the response.
|
||||
self.send_notification(ClientNotification::Initialized)
|
||||
.await?;
|
||||
|
||||
Ok(JSONRPCMessage::Response(response))
|
||||
}
|
||||
JSONRPCMessage::Error(error) => {
|
||||
if error.id != RequestId::Integer(request_id) {
|
||||
anyhow::bail!(
|
||||
"initialize error id mismatch: expected {}, got {:?}",
|
||||
request_id,
|
||||
error.id
|
||||
);
|
||||
}
|
||||
Ok(JSONRPCMessage::Error(error))
|
||||
}
|
||||
JSONRPCMessage::Notification(notification) => {
|
||||
anyhow::bail!("unexpected JSONRPCMessage::Notification: {notification:?}");
|
||||
}
|
||||
JSONRPCMessage::Request(request) => {
|
||||
anyhow::bail!("unexpected JSONRPCMessage::Request: {request:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a `newConversation` JSON-RPC request.
|
||||
pub async fn send_new_conversation_request(
|
||||
&mut self,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use app_test_support::DEFAULT_CLIENT_NAME;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::GetUserAgentResponse;
|
||||
@@ -25,13 +26,13 @@ async fn get_user_agent_returns_current_codex_user_agent() -> Result<()> {
|
||||
.await??;
|
||||
|
||||
let os_info = os_info::get();
|
||||
let originator = codex_core::default_client::originator().value.as_str();
|
||||
let originator = DEFAULT_CLIENT_NAME;
|
||||
let os_type = os_info.os_type();
|
||||
let os_version = os_info.version();
|
||||
let architecture = os_info.architecture().unwrap_or("unknown");
|
||||
let terminal_ua = codex_core::terminal::user_agent();
|
||||
let user_agent = format!(
|
||||
"{originator}/0.0.0 ({os_type} {os_version}; {architecture}) {terminal_ua} (codex-app-server-tests; 0.1.0)"
|
||||
"{originator}/0.0.0 ({os_type} {os_version}; {architecture}) {terminal_ua} ({DEFAULT_CLIENT_NAME}; 0.1.0)"
|
||||
);
|
||||
|
||||
let received: GetUserAgentResponse = to_response(response)?;
|
||||
|
||||
137
codex-rs/app-server/tests/suite/v2/initialize.rs
Normal file
137
codex-rs/app-server/tests/suite/v2/initialize.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::create_mock_responses_server_sequence_unchecked;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::InitializeResponse;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
#[tokio::test]
|
||||
async fn initialize_uses_client_info_name_as_originator() -> Result<()> {
|
||||
let responses = Vec::new();
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
|
||||
let message = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.initialize_with_client_info(ClientInfo {
|
||||
name: "codex_vscode".to_string(),
|
||||
title: Some("Codex VS Code Extension".to_string()),
|
||||
version: "0.1.0".to_string(),
|
||||
}),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let JSONRPCMessage::Response(response) = message else {
|
||||
anyhow::bail!("expected initialize response, got {message:?}");
|
||||
};
|
||||
let InitializeResponse { user_agent } = to_response::<InitializeResponse>(response)?;
|
||||
|
||||
assert!(user_agent.starts_with("codex_vscode/"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn initialize_respects_originator_override_env_var() -> Result<()> {
|
||||
let responses = Vec::new();
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = McpProcess::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(
|
||||
"CODEX_INTERNAL_ORIGINATOR_OVERRIDE",
|
||||
Some("codex_originator_via_env_var"),
|
||||
)],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let message = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.initialize_with_client_info(ClientInfo {
|
||||
name: "codex_vscode".to_string(),
|
||||
title: Some("Codex VS Code Extension".to_string()),
|
||||
version: "0.1.0".to_string(),
|
||||
}),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let JSONRPCMessage::Response(response) = message else {
|
||||
anyhow::bail!("expected initialize response, got {message:?}");
|
||||
};
|
||||
let InitializeResponse { user_agent } = to_response::<InitializeResponse>(response)?;
|
||||
|
||||
assert!(user_agent.starts_with("codex_originator_via_env_var/"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn initialize_rejects_invalid_client_name() -> Result<()> {
|
||||
let responses = Vec::new();
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = McpProcess::new_with_env(
|
||||
codex_home.path(),
|
||||
&[("CODEX_INTERNAL_ORIGINATOR_OVERRIDE", None)],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let message = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.initialize_with_client_info(ClientInfo {
|
||||
name: "bad\rname".to_string(),
|
||||
title: Some("Bad Client".to_string()),
|
||||
version: "0.1.0".to_string(),
|
||||
}),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let JSONRPCMessage::Error(error) = message else {
|
||||
anyhow::bail!("expected initialize error, got {message:?}");
|
||||
};
|
||||
|
||||
assert_eq!(error.error.code, -32600);
|
||||
assert_eq!(
|
||||
error.error.message,
|
||||
"Invalid clientInfo.name: 'bad\rname'. Must be a valid HTTP header value."
|
||||
);
|
||||
assert_eq!(error.error.data, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper to create a config.toml pointing at the mock model server.
|
||||
fn create_config_toml(
|
||||
codex_home: &Path,
|
||||
server_uri: &str,
|
||||
approval_policy: &str,
|
||||
) -> std::io::Result<()> {
|
||||
let config_toml = codex_home.join("config.toml");
|
||||
std::fs::write(
|
||||
config_toml,
|
||||
format!(
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "{approval_policy}"
|
||||
sandbox_mode = "read-only"
|
||||
|
||||
model_provider = "mock_provider"
|
||||
|
||||
[model_providers.mock_provider]
|
||||
name = "Mock provider for test"
|
||||
base_url = "{server_uri}/v1"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
"#
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
mod account;
|
||||
mod config_rpc;
|
||||
mod initialize;
|
||||
mod model_list;
|
||||
mod output_schema;
|
||||
mod rate_limits;
|
||||
|
||||
@@ -8,6 +8,7 @@ use app_test_support::create_mock_responses_server_sequence_unchecked;
|
||||
use app_test_support::create_shell_command_sse_response;
|
||||
use app_test_support::format_with_current_shell_display;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::CommandExecutionApprovalDecision;
|
||||
use codex_app_server_protocol::CommandExecutionRequestApprovalResponse;
|
||||
use codex_app_server_protocol::CommandExecutionStatus;
|
||||
@@ -40,6 +41,76 @@ use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
const TEST_ORIGINATOR: &str = "codex_vscode";
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_sends_originator_header() -> Result<()> {
|
||||
let responses = vec![create_final_assistant_message_sse_response("Done")?];
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.initialize_with_client_info(ClientInfo {
|
||||
name: TEST_ORIGINATOR.to_string(),
|
||||
title: Some("Codex VS Code Extension".to_string()),
|
||||
version: "0.1.0".to_string(),
|
||||
}),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let thread_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
|
||||
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "Hello".to_string(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("failed to fetch received requests");
|
||||
assert!(!requests.is_empty());
|
||||
for request in requests {
|
||||
let originator = request
|
||||
.headers
|
||||
.get("originator")
|
||||
.expect("originator header missing");
|
||||
assert_eq!(originator.to_str()?, TEST_ORIGINATOR);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<()> {
|
||||
|
||||
11
codex-rs/apply-patch/BUILD.bazel
Normal file
11
codex-rs/apply-patch/BUILD.bazel
Normal file
@@ -0,0 +1,11 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
exports_files(["apply_patch_tool_instructions.md"])
|
||||
|
||||
codex_rust_crate(
|
||||
name = "apply-patch",
|
||||
crate_name = "codex_apply_patch",
|
||||
compile_data = [
|
||||
"apply_patch_tool_instructions.md",
|
||||
],
|
||||
)
|
||||
6
codex-rs/arg0/BUILD.bazel
Normal file
6
codex-rs/arg0/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "arg0",
|
||||
crate_name = "codex_arg0",
|
||||
)
|
||||
@@ -145,11 +145,41 @@ where
|
||||
/// that `apply_patch` can be on the PATH without requiring the user to
|
||||
/// install a separate `apply_patch` executable, simplifying the deployment of
|
||||
/// Codex CLI.
|
||||
/// Note: In debug builds the temp-dir guard is disabled to ease local testing.
|
||||
///
|
||||
/// IMPORTANT: This function modifies the PATH environment variable, so it MUST
|
||||
/// be called before multiple threads are spawned.
|
||||
pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result<TempDir> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let codex_home = codex_core::config::find_codex_home()?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
// Guard against placing helpers in system temp directories outside debug builds.
|
||||
let temp_root = std::env::temp_dir();
|
||||
if codex_home.starts_with(&temp_root) {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"Refusing to create helper binaries under temporary dir {temp_root:?} (codex_home: {codex_home:?})"
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
std::fs::create_dir_all(&codex_home)?;
|
||||
// Use a CODEX_HOME-scoped temp root to avoid cluttering the top-level directory.
|
||||
let temp_root = codex_home.join("tmp").join("path");
|
||||
std::fs::create_dir_all(&temp_root)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
// Ensure only the current user can access the temp directory.
|
||||
std::fs::set_permissions(&temp_root, std::fs::Permissions::from_mode(0o700))?;
|
||||
}
|
||||
|
||||
let temp_dir = tempfile::Builder::new()
|
||||
.prefix("codex-arg0")
|
||||
.tempdir_in(&temp_root)?;
|
||||
let path = temp_dir.path();
|
||||
|
||||
for filename in &[
|
||||
|
||||
6
codex-rs/async-utils/BUILD.bazel
Normal file
6
codex-rs/async-utils/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "async-utils",
|
||||
crate_name = "codex_async_utils",
|
||||
)
|
||||
7
codex-rs/backend-client/BUILD.bazel
Normal file
7
codex-rs/backend-client/BUILD.bazel
Normal file
@@ -0,0 +1,7 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "backend-client",
|
||||
crate_name = "codex_backend_client",
|
||||
compile_data = glob(["tests/fixtures/**"]),
|
||||
)
|
||||
6
codex-rs/chatgpt/BUILD.bazel
Normal file
6
codex-rs/chatgpt/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "chatgpt",
|
||||
crate_name = "codex_chatgpt",
|
||||
)
|
||||
10
codex-rs/cli/BUILD.bazel
Normal file
10
codex-rs/cli/BUILD.bazel
Normal file
@@ -0,0 +1,10 @@
|
||||
load("//:defs.bzl", "codex_rust_crate", "multiplatform_binaries")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "cli",
|
||||
crate_name = "codex_cli",
|
||||
)
|
||||
|
||||
multiplatform_binaries(
|
||||
name = "codex",
|
||||
)
|
||||
10
codex-rs/cloud-tasks-client/BUILD.bazel
Normal file
10
codex-rs/cloud-tasks-client/BUILD.bazel
Normal file
@@ -0,0 +1,10 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "cloud-tasks-client",
|
||||
crate_name = "codex_cloud_tasks_client",
|
||||
crate_features = [
|
||||
"mock",
|
||||
"online",
|
||||
],
|
||||
)
|
||||
6
codex-rs/cloud-tasks/BUILD.bazel
Normal file
6
codex-rs/cloud-tasks/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "cloud-tasks",
|
||||
crate_name = "codex_cloud_tasks",
|
||||
)
|
||||
6
codex-rs/codex-api/BUILD.bazel
Normal file
6
codex-rs/codex-api/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "codex-api",
|
||||
crate_name = "codex_api",
|
||||
)
|
||||
@@ -301,7 +301,9 @@ pub async fn process_sse(
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
_ => {
|
||||
trace!("unhandled SSE event: {:#?}", event.kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
codex-rs/codex-backend-openapi-models/BUILD.bazel
Normal file
6
codex-rs/codex-backend-openapi-models/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "codex-backend-openapi-models",
|
||||
crate_name = "codex_backend_openapi_models",
|
||||
)
|
||||
6
codex-rs/codex-client/BUILD.bazel
Normal file
6
codex-rs/codex-client/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "codex-client",
|
||||
crate_name = "codex_client",
|
||||
)
|
||||
@@ -7,6 +7,7 @@ pub enum TransportError {
|
||||
#[error("http {status}: {body:?}")]
|
||||
Http {
|
||||
status: StatusCode,
|
||||
url: Option<String>,
|
||||
headers: Option<HeaderMap>,
|
||||
body: Option<String>,
|
||||
},
|
||||
|
||||
@@ -131,6 +131,7 @@ impl HttpTransport for ReqwestTransport {
|
||||
);
|
||||
}
|
||||
|
||||
let url = req.url.clone();
|
||||
let builder = self.build(req)?;
|
||||
let resp = builder.send().await.map_err(Self::map_error)?;
|
||||
let status = resp.status();
|
||||
@@ -140,6 +141,7 @@ impl HttpTransport for ReqwestTransport {
|
||||
let body = String::from_utf8(bytes.to_vec()).ok();
|
||||
return Err(TransportError::Http {
|
||||
status,
|
||||
url: Some(url),
|
||||
headers: Some(headers),
|
||||
body,
|
||||
});
|
||||
@@ -161,6 +163,7 @@ impl HttpTransport for ReqwestTransport {
|
||||
);
|
||||
}
|
||||
|
||||
let url = req.url.clone();
|
||||
let builder = self.build(req)?;
|
||||
let resp = builder.send().await.map_err(Self::map_error)?;
|
||||
let status = resp.status();
|
||||
@@ -169,6 +172,7 @@ impl HttpTransport for ReqwestTransport {
|
||||
let body = resp.text().await.ok();
|
||||
return Err(TransportError::Http {
|
||||
status,
|
||||
url: Some(url),
|
||||
headers: Some(headers),
|
||||
body,
|
||||
});
|
||||
|
||||
11
codex-rs/common/BUILD.bazel
Normal file
11
codex-rs/common/BUILD.bazel
Normal file
@@ -0,0 +1,11 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "common",
|
||||
crate_name = "codex_common",
|
||||
crate_features = [
|
||||
"cli",
|
||||
"elapsed",
|
||||
"sandbox_summary",
|
||||
],
|
||||
)
|
||||
40
codex-rs/core/BUILD.bazel
Normal file
40
codex-rs/core/BUILD.bazel
Normal file
@@ -0,0 +1,40 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "core",
|
||||
crate_name = "codex_core",
|
||||
# TODO(mbolin): Eliminate the use of features in the version of the
|
||||
# rust_library() that is used by rust_binary() rules for release artifacts
|
||||
# such as the Codex CLI.
|
||||
crate_features = ["deterministic_process_ids", "test-support"],
|
||||
compile_data = glob(
|
||||
include = ["**"],
|
||||
exclude = [
|
||||
"**/* *",
|
||||
"BUILD.bazel",
|
||||
"Cargo.toml",
|
||||
],
|
||||
allow_empty = True,
|
||||
),
|
||||
integration_compile_data_extra = [
|
||||
"//codex-rs/apply-patch:apply_patch_tool_instructions.md",
|
||||
"prompt.md",
|
||||
],
|
||||
# This is a bit of a hack, but empirically, some of our integration tests
|
||||
# are relying on the presence of this file as a repo root marker. When
|
||||
# running tests locally, this "just works," but in remote execution,
|
||||
# the working directory is different and so the file is not found unless it
|
||||
# is explicitly added as test data.
|
||||
#
|
||||
# TODO(aibrahim): Update the tests so that `just bazel-remote-test` succeeds
|
||||
# without this workaround.
|
||||
test_data_extra = ["//:AGENTS.md"],
|
||||
integration_deps_extra = ["//codex-rs/core/tests/common:common"],
|
||||
test_tags = ["no-sandbox"],
|
||||
extra_binaries = [
|
||||
"//codex-rs/linux-sandbox:codex-linux-sandbox",
|
||||
"//codex-rs/rmcp-client:test_stdio_server",
|
||||
"//codex-rs/rmcp-client:test_streamable_http_server",
|
||||
"//codex-rs/cli:codex",
|
||||
],
|
||||
)
|
||||
@@ -9,6 +9,10 @@ doctest = false
|
||||
name = "codex_core"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "codex-write-config-schema"
|
||||
path = "src/bin/config_schema.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -58,6 +62,7 @@ reqwest = { workspace = true, features = ["json", "stream"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
sha1 = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
shlex = { workspace = true }
|
||||
|
||||
1821
codex-rs/core/config.schema.json
Normal file
1821
codex-rs/core/config.schema.json
Normal file
File diff suppressed because it is too large
Load Diff
7
codex-rs/core/hierarchical_agents_message.md
Normal file
7
codex-rs/core/hierarchical_agents_message.md
Normal file
@@ -0,0 +1,7 @@
|
||||
Files called AGENTS.md commonly appear in many places inside a container - at "/", in "~", deep within git repositories, or in any other directory; their location is not limited to version-controlled folders.
|
||||
|
||||
Their purpose is to pass along human guidance to you, the agent. Such guidance can include coding standards, explanations of the project layout, steps for building or testing, and even wording that must accompany a GitHub pull-request description produced by the agent; all of it is to be followed.
|
||||
|
||||
Each AGENTS.md governs the entire directory that contains it and every child directory beneath that point. Whenever you change a file, you have to comply with every AGENTS.md whose scope covers that file. Naming conventions, stylistic rules and similar directives are restricted to the code that falls inside that scope unless the document explicitly states otherwise.
|
||||
|
||||
When two AGENTS.md files disagree, the one located deeper in the directory structure overrides the higher-level file, while instructions given directly in the prompt by the system, developer, or user outrank any AGENTS.md content.
|
||||
@@ -25,11 +25,13 @@ pub(crate) fn map_api_error(err: ApiError) -> CodexErr {
|
||||
ApiError::Api { status, message } => CodexErr::UnexpectedStatus(UnexpectedResponseError {
|
||||
status,
|
||||
body: message,
|
||||
url: None,
|
||||
request_id: None,
|
||||
}),
|
||||
ApiError::Transport(transport) => match transport {
|
||||
TransportError::Http {
|
||||
status,
|
||||
url,
|
||||
headers,
|
||||
body,
|
||||
} => {
|
||||
@@ -71,6 +73,7 @@ pub(crate) fn map_api_error(err: ApiError) -> CodexErr {
|
||||
CodexErr::UnexpectedStatus(UnexpectedResponseError {
|
||||
status,
|
||||
body: body_text,
|
||||
url,
|
||||
request_id: extract_request_id(headers.as_ref()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use sha2::Digest;
|
||||
@@ -21,7 +22,7 @@ use codex_keyring_store::DefaultKeyringStore;
|
||||
use codex_keyring_store::KeyringStore;
|
||||
|
||||
/// Determine where Codex should store CLI auth credentials.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AuthCredentialsStoreMode {
|
||||
#[default]
|
||||
|
||||
25
codex-rs/core/src/bin/config_schema.rs
Normal file
25
codex-rs/core/src/bin/config_schema.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Generate the JSON Schema for `config.toml` and write it to `config.schema.json`.
|
||||
fn main() -> Result<()> {
|
||||
let mut args = env::args().skip(1);
|
||||
let mut out_path = None;
|
||||
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--out" | "-o" => {
|
||||
let value = args.next().context("expected a path after --out/-o")?;
|
||||
out_path = Some(PathBuf::from(value));
|
||||
}
|
||||
_ => anyhow::bail!("unknown argument: {arg}"),
|
||||
}
|
||||
}
|
||||
|
||||
let out_path = out_path
|
||||
.unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("config.schema.json"));
|
||||
codex_core::config::schema::write_config_schema(&out_path)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -533,6 +533,7 @@ async fn handle_unauthorized(
|
||||
fn map_unauthorized_status(status: StatusCode) -> CodexErr {
|
||||
map_api_error(ApiError::Transport(TransportError::Http {
|
||||
status,
|
||||
url: None,
|
||||
headers: None,
|
||||
body: None,
|
||||
}))
|
||||
|
||||
11
codex-rs/core/src/config/SCHEMA.md
Normal file
11
codex-rs/core/src/config/SCHEMA.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Config JSON Schema
|
||||
|
||||
We generate a JSON Schema for `~/.codex/config.toml` from the `ConfigToml` type
|
||||
and commit it at `codex-rs/core/config.schema.json` for editor integration.
|
||||
|
||||
When you change any fields included in `ConfigToml` (or nested config types),
|
||||
regenerate the schema:
|
||||
|
||||
```
|
||||
just write-config-schema
|
||||
```
|
||||
@@ -32,6 +32,7 @@ use crate::protocol::AskForApproval;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use codex_app_server_protocol::Tools;
|
||||
use codex_app_server_protocol::UserSavedConfig;
|
||||
use codex_protocol::config_types::AltScreenMode;
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
@@ -42,6 +43,7 @@ use codex_rmcp_client::OAuthCredentialsStoreMode;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::AbsolutePathBufGuard;
|
||||
use dirs::home_dir;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use similar::DiffableStr;
|
||||
@@ -60,6 +62,7 @@ use toml_edit::DocumentMut;
|
||||
mod constraint;
|
||||
pub mod edit;
|
||||
pub mod profile;
|
||||
pub mod schema;
|
||||
pub mod service;
|
||||
pub mod types;
|
||||
pub use constraint::Constrained;
|
||||
@@ -236,6 +239,14 @@ pub struct Config {
|
||||
/// consistently to both mouse wheels and trackpads.
|
||||
pub tui_scroll_invert: bool,
|
||||
|
||||
/// Controls whether the TUI uses the terminal's alternate screen buffer.
|
||||
///
|
||||
/// This is the same `tui.alternate_screen` value from `config.toml` (see [`Tui`]).
|
||||
/// - `auto` (default): Disable alternate screen in Zellij, enable elsewhere.
|
||||
/// - `always`: Always use alternate screen (original behavior).
|
||||
/// - `never`: Never use alternate screen (inline mode, preserves scrollback).
|
||||
pub tui_alternate_screen: AltScreenMode,
|
||||
|
||||
/// The directory that should be treated as the current working directory
|
||||
/// for the session. All relative paths inside the business-logic layer are
|
||||
/// resolved against this path.
|
||||
@@ -673,7 +684,8 @@ pub fn set_default_oss_provider(codex_home: &Path, provider: &str) -> std::io::R
|
||||
}
|
||||
|
||||
/// Base config deserialized from ~/.codex/config.toml.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ConfigToml {
|
||||
/// Optional override of model selection.
|
||||
pub model: Option<String>,
|
||||
@@ -732,6 +744,8 @@ pub struct ConfigToml {
|
||||
|
||||
/// Definition for MCP servers that Codex can reach out to for tool calls.
|
||||
#[serde(default)]
|
||||
// Uses the raw MCP input shape (custom deserialization) rather than `McpServerConfig`.
|
||||
#[schemars(schema_with = "crate::config::schema::mcp_servers_schema")]
|
||||
pub mcp_servers: HashMap<String, McpServerConfig>,
|
||||
|
||||
/// Preferred backend for storing MCP OAuth credentials.
|
||||
@@ -799,6 +813,8 @@ pub struct ConfigToml {
|
||||
|
||||
/// Centralized feature flags (new). Prefer this over individual toggles.
|
||||
#[serde(default)]
|
||||
// Injects known feature keys into the schema and forbids unknown keys.
|
||||
#[schemars(schema_with = "crate::config::schema::features_schema")]
|
||||
pub features: Option<FeaturesToml>,
|
||||
|
||||
/// Settings for ghost snapshots (used for undo).
|
||||
@@ -872,7 +888,8 @@ impl From<ConfigToml> for UserSavedConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ProjectConfig {
|
||||
pub trust_level: Option<TrustLevel>,
|
||||
}
|
||||
@@ -887,7 +904,8 @@ impl ProjectConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ToolsToml {
|
||||
#[serde(default, alias = "web_search_request")]
|
||||
pub web_search: Option<bool>,
|
||||
@@ -906,7 +924,8 @@ impl From<ToolsToml> for Tools {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct GhostSnapshotToml {
|
||||
/// Exclude untracked files larger than this many bytes from ghost snapshots.
|
||||
#[serde(alias = "ignore_untracked_files_over_bytes")]
|
||||
@@ -1443,6 +1462,11 @@ impl Config {
|
||||
.as_ref()
|
||||
.and_then(|t| t.scroll_wheel_like_max_duration_ms),
|
||||
tui_scroll_invert: cfg.tui.as_ref().map(|t| t.scroll_invert).unwrap_or(false),
|
||||
tui_alternate_screen: cfg
|
||||
.tui
|
||||
.as_ref()
|
||||
.map(|t| t.alternate_screen)
|
||||
.unwrap_or_default(),
|
||||
otel: {
|
||||
let t: OtelConfigToml = cfg.otel.unwrap_or_default();
|
||||
let log_user_prompt = t.log_user_prompt.unwrap_or(false);
|
||||
@@ -1641,6 +1665,7 @@ persistence = "none"
|
||||
scroll_wheel_tick_detect_max_ms: None,
|
||||
scroll_wheel_like_max_duration_ms: None,
|
||||
scroll_invert: false,
|
||||
alternate_screen: AltScreenMode::Auto,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -3276,6 +3301,7 @@ model_verbosity = "high"
|
||||
tui_scroll_wheel_tick_detect_max_ms: None,
|
||||
tui_scroll_wheel_like_max_duration_ms: None,
|
||||
tui_scroll_invert: false,
|
||||
tui_alternate_screen: AltScreenMode::Auto,
|
||||
otel: OtelConfig::default(),
|
||||
},
|
||||
o3_profile_config
|
||||
@@ -3361,6 +3387,7 @@ model_verbosity = "high"
|
||||
tui_scroll_wheel_tick_detect_max_ms: None,
|
||||
tui_scroll_wheel_like_max_duration_ms: None,
|
||||
tui_scroll_invert: false,
|
||||
tui_alternate_screen: AltScreenMode::Auto,
|
||||
otel: OtelConfig::default(),
|
||||
};
|
||||
|
||||
@@ -3461,6 +3488,7 @@ model_verbosity = "high"
|
||||
tui_scroll_wheel_tick_detect_max_ms: None,
|
||||
tui_scroll_wheel_like_max_duration_ms: None,
|
||||
tui_scroll_invert: false,
|
||||
tui_alternate_screen: AltScreenMode::Auto,
|
||||
otel: OtelConfig::default(),
|
||||
};
|
||||
|
||||
@@ -3547,6 +3575,7 @@ model_verbosity = "high"
|
||||
tui_scroll_wheel_tick_detect_max_ms: None,
|
||||
tui_scroll_wheel_like_max_duration_ms: None,
|
||||
tui_scroll_invert: false,
|
||||
tui_alternate_screen: AltScreenMode::Auto,
|
||||
otel: OtelConfig::default(),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -10,7 +11,8 @@ use codex_protocol::openai_models::ReasoningEffort;
|
||||
|
||||
/// Collection of common configuration options that a user can define as a unit
|
||||
/// in `config.toml`.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ConfigProfile {
|
||||
pub model: Option<String>,
|
||||
/// The key in the `model_providers` map identifying the
|
||||
@@ -32,6 +34,8 @@ pub struct ConfigProfile {
|
||||
pub analytics: Option<crate::config::types::AnalyticsConfigToml>,
|
||||
/// Optional feature toggles scoped to this profile.
|
||||
#[serde(default)]
|
||||
// Injects known feature keys into the schema and forbids unknown keys.
|
||||
#[schemars(schema_with = "crate::config::schema::features_schema")]
|
||||
pub features: Option<crate::features::FeaturesToml>,
|
||||
pub oss_provider: Option<String>,
|
||||
}
|
||||
|
||||
87
codex-rs/core/src/config/schema.rs
Normal file
87
codex-rs/core/src/config/schema.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use crate::config::ConfigToml;
|
||||
use crate::config::types::RawMcpServerConfig;
|
||||
use crate::features::FEATURES;
|
||||
use schemars::Schema;
|
||||
use schemars::SchemaGenerator;
|
||||
use schemars::generate::SchemaSettings;
|
||||
use serde_json::Map;
|
||||
use serde_json::Value;
|
||||
use std::path::Path;
|
||||
|
||||
/// Schema for the `[features]` map with known + legacy keys only.
|
||||
pub(crate) fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema {
|
||||
let mut properties = Map::new();
|
||||
for feature in FEATURES {
|
||||
properties.insert(
|
||||
feature.key.to_string(),
|
||||
schema_gen.subschema_for::<bool>().to_value(),
|
||||
);
|
||||
}
|
||||
for legacy_key in crate::features::legacy_feature_keys() {
|
||||
properties.insert(
|
||||
legacy_key.to_string(),
|
||||
schema_gen.subschema_for::<bool>().to_value(),
|
||||
);
|
||||
}
|
||||
let mut schema = Map::new();
|
||||
schema.insert("type".into(), Value::String("object".into()));
|
||||
schema.insert("properties".into(), Value::Object(properties));
|
||||
schema.insert("additionalProperties".into(), Value::Bool(false));
|
||||
schema.into()
|
||||
}
|
||||
|
||||
/// Schema for the `[mcp_servers]` map using the raw input shape.
|
||||
pub(crate) fn mcp_servers_schema(schema_gen: &mut SchemaGenerator) -> Schema {
|
||||
let mut schema = Map::new();
|
||||
schema.insert("type".into(), Value::String("object".into()));
|
||||
schema.insert(
|
||||
"additionalProperties".into(),
|
||||
schema_gen.subschema_for::<RawMcpServerConfig>().to_value(),
|
||||
);
|
||||
schema.into()
|
||||
}
|
||||
|
||||
/// Build the config schema for `config.toml`.
|
||||
pub fn config_schema() -> Schema {
|
||||
SchemaSettings::draft07()
|
||||
.into_generator()
|
||||
.into_root_schema_for::<ConfigToml>()
|
||||
}
|
||||
|
||||
/// Render the config schema as pretty-printed JSON.
|
||||
pub fn config_schema_json() -> anyhow::Result<Vec<u8>> {
|
||||
let schema = config_schema();
|
||||
let json = serde_json::to_vec_pretty(&schema)?;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
/// Write the config schema fixture to disk.
|
||||
pub fn write_config_schema(out_path: &Path) -> anyhow::Result<()> {
|
||||
let json = config_schema_json()?;
|
||||
std::fs::write(out_path, json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::config_schema_json;
|
||||
use similar::TextDiff;
|
||||
|
||||
#[test]
|
||||
fn config_schema_matches_fixture() {
|
||||
let fixture_path = codex_utils_cargo_bin::find_resource!("config.schema.json")
|
||||
.expect("resolve config schema fixture path");
|
||||
let fixture = std::fs::read_to_string(fixture_path).expect("read config schema fixture");
|
||||
let schema_json = config_schema_json().expect("serialize config schema");
|
||||
let schema_str = String::from_utf8(schema_json).expect("decode schema json");
|
||||
if fixture != schema_str {
|
||||
let diff = TextDiff::from_lines(&fixture, &schema_str)
|
||||
.unified_diff()
|
||||
.to_string();
|
||||
let short = diff.lines().take(50).collect::<Vec<_>>().join("\n");
|
||||
panic!(
|
||||
"Current schema for `config.toml` doesn't match the fixture. Run `just write-config-schema` to overwrite with your changes.\n\n{short}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
// Note this file should generally be restricted to simple struct/enum
|
||||
// definitions that do not contain business logic.
|
||||
|
||||
pub use codex_protocol::config_types::AltScreenMode;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
@@ -10,6 +11,7 @@ use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use wildmatch::WildMatchPattern;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
@@ -47,47 +49,51 @@ pub struct McpServerConfig {
|
||||
pub disabled_tools: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
// Raw MCP config shape used for deserialization and JSON Schema generation.
|
||||
// Keep this in sync with the validation logic in `McpServerConfig`.
|
||||
#[derive(Deserialize, Clone, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub(crate) struct RawMcpServerConfig {
|
||||
// stdio
|
||||
pub command: Option<String>,
|
||||
#[serde(default)]
|
||||
pub args: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
#[serde(default)]
|
||||
pub env_vars: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub cwd: Option<PathBuf>,
|
||||
pub http_headers: Option<HashMap<String, String>>,
|
||||
#[serde(default)]
|
||||
pub env_http_headers: Option<HashMap<String, String>>,
|
||||
|
||||
// streamable_http
|
||||
pub url: Option<String>,
|
||||
pub bearer_token: Option<String>,
|
||||
pub bearer_token_env_var: Option<String>,
|
||||
|
||||
// shared
|
||||
#[serde(default)]
|
||||
pub startup_timeout_sec: Option<f64>,
|
||||
#[serde(default)]
|
||||
pub startup_timeout_ms: Option<u64>,
|
||||
#[serde(default, with = "option_duration_secs")]
|
||||
#[schemars(with = "Option<f64>")]
|
||||
pub tool_timeout_sec: Option<Duration>,
|
||||
#[serde(default)]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub enabled_tools: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub disabled_tools: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for McpServerConfig {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct RawMcpServerConfig {
|
||||
// stdio
|
||||
command: Option<String>,
|
||||
#[serde(default)]
|
||||
args: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
env: Option<HashMap<String, String>>,
|
||||
#[serde(default)]
|
||||
env_vars: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
cwd: Option<PathBuf>,
|
||||
http_headers: Option<HashMap<String, String>>,
|
||||
#[serde(default)]
|
||||
env_http_headers: Option<HashMap<String, String>>,
|
||||
|
||||
// streamable_http
|
||||
url: Option<String>,
|
||||
bearer_token: Option<String>,
|
||||
bearer_token_env_var: Option<String>,
|
||||
|
||||
// shared
|
||||
#[serde(default)]
|
||||
startup_timeout_sec: Option<f64>,
|
||||
#[serde(default)]
|
||||
startup_timeout_ms: Option<u64>,
|
||||
#[serde(default, with = "option_duration_secs")]
|
||||
tool_timeout_sec: Option<Duration>,
|
||||
#[serde(default)]
|
||||
enabled: Option<bool>,
|
||||
#[serde(default)]
|
||||
enabled_tools: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
disabled_tools: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
let mut raw = RawMcpServerConfig::deserialize(deserializer)?;
|
||||
|
||||
let startup_timeout_sec = match (raw.startup_timeout_sec, raw.startup_timeout_ms) {
|
||||
@@ -163,7 +169,7 @@ const fn default_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)]
|
||||
#[serde(untagged, deny_unknown_fields, rename_all = "snake_case")]
|
||||
pub enum McpServerTransportConfig {
|
||||
/// https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio
|
||||
@@ -221,7 +227,7 @@ mod option_duration_secs {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, JsonSchema)]
|
||||
pub enum UriBasedFileOpener {
|
||||
#[serde(rename = "vscode")]
|
||||
VsCode,
|
||||
@@ -253,7 +259,8 @@ impl UriBasedFileOpener {
|
||||
}
|
||||
|
||||
/// Settings that govern if and what will be written to `~/.codex/history.jsonl`.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct History {
|
||||
/// If true, history entries will not be written to disk.
|
||||
pub persistence: HistoryPersistence,
|
||||
@@ -263,7 +270,7 @@ pub struct History {
|
||||
pub max_bytes: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Default)]
|
||||
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum HistoryPersistence {
|
||||
/// Save all history entries to disk.
|
||||
@@ -276,13 +283,15 @@ pub enum HistoryPersistence {
|
||||
// ===== Analytics configuration =====
|
||||
|
||||
/// Analytics settings loaded from config.toml. Fields are optional so we can apply defaults.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct AnalyticsConfigToml {
|
||||
/// When `false`, disables analytics across Codex product surfaces in this profile.
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct FeedbackConfigToml {
|
||||
/// When `false`, disables the feedback flow across Codex product surfaces.
|
||||
pub enabled: Option<bool>,
|
||||
@@ -290,7 +299,7 @@ pub struct FeedbackConfigToml {
|
||||
|
||||
// ===== OTEL configuration =====
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum OtelHttpProtocol {
|
||||
/// Binary payload
|
||||
@@ -299,7 +308,8 @@ pub enum OtelHttpProtocol {
|
||||
Json,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct OtelTlsConfig {
|
||||
pub ca_certificate: Option<AbsolutePathBuf>,
|
||||
@@ -308,7 +318,8 @@ pub struct OtelTlsConfig {
|
||||
}
|
||||
|
||||
/// Which OTEL exporter to use.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum OtelExporterKind {
|
||||
None,
|
||||
@@ -331,7 +342,8 @@ pub enum OtelExporterKind {
|
||||
}
|
||||
|
||||
/// OTEL settings loaded from config.toml. Fields are optional so we can apply defaults.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct OtelConfigToml {
|
||||
/// Log user prompt in traces
|
||||
pub log_user_prompt: Option<bool>,
|
||||
@@ -368,7 +380,7 @@ impl Default for OtelConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[derive(Serialize, Debug, Clone, PartialEq, Eq, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum Notifications {
|
||||
Enabled(bool),
|
||||
@@ -386,7 +398,7 @@ impl Default for Notifications {
|
||||
/// Terminals generally encode both mouse wheels and trackpads as the same "scroll up/down" mouse
|
||||
/// button events, without a magnitude. This setting controls whether Codex uses a heuristic to
|
||||
/// infer wheel vs trackpad per stream, or forces a specific behavior.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ScrollInputMode {
|
||||
/// Infer wheel vs trackpad behavior per scroll stream.
|
||||
@@ -404,7 +416,8 @@ impl Default for ScrollInputMode {
|
||||
}
|
||||
|
||||
/// Collection of settings that are specific to the TUI.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct Tui {
|
||||
/// Enable desktop notifications from the TUI when the terminal is unfocused.
|
||||
/// Defaults to `true`.
|
||||
@@ -523,6 +536,17 @@ pub struct Tui {
|
||||
/// wheel and trackpad input.
|
||||
#[serde(default)]
|
||||
pub scroll_invert: bool,
|
||||
|
||||
/// Controls whether the TUI uses the terminal's alternate screen buffer.
|
||||
///
|
||||
/// - `auto` (default): Disable alternate screen in Zellij, enable elsewhere.
|
||||
/// - `always`: Always use alternate screen (original behavior).
|
||||
/// - `never`: Never use alternate screen (inline mode only, preserves scrollback).
|
||||
///
|
||||
/// Using alternate screen provides a cleaner fullscreen experience but prevents
|
||||
/// scrollback in terminal multiplexers like Zellij that follow the xterm spec.
|
||||
#[serde(default)]
|
||||
pub alternate_screen: AltScreenMode,
|
||||
}
|
||||
|
||||
const fn default_true() -> bool {
|
||||
@@ -532,7 +556,8 @@ const fn default_true() -> bool {
|
||||
/// Settings for notices we display to users via the tui and app-server clients
|
||||
/// (primarily the Codex IDE extension). NOTE: these are different from
|
||||
/// notifications - notices are warnings, NUX screens, acknowledgements, etc.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct Notice {
|
||||
/// Tracks whether the user has acknowledged the full access warning prompt.
|
||||
pub hide_full_access_warning: Option<bool>,
|
||||
@@ -555,7 +580,8 @@ impl Notice {
|
||||
pub(crate) const TABLE_KEY: &'static str = "notice";
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct SandboxWorkspaceWrite {
|
||||
#[serde(default)]
|
||||
pub writable_roots: Vec<AbsolutePathBuf>,
|
||||
@@ -578,7 +604,7 @@ impl From<SandboxWorkspaceWrite> for codex_app_server_protocol::SandboxSettings
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum ShellEnvironmentPolicyInherit {
|
||||
/// "Core" environment variables for the platform. On UNIX, this would
|
||||
@@ -595,7 +621,8 @@ pub enum ShellEnvironmentPolicyInherit {
|
||||
|
||||
/// Policy for building the `env` when spawning a process via either the
|
||||
/// `shell` or `local_shell` tool.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ShellEnvironmentPolicyToml {
|
||||
pub inherit: Option<ShellEnvironmentPolicyInherit>,
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ pub use codex_client::CodexRequestBuilder;
|
||||
use reqwest::header::HeaderValue;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::RwLock;
|
||||
|
||||
/// Set this to add a suffix to the User-Agent string.
|
||||
///
|
||||
@@ -30,7 +30,7 @@ pub struct Originator {
|
||||
pub value: String,
|
||||
pub header_value: HeaderValue,
|
||||
}
|
||||
static ORIGINATOR: OnceLock<Originator> = OnceLock::new();
|
||||
static ORIGINATOR: LazyLock<RwLock<Option<Originator>>> = LazyLock::new(|| RwLock::new(None));
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SetOriginatorError {
|
||||
@@ -60,22 +60,48 @@ fn get_originator_value(provided: Option<String>) -> Originator {
|
||||
}
|
||||
|
||||
pub fn set_default_originator(value: String) -> Result<(), SetOriginatorError> {
|
||||
if HeaderValue::from_str(&value).is_err() {
|
||||
return Err(SetOriginatorError::InvalidHeaderValue);
|
||||
}
|
||||
let originator = get_originator_value(Some(value));
|
||||
ORIGINATOR
|
||||
.set(originator)
|
||||
.map_err(|_| SetOriginatorError::AlreadyInitialized)
|
||||
let Ok(mut guard) = ORIGINATOR.write() else {
|
||||
return Err(SetOriginatorError::AlreadyInitialized);
|
||||
};
|
||||
if guard.is_some() {
|
||||
return Err(SetOriginatorError::AlreadyInitialized);
|
||||
}
|
||||
*guard = Some(originator);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn originator() -> &'static Originator {
|
||||
ORIGINATOR.get_or_init(|| get_originator_value(None))
|
||||
pub fn originator() -> Originator {
|
||||
if let Ok(guard) = ORIGINATOR.read()
|
||||
&& let Some(originator) = guard.as_ref()
|
||||
{
|
||||
return originator.clone();
|
||||
}
|
||||
|
||||
if std::env::var(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR).is_ok() {
|
||||
let originator = get_originator_value(None);
|
||||
if let Ok(mut guard) = ORIGINATOR.write() {
|
||||
match guard.as_ref() {
|
||||
Some(originator) => return originator.clone(),
|
||||
None => *guard = Some(originator.clone()),
|
||||
}
|
||||
}
|
||||
return originator;
|
||||
}
|
||||
|
||||
get_originator_value(None)
|
||||
}
|
||||
|
||||
pub fn get_codex_user_agent() -> String {
|
||||
let build_version = env!("CARGO_PKG_VERSION");
|
||||
let os_info = os_info::get();
|
||||
let originator = originator();
|
||||
let prefix = format!(
|
||||
"{}/{build_version} ({} {}; {}) {}",
|
||||
originator().value.as_str(),
|
||||
originator.value.as_str(),
|
||||
os_info.os_type(),
|
||||
os_info.version(),
|
||||
os_info.architecture().unwrap_or("unknown"),
|
||||
@@ -123,7 +149,7 @@ fn sanitize_user_agent(candidate: String, fallback: &str) -> String {
|
||||
tracing::warn!(
|
||||
"Falling back to default Codex originator because base user agent string is invalid"
|
||||
);
|
||||
originator().value.clone()
|
||||
originator().value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +163,7 @@ pub fn build_reqwest_client() -> reqwest::Client {
|
||||
use reqwest::header::HeaderMap;
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("originator", originator().header_value.clone());
|
||||
headers.insert("originator", originator().header_value);
|
||||
let ua = get_codex_user_agent();
|
||||
|
||||
let mut builder = reqwest::Client::builder()
|
||||
@@ -163,7 +189,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_get_codex_user_agent() {
|
||||
let user_agent = get_codex_user_agent();
|
||||
let originator = originator().value.as_str();
|
||||
let originator = originator().value;
|
||||
let prefix = format!("{originator}/");
|
||||
assert!(user_agent.starts_with(&prefix));
|
||||
}
|
||||
|
||||
@@ -277,6 +277,7 @@ pub enum RefreshTokenFailedReason {
|
||||
pub struct UnexpectedResponseError {
|
||||
pub status: StatusCode,
|
||||
pub body: String,
|
||||
pub url: Option<String>,
|
||||
pub request_id: Option<String>,
|
||||
}
|
||||
|
||||
@@ -293,7 +294,11 @@ impl UnexpectedResponseError {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut message = format!("{CLOUDFLARE_BLOCKED_MESSAGE} (status {})", self.status);
|
||||
let status = self.status;
|
||||
let mut message = format!("{CLOUDFLARE_BLOCKED_MESSAGE} (status {status})");
|
||||
if let Some(url) = &self.url {
|
||||
message.push_str(&format!(", url: {url}"));
|
||||
}
|
||||
if let Some(id) = &self.request_id {
|
||||
message.push_str(&format!(", request id: {id}"));
|
||||
}
|
||||
@@ -307,16 +312,16 @@ impl std::fmt::Display for UnexpectedResponseError {
|
||||
if let Some(friendly) = self.friendly_message() {
|
||||
write!(f, "{friendly}")
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"unexpected status {}: {}{}",
|
||||
self.status,
|
||||
self.body,
|
||||
self.request_id
|
||||
.as_ref()
|
||||
.map(|id| format!(", request id: {id}"))
|
||||
.unwrap_or_default()
|
||||
)
|
||||
let status = self.status;
|
||||
let body = &self.body;
|
||||
let mut message = format!("unexpected status {status}: {body}");
|
||||
if let Some(url) = &self.url {
|
||||
message.push_str(&format!(", url: {url}"));
|
||||
}
|
||||
if let Some(id) = &self.request_id {
|
||||
message.push_str(&format!(", request id: {id}"));
|
||||
}
|
||||
write!(f, "{message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -826,12 +831,16 @@ mod tests {
|
||||
status: StatusCode::FORBIDDEN,
|
||||
body: "<html><body>Cloudflare error: Sorry, you have been blocked</body></html>"
|
||||
.to_string(),
|
||||
url: Some("http://example.com/blocked".to_string()),
|
||||
request_id: Some("ray-id".to_string()),
|
||||
};
|
||||
let status = StatusCode::FORBIDDEN.to_string();
|
||||
let url = "http://example.com/blocked";
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
format!("{CLOUDFLARE_BLOCKED_MESSAGE} (status {status}), request id: ray-id")
|
||||
format!(
|
||||
"{CLOUDFLARE_BLOCKED_MESSAGE} (status {status}), url: {url}, request id: ray-id"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -840,12 +849,14 @@ mod tests {
|
||||
let err = UnexpectedResponseError {
|
||||
status: StatusCode::FORBIDDEN,
|
||||
body: "plain text error".to_string(),
|
||||
url: Some("http://example.com/plain".to_string()),
|
||||
request_id: None,
|
||||
};
|
||||
let status = StatusCode::FORBIDDEN.to_string();
|
||||
let url = "http://example.com/plain";
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
format!("unexpected status {status}: plain text error")
|
||||
format!("unexpected status {status}: plain text error, url: {url}")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
use crate::config::ConfigToml;
|
||||
use crate::config::profile::ConfigProfile;
|
||||
use codex_otel::OtelManager;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -15,6 +16,7 @@ use std::collections::BTreeSet;
|
||||
|
||||
mod legacy;
|
||||
pub(crate) use legacy::LegacyFeatureToggles;
|
||||
pub(crate) use legacy::legacy_feature_keys;
|
||||
|
||||
/// High-level lifecycle stage for a feature.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -86,6 +88,8 @@ pub enum Feature {
|
||||
RemoteModels,
|
||||
/// Experimental shell snapshotting.
|
||||
ShellSnapshot,
|
||||
/// Append additional AGENTS.md guidance to user instructions.
|
||||
HierarchicalAgents,
|
||||
/// Experimental TUI v2 (viewport) implementation.
|
||||
Tui2,
|
||||
/// Enforce UTF8 output in Powershell.
|
||||
@@ -290,7 +294,7 @@ pub fn is_known_feature_key(key: &str) -> bool {
|
||||
}
|
||||
|
||||
/// Deserializable features table for TOML.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
|
||||
pub struct FeaturesToml {
|
||||
#[serde(flatten)]
|
||||
pub entries: BTreeMap<String, bool>,
|
||||
@@ -352,6 +356,12 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
},
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::HierarchicalAgents,
|
||||
key: "hierarchical_agents",
|
||||
stage: Stage::Experimental,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::ApplyPatchFreeform,
|
||||
key: "apply_patch_freeform",
|
||||
|
||||
@@ -31,6 +31,10 @@ const ALIASES: &[Alias] = &[
|
||||
},
|
||||
];
|
||||
|
||||
pub(crate) fn legacy_feature_keys() -> impl Iterator<Item = &'static str> {
|
||||
ALIASES.iter().map(|alias| alias.legacy_key)
|
||||
}
|
||||
|
||||
pub(crate) fn feature_for_key(key: &str) -> Option<Feature> {
|
||||
ALIASES
|
||||
.iter()
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_app_server_protocol::AuthMode;
|
||||
use http::HeaderMap;
|
||||
use http::header::HeaderName;
|
||||
use http::header::HeaderValue;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
@@ -36,7 +37,7 @@ const OPENAI_PROVIDER_NAME: &str = "OpenAI";
|
||||
/// *Responses* API. The two protocols use different request/response shapes
|
||||
/// and *cannot* be auto-detected at runtime, therefore each provider entry
|
||||
/// must declare which one it expects.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum WireApi {
|
||||
/// The Responses API exposed by OpenAI at `/v1/responses`.
|
||||
@@ -48,7 +49,8 @@ pub enum WireApi {
|
||||
}
|
||||
|
||||
/// Serializable representation of a provider definition.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ModelProviderInfo {
|
||||
/// Friendly display name.
|
||||
pub name: String,
|
||||
|
||||
@@ -15,6 +15,7 @@ use std::error::Error;
|
||||
pub fn build_provider(
|
||||
config: &Config,
|
||||
service_version: &str,
|
||||
service_name_override: Option<&str>,
|
||||
default_analytics_enabled: bool,
|
||||
) -> Result<Option<OtelProvider>, Box<dyn Error>> {
|
||||
let to_otel_exporter = |kind: &Kind| match kind {
|
||||
@@ -74,8 +75,11 @@ pub fn build_provider(
|
||||
OtelExporter::None
|
||||
};
|
||||
|
||||
let originator = originator();
|
||||
let service_name = service_name_override.unwrap_or(originator.value.as_str());
|
||||
|
||||
OtelProvider::from(&OtelSettings {
|
||||
service_name: originator().value.to_owned(),
|
||||
service_name: service_name.to_string(),
|
||||
service_version: service_version.to_string(),
|
||||
codex_home: config.codex_home.clone(),
|
||||
environment: config.otel.environment.to_string(),
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
//! 3. We do **not** walk past the Git root.
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::features::Feature;
|
||||
use crate::skills::SkillMetadata;
|
||||
use crate::skills::render_skills_section;
|
||||
use dunce::canonicalize as normalize_path;
|
||||
@@ -21,6 +22,9 @@ use std::path::PathBuf;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tracing::error;
|
||||
|
||||
pub(crate) const HIERARCHICAL_AGENTS_MESSAGE: &str =
|
||||
include_str!("../hierarchical_agents_message.md");
|
||||
|
||||
/// Default filename scanned for project-level docs.
|
||||
pub const DEFAULT_PROJECT_DOC_FILENAME: &str = "AGENTS.md";
|
||||
/// Preferred local override for project-level docs.
|
||||
@@ -36,35 +40,46 @@ pub(crate) async fn get_user_instructions(
|
||||
config: &Config,
|
||||
skills: Option<&[SkillMetadata]>,
|
||||
) -> Option<String> {
|
||||
let skills_section = skills.and_then(render_skills_section);
|
||||
let project_docs = read_project_docs(config).await;
|
||||
|
||||
let project_docs = match read_project_docs(config).await {
|
||||
Ok(docs) => docs,
|
||||
let mut output = String::new();
|
||||
|
||||
if let Some(instructions) = config.user_instructions.clone() {
|
||||
output.push_str(&instructions);
|
||||
}
|
||||
|
||||
match project_docs {
|
||||
Ok(Some(docs)) => {
|
||||
if !output.is_empty() {
|
||||
output.push_str(PROJECT_DOC_SEPARATOR);
|
||||
}
|
||||
output.push_str(&docs);
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
error!("error trying to find project doc: {e:#}");
|
||||
return config.user_instructions.clone();
|
||||
}
|
||||
};
|
||||
|
||||
let combined_project_docs = merge_project_docs_with_skills(project_docs, skills_section);
|
||||
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
|
||||
if let Some(instructions) = config.user_instructions.clone() {
|
||||
parts.push(instructions);
|
||||
}
|
||||
|
||||
if let Some(project_doc) = combined_project_docs {
|
||||
if !parts.is_empty() {
|
||||
parts.push(PROJECT_DOC_SEPARATOR.to_string());
|
||||
let skills_section = skills.and_then(render_skills_section);
|
||||
if let Some(skills_section) = skills_section {
|
||||
if !output.is_empty() {
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
parts.push(project_doc);
|
||||
output.push_str(&skills_section);
|
||||
}
|
||||
|
||||
if parts.is_empty() {
|
||||
None
|
||||
if config.features.enabled(Feature::HierarchicalAgents) {
|
||||
if !output.is_empty() {
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
output.push_str(HIERARCHICAL_AGENTS_MESSAGE);
|
||||
}
|
||||
|
||||
if !output.is_empty() {
|
||||
Some(output)
|
||||
} else {
|
||||
Some(parts.concat())
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,18 +232,6 @@ fn candidate_filenames<'a>(config: &'a Config) -> Vec<&'a str> {
|
||||
names
|
||||
}
|
||||
|
||||
fn merge_project_docs_with_skills(
|
||||
project_doc: Option<String>,
|
||||
skills_section: Option<String>,
|
||||
) -> Option<String> {
|
||||
match (project_doc, skills_section) {
|
||||
(Some(doc), Some(skills)) => Some(format!("{doc}\n\n{skills}")),
|
||||
(Some(doc), None) => Some(doc),
|
||||
(None, Some(skills)) => Some(skills),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -143,7 +143,7 @@ impl RolloutRecorder {
|
||||
id: session_id,
|
||||
timestamp,
|
||||
cwd: config.cwd.clone(),
|
||||
originator: originator().value.clone(),
|
||||
originator: originator().value,
|
||||
cli_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
instructions,
|
||||
source,
|
||||
|
||||
7
codex-rs/core/tests/common/BUILD.bazel
Normal file
7
codex-rs/core/tests/common/BUILD.bazel
Normal file
@@ -0,0 +1,7 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "common",
|
||||
crate_name = "core_test_support",
|
||||
crate_srcs = glob(["*.rs"]),
|
||||
)
|
||||
71
codex-rs/core/tests/suite/hierarchical_agents.rs
Normal file
71
codex-rs/core/tests/suite/hierarchical_agents.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use codex_core::features::Feature;
|
||||
use core_test_support::load_sse_fixture_with_id;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
|
||||
const HIERARCHICAL_AGENTS_SNIPPET: &str =
|
||||
"Files called AGENTS.md commonly appear in many places inside a container";
|
||||
|
||||
fn sse_completed(id: &str) -> String {
|
||||
load_sse_fixture_with_id("../fixtures/completed_template.json", id)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn hierarchical_agents_appends_to_project_doc_in_user_instructions() {
|
||||
let server = start_mock_server().await;
|
||||
let resp_mock = mount_sse_once(&server, sse_completed("resp1")).await;
|
||||
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
config.features.enable(Feature::HierarchicalAgents);
|
||||
std::fs::write(config.cwd.join("AGENTS.md"), "be nice").expect("write AGENTS.md");
|
||||
});
|
||||
let test = builder.build(&server).await.expect("build test codex");
|
||||
|
||||
test.submit_turn("hello").await.expect("submit turn");
|
||||
|
||||
let request = resp_mock.single_request();
|
||||
let user_messages = request.message_input_texts("user");
|
||||
let instructions = user_messages
|
||||
.iter()
|
||||
.find(|text| text.starts_with("# AGENTS.md instructions for "))
|
||||
.expect("instructions message");
|
||||
assert!(
|
||||
instructions.contains("be nice"),
|
||||
"expected AGENTS.md text included: {instructions}"
|
||||
);
|
||||
let snippet_pos = instructions
|
||||
.find(HIERARCHICAL_AGENTS_SNIPPET)
|
||||
.expect("expected hierarchical agents snippet");
|
||||
let base_pos = instructions
|
||||
.find("be nice")
|
||||
.expect("expected AGENTS.md text");
|
||||
assert!(
|
||||
snippet_pos > base_pos,
|
||||
"expected hierarchical agents message appended after base instructions: {instructions}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn hierarchical_agents_emits_when_no_project_doc() {
|
||||
let server = start_mock_server().await;
|
||||
let resp_mock = mount_sse_once(&server, sse_completed("resp1")).await;
|
||||
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
config.features.enable(Feature::HierarchicalAgents);
|
||||
});
|
||||
let test = builder.build(&server).await.expect("build test codex");
|
||||
|
||||
test.submit_turn("hello").await.expect("submit turn");
|
||||
|
||||
let request = resp_mock.single_request();
|
||||
let user_messages = request.message_input_texts("user");
|
||||
let instructions = user_messages
|
||||
.iter()
|
||||
.find(|text| text.starts_with("# AGENTS.md instructions for "))
|
||||
.expect("instructions message");
|
||||
assert!(
|
||||
instructions.contains(HIERARCHICAL_AGENTS_SNIPPET),
|
||||
"expected hierarchical agents message appended: {instructions}"
|
||||
);
|
||||
}
|
||||
@@ -30,6 +30,7 @@ mod exec;
|
||||
mod exec_policy;
|
||||
mod fork_thread;
|
||||
mod grep_files;
|
||||
mod hierarchical_agents;
|
||||
mod items;
|
||||
mod json_result;
|
||||
mod list_dir;
|
||||
|
||||
@@ -4,9 +4,7 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_core::CodexAuth;
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::ModelProviderInfo;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::built_in_model_providers;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::error::CodexErr;
|
||||
@@ -39,6 +37,8 @@ use core_test_support::responses::mount_sse_sequence;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::skip_if_sandbox;
|
||||
use core_test_support::test_codex::TestCodex;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use core_test_support::wait_for_event_match;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -98,19 +98,19 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
|
||||
)
|
||||
.await;
|
||||
|
||||
let harness = build_remote_models_harness(&server, |config| {
|
||||
config.features.enable(Feature::RemoteModels);
|
||||
config.model = Some("gpt-5.1".to_string());
|
||||
})
|
||||
.await?;
|
||||
|
||||
let RemoteModelsHarness {
|
||||
let mut builder = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(|config| {
|
||||
config.features.enable(Feature::RemoteModels);
|
||||
config.model = Some("gpt-5.1".to_string());
|
||||
});
|
||||
let TestCodex {
|
||||
codex,
|
||||
cwd,
|
||||
config,
|
||||
thread_manager,
|
||||
..
|
||||
} = harness;
|
||||
} = builder.build(&server).await?;
|
||||
|
||||
let models_manager = thread_manager.get_models_manager();
|
||||
let available_model =
|
||||
@@ -214,16 +214,19 @@ async fn remote_models_truncation_policy_without_override_preserves_remote() ->
|
||||
)
|
||||
.await;
|
||||
|
||||
let harness = build_remote_models_harness(&server, |config| {
|
||||
config.model = Some("gpt-5.1".to_string());
|
||||
})
|
||||
.await?;
|
||||
let mut builder = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(|config| {
|
||||
config.features.enable(Feature::RemoteModels);
|
||||
config.model = Some("gpt-5.1".to_string());
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
let models_manager = harness.thread_manager.get_models_manager();
|
||||
wait_for_model_available(&models_manager, slug, &harness.config).await;
|
||||
let models_manager = test.thread_manager.get_models_manager();
|
||||
wait_for_model_available(&models_manager, slug, &test.config).await;
|
||||
|
||||
let model_info = models_manager
|
||||
.construct_model_info(slug, &harness.config)
|
||||
.construct_model_info(slug, &test.config)
|
||||
.await;
|
||||
assert_eq!(
|
||||
model_info.truncation_policy,
|
||||
@@ -258,17 +261,20 @@ async fn remote_models_truncation_policy_with_tool_output_override() -> Result<(
|
||||
)
|
||||
.await;
|
||||
|
||||
let harness = build_remote_models_harness(&server, |config| {
|
||||
config.model = Some("gpt-5.1".to_string());
|
||||
config.tool_output_token_limit = Some(50);
|
||||
})
|
||||
.await?;
|
||||
let mut builder = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(|config| {
|
||||
config.features.enable(Feature::RemoteModels);
|
||||
config.model = Some("gpt-5.1".to_string());
|
||||
config.tool_output_token_limit = Some(50);
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
let models_manager = harness.thread_manager.get_models_manager();
|
||||
wait_for_model_available(&models_manager, slug, &harness.config).await;
|
||||
let models_manager = test.thread_manager.get_models_manager();
|
||||
wait_for_model_available(&models_manager, slug, &test.config).await;
|
||||
|
||||
let model_info = models_manager
|
||||
.construct_model_info(slug, &harness.config)
|
||||
.construct_model_info(slug, &test.config)
|
||||
.await;
|
||||
assert_eq!(
|
||||
model_info.truncation_policy,
|
||||
@@ -335,19 +341,19 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
|
||||
)
|
||||
.await;
|
||||
|
||||
let harness = build_remote_models_harness(&server, |config| {
|
||||
config.features.enable(Feature::RemoteModels);
|
||||
config.model = Some("gpt-5.1".to_string());
|
||||
})
|
||||
.await?;
|
||||
|
||||
let RemoteModelsHarness {
|
||||
let mut builder = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(|config| {
|
||||
config.features.enable(Feature::RemoteModels);
|
||||
config.model = Some("gpt-5.1".to_string());
|
||||
});
|
||||
let TestCodex {
|
||||
codex,
|
||||
cwd,
|
||||
config,
|
||||
thread_manager,
|
||||
..
|
||||
} = harness;
|
||||
} = builder.build(&server).await?;
|
||||
|
||||
let models_manager = thread_manager.get_models_manager();
|
||||
wait_for_model_available(&models_manager, model, &config).await;
|
||||
@@ -577,50 +583,6 @@ async fn wait_for_model_available(
|
||||
}
|
||||
}
|
||||
|
||||
struct RemoteModelsHarness {
|
||||
codex: Arc<CodexThread>,
|
||||
cwd: Arc<TempDir>,
|
||||
config: Config,
|
||||
thread_manager: Arc<ThreadManager>,
|
||||
}
|
||||
|
||||
// todo(aibrahim): move this to with_model_provier in test_codex
|
||||
async fn build_remote_models_harness<F>(
|
||||
server: &MockServer,
|
||||
mutate_config: F,
|
||||
) -> Result<RemoteModelsHarness>
|
||||
where
|
||||
F: FnOnce(&mut Config),
|
||||
{
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let home = Arc::new(TempDir::new()?);
|
||||
let cwd = Arc::new(TempDir::new()?);
|
||||
|
||||
let mut config = load_default_config_for_test(&home).await;
|
||||
config.cwd = cwd.path().to_path_buf();
|
||||
config.features.enable(Feature::RemoteModels);
|
||||
|
||||
let provider = ModelProviderInfo {
|
||||
base_url: Some(format!("{}/v1", server.uri())),
|
||||
..built_in_model_providers()["openai"].clone()
|
||||
};
|
||||
config.model_provider = provider.clone();
|
||||
|
||||
mutate_config(&mut config);
|
||||
|
||||
let thread_manager = ThreadManager::with_models_provider(auth, provider);
|
||||
let thread_manager = Arc::new(thread_manager);
|
||||
|
||||
let new_conversation = thread_manager.start_thread(config.clone()).await?;
|
||||
|
||||
Ok(RemoteModelsHarness {
|
||||
codex: new_conversation.thread,
|
||||
cwd,
|
||||
config,
|
||||
thread_manager,
|
||||
})
|
||||
}
|
||||
|
||||
fn test_remote_model(slug: &str, visibility: ModelVisibility, priority: i32) -> ModelInfo {
|
||||
test_remote_model_with_policy(
|
||||
slug,
|
||||
|
||||
52
codex-rs/docs/bazel.md
Normal file
52
codex-rs/docs/bazel.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Bazel in codex-rs
|
||||
|
||||
This repository uses Bazel to build the Rust workspace under `codex-rs`.
|
||||
Cargo remains the source of truth for crates and features, while Bazel
|
||||
provides hermetic builds, toolchains, and cross-platform artifacts.
|
||||
|
||||
As of 1/9/2026, this setup is still experimental as we stabilize it.
|
||||
|
||||
## High-level layout
|
||||
|
||||
- `../MODULE.bazel` defines Bazel dependencies and Rust toolchains.
|
||||
- `rules_rs` imports third-party crates from `codex-rs/Cargo.toml` and
|
||||
`codex-rs/Cargo.lock` via `crate.from_cargo(...)` and exposes them under
|
||||
`@crates`.
|
||||
- `../defs.bzl` provides `codex_rust_crate`, which wraps `rust_library`,
|
||||
`rust_binary`, and `rust_test` so Bazel targets line up with Cargo conventions.
|
||||
It provides a sane set of defaults that work for most first-party crates, but may
|
||||
need tweaks in some cases.
|
||||
- Each crate in `codex-rs/*/BUILD.bazel` typically uses `codex_rust_crate` and
|
||||
makes some adjustments if the crate needs additional compile-time or runtime data,
|
||||
or other customizations.
|
||||
|
||||
## Evolving the setup
|
||||
|
||||
When you add or change Rust dependencies, update the Cargo.toml/Cargo.lock as normal.
|
||||
The Bazel build should automatically pick things up without any manual action needed.
|
||||
|
||||
In some cases, an upstream crate may need a patch or a `crate.annotation` in `../MODULE.bzl`
|
||||
to have it build in Bazel's sandbox or make it cross-compilation-friendly. If you see issues,
|
||||
feel free to ping zbarsky or mbolin.
|
||||
|
||||
When you add a new crate or binary:
|
||||
|
||||
1. Add it to the Cargo workspace as usual.
|
||||
2. Create a `BUILD.bazel` that calls `codex_rust_crate` (see nearby crates for
|
||||
examples).
|
||||
3. If a dependency needs special handling (compile/runtime data, additional binaries
|
||||
for integration tests, env vars, etc) you may need to adjust the parameters to
|
||||
`codex_rust_crate` to configure it.
|
||||
One common customization is setting `test_tags = ["no-sandbox]` to run the test
|
||||
unsandboxed. Prefer to avoid it, but it is necessary in some cases such as when the
|
||||
test itself uses Seatbelt (the sandbox does as well, and it cannot be nested).
|
||||
To limit the blast radius, consider isolating such tests to a separate crate.
|
||||
|
||||
If you see build issue and are not sure how to apply the proper customizations, feel free to ping zbarsky or mbolin.
|
||||
|
||||
## References
|
||||
|
||||
- Bazel overview: https://bazel.build/
|
||||
- Bzlmod (module system): https://bazel.build/external/overview
|
||||
- rules_rust: https://github.com/bazelbuild/rules_rust
|
||||
- rules_rs: https://github.com/bazelbuild/rules_rs
|
||||
11
codex-rs/exec-server/BUILD.bazel
Normal file
11
codex-rs/exec-server/BUILD.bazel
Normal file
@@ -0,0 +1,11 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "exec-server",
|
||||
crate_name = "codex_exec_server",
|
||||
integration_deps_extra = ["//codex-rs/exec-server/tests/common:common"],
|
||||
test_tags = ["no-sandbox"],
|
||||
extra_binaries = [
|
||||
"//codex-rs/cli:codex",
|
||||
],
|
||||
)
|
||||
7
codex-rs/exec-server/tests/common/BUILD.bazel
Normal file
7
codex-rs/exec-server/tests/common/BUILD.bazel
Normal file
@@ -0,0 +1,7 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "common",
|
||||
crate_name = "exec_server_test_support",
|
||||
crate_srcs = glob(["*.rs"]),
|
||||
)
|
||||
7
codex-rs/exec/BUILD.bazel
Normal file
7
codex-rs/exec/BUILD.bazel
Normal file
@@ -0,0 +1,7 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "exec",
|
||||
crate_name = "codex_exec",
|
||||
test_tags = ["no-sandbox"],
|
||||
)
|
||||
@@ -223,7 +223,8 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let otel = codex_core::otel_init::build_provider(&config, env!("CARGO_PKG_VERSION"), false);
|
||||
let otel =
|
||||
codex_core::otel_init::build_provider(&config, env!("CARGO_PKG_VERSION"), None, false);
|
||||
|
||||
#[allow(clippy::print_stderr)]
|
||||
let otel = match otel {
|
||||
|
||||
7
codex-rs/execpolicy-legacy/BUILD.bazel
Normal file
7
codex-rs/execpolicy-legacy/BUILD.bazel
Normal file
@@ -0,0 +1,7 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "execpolicy-legacy",
|
||||
crate_name = "codex_execpolicy_legacy",
|
||||
compile_data = ["src/default.policy"],
|
||||
)
|
||||
6
codex-rs/execpolicy/BUILD.bazel
Normal file
6
codex-rs/execpolicy/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "execpolicy",
|
||||
crate_name = "codex_execpolicy",
|
||||
)
|
||||
6
codex-rs/feedback/BUILD.bazel
Normal file
6
codex-rs/feedback/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "feedback",
|
||||
crate_name = "codex_feedback",
|
||||
)
|
||||
6
codex-rs/file-search/BUILD.bazel
Normal file
6
codex-rs/file-search/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "file-search",
|
||||
crate_name = "codex_file_search",
|
||||
)
|
||||
6
codex-rs/keyring-store/BUILD.bazel
Normal file
6
codex-rs/keyring-store/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "keyring-store",
|
||||
crate_name = "codex_keyring_store",
|
||||
)
|
||||
6
codex-rs/linux-sandbox/BUILD.bazel
Normal file
6
codex-rs/linux-sandbox/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "linux-sandbox",
|
||||
crate_name = "codex_linux_sandbox",
|
||||
)
|
||||
6
codex-rs/lmstudio/BUILD.bazel
Normal file
6
codex-rs/lmstudio/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "lmstudio",
|
||||
crate_name = "codex_lmstudio",
|
||||
)
|
||||
7
codex-rs/login/BUILD.bazel
Normal file
7
codex-rs/login/BUILD.bazel
Normal file
@@ -0,0 +1,7 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "login",
|
||||
crate_name = "codex_login",
|
||||
compile_data = ["src/assets/success.html"],
|
||||
)
|
||||
7
codex-rs/mcp-server/BUILD.bazel
Normal file
7
codex-rs/mcp-server/BUILD.bazel
Normal file
@@ -0,0 +1,7 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "mcp-server",
|
||||
crate_name = "codex_mcp_server",
|
||||
integration_deps_extra = ["//codex-rs/mcp-server/tests/common:common"],
|
||||
)
|
||||
@@ -8,7 +8,7 @@ use codex_utils_json_to_toml::json_to_toml;
|
||||
use mcp_types::Tool;
|
||||
use mcp_types::ToolInputSchema;
|
||||
use schemars::JsonSchema;
|
||||
use schemars::r#gen::SchemaSettings;
|
||||
use schemars::generate::SchemaSettings;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
@@ -108,7 +108,6 @@ pub(crate) fn create_tool_for_codex_tool_call_param() -> Tool {
|
||||
let schema = SchemaSettings::draft2019_09()
|
||||
.with(|s| {
|
||||
s.inline_subschemas = true;
|
||||
s.option_add_null_type = false;
|
||||
})
|
||||
.into_generator()
|
||||
.into_root_schema_for::<CodexToolCallParam>();
|
||||
@@ -197,7 +196,6 @@ pub(crate) fn create_tool_for_codex_tool_call_reply_param() -> Tool {
|
||||
let schema = SchemaSettings::draft2019_09()
|
||||
.with(|s| {
|
||||
s.inline_subschemas = true;
|
||||
s.option_add_null_type = false;
|
||||
})
|
||||
.into_generator()
|
||||
.into_root_schema_for::<CodexToolCallReplyParam>();
|
||||
@@ -249,43 +247,68 @@ mod tests {
|
||||
"inputSchema": {
|
||||
"properties": {
|
||||
"approval-policy": {
|
||||
"description": "Approval policy for shell commands generated by the model: `untrusted`, `on-failure`, `on-request`, `never`.",
|
||||
"description": "Approval policy for shell commands generated by the model:\n`untrusted`, `on-failure`, `on-request`, `never`.",
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
"never",
|
||||
null
|
||||
],
|
||||
"type": "string"
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"base-instructions": {
|
||||
"description": "The set of instructions to use instead of the default ones.",
|
||||
"type": "string"
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"compact-prompt": {
|
||||
"description": "Prompt used when compacting the conversation.",
|
||||
"type": "string"
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"additionalProperties": true,
|
||||
"description": "Individual config settings that will override what is in CODEX_HOME/config.toml.",
|
||||
"type": "object"
|
||||
"description": "Individual config settings that will override what is in\nCODEX_HOME/config.toml.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"cwd": {
|
||||
"description": "Working directory for the session. If relative, it is resolved against the server process's current working directory.",
|
||||
"type": "string"
|
||||
"description": "Working directory for the session. If relative, it is resolved against\nthe server process's current working directory.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"developer-instructions": {
|
||||
"description": "Developer instructions that should be injected as a developer role message.",
|
||||
"type": "string"
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"model": {
|
||||
"description": "Optional override for the model name (e.g. 'gpt-5.2', 'gpt-5.2-codex').",
|
||||
"type": "string"
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"profile": {
|
||||
"description": "Configuration profile from config.toml to specify default options.",
|
||||
"type": "string"
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"prompt": {
|
||||
"description": "The *initial user prompt* to start the Codex conversation.",
|
||||
@@ -296,9 +319,13 @@ mod tests {
|
||||
"enum": [
|
||||
"read-only",
|
||||
"workspace-write",
|
||||
"danger-full-access"
|
||||
"danger-full-access",
|
||||
null
|
||||
],
|
||||
"type": "string"
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
7
codex-rs/mcp-server/tests/common/BUILD.bazel
Normal file
7
codex-rs/mcp-server/tests/common/BUILD.bazel
Normal file
@@ -0,0 +1,7 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "common",
|
||||
crate_name = "mcp_test_support",
|
||||
crate_srcs = glob(["*.rs"]),
|
||||
)
|
||||
6
codex-rs/mcp-types/BUILD.bazel
Normal file
6
codex-rs/mcp-types/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "mcp-types",
|
||||
crate_name = "mcp_types",
|
||||
)
|
||||
6
codex-rs/ollama/BUILD.bazel
Normal file
6
codex-rs/ollama/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "ollama",
|
||||
crate_name = "codex_ollama",
|
||||
)
|
||||
6
codex-rs/otel/BUILD.bazel
Normal file
6
codex-rs/otel/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "otel",
|
||||
crate_name = "codex_otel",
|
||||
)
|
||||
6
codex-rs/process-hardening/BUILD.bazel
Normal file
6
codex-rs/process-hardening/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "process-hardening",
|
||||
crate_name = "codex_process_hardening",
|
||||
)
|
||||
6
codex-rs/protocol/BUILD.bazel
Normal file
6
codex-rs/protocol/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "protocol",
|
||||
crate_name = "codex_protocol",
|
||||
)
|
||||
@@ -80,3 +80,38 @@ pub enum TrustLevel {
|
||||
Trusted,
|
||||
Untrusted,
|
||||
}
|
||||
|
||||
/// Controls whether the TUI uses the terminal's alternate screen buffer.
|
||||
///
|
||||
/// **Background:** The alternate screen buffer provides a cleaner fullscreen experience
|
||||
/// without polluting the terminal's scrollback history. However, it conflicts with terminal
|
||||
/// multiplexers like Zellij that strictly follow the xterm specification, which defines
|
||||
/// that alternate screen buffers should not have scrollback.
|
||||
///
|
||||
/// **Zellij's behavior:** Zellij intentionally disables scrollback in alternate screen mode
|
||||
/// (see https://github.com/zellij-org/zellij/pull/1032) to comply with the xterm spec. This
|
||||
/// is by design and not configurable in Zellij—there is no option to enable scrollback in
|
||||
/// alternate screen mode.
|
||||
///
|
||||
/// **Solution:** This setting provides a pragmatic workaround:
|
||||
/// - `auto` (default): Automatically detect the terminal multiplexer. If running in Zellij,
|
||||
/// disable alternate screen to preserve scrollback. Enable it everywhere else.
|
||||
/// - `always`: Always use alternate screen mode (original behavior before this fix).
|
||||
/// - `never`: Never use alternate screen mode. Runs in inline mode, preserving scrollback
|
||||
/// in all multiplexers.
|
||||
///
|
||||
/// The CLI flag `--no-alt-screen` can override this setting at runtime.
|
||||
#[derive(
|
||||
Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum AltScreenMode {
|
||||
/// Auto-detect: disable alternate screen in Zellij, enable elsewhere.
|
||||
#[default]
|
||||
Auto,
|
||||
/// Always use alternate screen (original behavior).
|
||||
Always,
|
||||
/// Never use alternate screen (inline mode only).
|
||||
Never,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Display;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use schemars::r#gen::SchemaGenerator;
|
||||
use schemars::schema::Schema;
|
||||
use schemars::Schema;
|
||||
use schemars::SchemaGenerator;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
@@ -61,8 +62,8 @@ impl<'de> Deserialize<'de> for ThreadId {
|
||||
}
|
||||
|
||||
impl JsonSchema for ThreadId {
|
||||
fn schema_name() -> String {
|
||||
"ThreadId".to_string()
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"ThreadId".into()
|
||||
}
|
||||
|
||||
fn json_schema(generator: &mut SchemaGenerator) -> Schema {
|
||||
|
||||
6
codex-rs/responses-api-proxy/BUILD.bazel
Normal file
6
codex-rs/responses-api-proxy/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "responses-api-proxy",
|
||||
crate_name = "codex_responses_api_proxy",
|
||||
)
|
||||
6
codex-rs/rmcp-client/BUILD.bazel
Normal file
6
codex-rs/rmcp-client/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "rmcp-client",
|
||||
crate_name = "codex_rmcp_client",
|
||||
)
|
||||
@@ -36,6 +36,7 @@ rmcp = { workspace = true, default-features = false, features = [
|
||||
"transport-streamable-http-client-reqwest",
|
||||
"transport-streamable-http-server",
|
||||
] }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
|
||||
@@ -26,6 +26,7 @@ use oauth2::Scope;
|
||||
use oauth2::TokenResponse;
|
||||
use oauth2::basic::BasicTokenType;
|
||||
use rmcp::transport::auth::OAuthTokenResponse;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
@@ -63,7 +64,7 @@ pub struct StoredOAuthTokens {
|
||||
}
|
||||
|
||||
/// Determine where Codex should store and read MCP credentials.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum OAuthCredentialsStoreMode {
|
||||
/// `Keyring` when available; otherwise, `File`.
|
||||
|
||||
6
codex-rs/stdio-to-uds/BUILD.bazel
Normal file
6
codex-rs/stdio-to-uds/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "stdio-to-uds",
|
||||
crate_name = "codex_stdio_to_uds",
|
||||
)
|
||||
17
codex-rs/tui/BUILD.bazel
Normal file
17
codex-rs/tui/BUILD.bazel
Normal file
@@ -0,0 +1,17 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "tui",
|
||||
crate_name = "codex_tui",
|
||||
compile_data = glob(
|
||||
include = ["**"],
|
||||
exclude = [
|
||||
"**/* *",
|
||||
"BUILD.bazel",
|
||||
"Cargo.toml",
|
||||
],
|
||||
allow_empty = True,
|
||||
),
|
||||
test_data_extra = glob(["src/**/snapshots/**"]),
|
||||
integration_compile_data_extra = ["src/test_backend.rs"],
|
||||
)
|
||||
@@ -85,6 +85,14 @@ pub struct Cli {
|
||||
#[arg(long = "add-dir", value_name = "DIR", value_hint = ValueHint::DirPath)]
|
||||
pub add_dir: Vec<PathBuf>,
|
||||
|
||||
/// Disable alternate screen mode
|
||||
///
|
||||
/// Runs the TUI in inline mode, preserving terminal scrollback history. This is useful
|
||||
/// in terminal multiplexers like Zellij that follow the xterm spec strictly and disable
|
||||
/// scrollback in alternate screen buffers.
|
||||
#[arg(long = "no-alt-screen", default_value_t = false)]
|
||||
pub no_alt_screen: bool,
|
||||
|
||||
#[clap(skip)]
|
||||
pub config_overrides: CliConfigOverrides,
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user