diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml
index 292e549b41..59a1e07fa7 100644
--- a/.github/workflows/bazel.yml
+++ b/.github/workflows/bazel.yml
@@ -65,6 +65,11 @@ jobs:
- name: Set up Bazel
uses: bazelbuild/setup-bazelisk@v3
+ - name: Check MODULE.bazel.lock is up to date
+ if: matrix.os == 'ubuntu-24.04' && matrix.target == 'x86_64-unknown-linux-gnu'
+ shell: bash
+ run: ./scripts/check-module-bazel-lock.sh
+
# 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!
diff --git a/.github/workflows/shell-tool-mcp.yml b/.github/workflows/shell-tool-mcp.yml
index 04e127a785..609c933bd9 100644
--- a/.github/workflows/shell-tool-mcp.yml
+++ b/.github/workflows/shell-tool-mcp.yml
@@ -408,9 +408,8 @@ jobs:
shell: bash
run: |
set -euo pipefail
- git clone --depth 1 https://git.code.sf.net/p/zsh/code /tmp/zsh
+ git clone https://git.code.sf.net/p/zsh/code /tmp/zsh
cd /tmp/zsh
- git fetch --depth 1 origin 77045ef899e53b9598bebc5a41db93a548a40ca6
git checkout 77045ef899e53b9598bebc5a41db93a548a40ca6
git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/zsh-exec-wrapper.patch"
./Util/preconfig
@@ -487,9 +486,8 @@ jobs:
shell: bash
run: |
set -euo pipefail
- git clone --depth 1 https://git.code.sf.net/p/zsh/code /tmp/zsh
+ git clone https://git.code.sf.net/p/zsh/code /tmp/zsh
cd /tmp/zsh
- git fetch --depth 1 origin 77045ef899e53b9598bebc5a41db93a548a40ca6
git checkout 77045ef899e53b9598bebc5a41db93a548a40ca6
git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/zsh-exec-wrapper.patch"
./Util/preconfig
diff --git a/AGENTS.md b/AGENTS.md
index fc545c5080..fa2a4bbaba 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -15,6 +15,10 @@ In the codex-rs folder where the rust code lives:
- 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`.
+- If you change Rust dependencies (`Cargo.toml` or `Cargo.lock`), run `just bazel-lock-update` from the
+ repo root to refresh `MODULE.bazel.lock`, and include that lockfile update in the same change.
+- After dependency changes, run `just bazel-lock-check` from the repo root so lockfile drift is caught
+ locally before CI.
- Do not create small helper methods that are referenced only once.
Run `just fmt` (in `codex-rs` directory) automatically after you have finished making Rust code changes; do not ask for approval to run it. Additionally, run the tests:
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
index 98efd3cede..80e889a031 100644
--- a/MODULE.bazel.lock
+++ b/MODULE.bazel.lock
@@ -608,7 +608,7 @@
"anyhow_1.0.101": "{\"dependencies\":[{\"name\":\"backtrace\",\"optional\":true,\"req\":\"^0.3.51\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.6\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.108\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}",
"arbitrary_1.4.2": "{\"dependencies\":[{\"name\":\"derive_arbitrary\",\"optional\":true,\"req\":\"~1.4.0\"},{\"kind\":\"dev\",\"name\":\"exhaustigen\",\"req\":\"^0.1.0\"}],\"features\":{\"derive\":[\"derive_arbitrary\"]}}",
"arboard_3.6.1": "{\"dependencies\":[{\"features\":[\"std\"],\"name\":\"clipboard-win\",\"req\":\"^5.3.1\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10.2\"},{\"default_features\":false,\"features\":[\"png\"],\"name\":\"image\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"default_features\":false,\"features\":[\"tiff\"],\"name\":\"image\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"png\",\"bmp\"],\"name\":\"image\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(windows)\"},{\"name\":\"log\",\"req\":\"^0.4\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"name\":\"log\",\"req\":\"^0.4\",\"target\":\"cfg(windows)\"},{\"name\":\"objc2\",\"req\":\"^0.6.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"objc2-core-graphics\",\"NSPasteboard\",\"NSPasteboardItem\",\"NSImage\"],\"name\":\"objc2-app-kit\",\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"CFCGTypes\"],\"name\":\"objc2-core-foundation\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"CGImage\",\"CGColorSpace\",\"CGDataProvider\"],\"name\":\"objc2-core-graphics\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"NSArray\",\"NSString\",\"NSEnumerator\",\"NSGeometry\",\"NSValue\"],\"name\":\"objc2-foundation\",\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3.1\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"features\":[\"Win32_Foundation\",\"Win32_Storage_FileSystem\",\"Win32_System_DataExchange\",\"Win32_System_Memory\",\"Win32_System_Ole\",\"Win32_UI_Shell\"],\"name\":\"windows-sys\",\"req\":\">=0.52.0, <0.61.0\",\"target\":\"cfg(windows)\"},{\"name\":\"wl-clipboard-rs\",\"optional\":true,\"req\":\"^0.9.0\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"name\":\"x11rb\",\"req\":\"^0.13\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"}],\"features\":{\"core-graphics\":[\"dep:objc2-core-graphics\"],\"default\":[\"image-data\"],\"image\":[\"dep:image\"],\"image-data\":[\"dep:objc2-core-graphics\",\"dep:objc2-core-foundation\",\"image\",\"windows-sys\",\"core-graphics\"],\"wayland-data-control\":[\"wl-clipboard-rs\"],\"windows-sys\":[\"windows-sys/Win32_Graphics_Gdi\"],\"wl-clipboard-rs\":[\"dep:wl-clipboard-rs\"]}}",
- "arc-swap_1.8.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"adaptive-barrier\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"~0.7\"},{\"kind\":\"dev\",\"name\":\"crossbeam-utils\",\"req\":\"~0.8\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"parking_lot\",\"req\":\"~0.12\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"rustversion\",\"req\":\"^1\"},{\"features\":[\"rc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.130\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.177\"}],\"features\":{\"experimental-strategies\":[],\"experimental-thread-local\":[],\"internal-test-strategies\":[],\"weak\":[]}}",
+ "arc-swap_1.8.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"adaptive-barrier\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"~0.7\"},{\"kind\":\"dev\",\"name\":\"crossbeam-utils\",\"req\":\"~0.8\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"parking_lot\",\"req\":\"~0.12\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"rustversion\",\"req\":\"^1\"},{\"features\":[\"rc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.130\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.177\"}],\"features\":{\"experimental-strategies\":[],\"experimental-thread-local\":[],\"internal-test-strategies\":[],\"weak\":[]}}",
"arrayvec_0.7.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.4\"},{\"default_features\":false,\"name\":\"borsh\",\"optional\":true,\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"matches\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.4\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}",
"ascii-canvas_3.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"diff\",\"req\":\"^0.1\"},{\"name\":\"term\",\"req\":\"^0.7\"}],\"features\":{}}",
"ascii_1.1.0": "{\"dependencies\":[{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.25\"},{\"name\":\"serde_test\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}",
@@ -1154,8 +1154,8 @@
"reqwest_0.12.28": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"brotli_crate\",\"package\":\"brotli\",\"req\":\"^8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"bytes\",\"req\":\"^1.2\"},{\"name\":\"cookie_crate\",\"optional\":true,\"package\":\"cookie\",\"req\":\"^0.18.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"cookie_store\",\"optional\":true,\"req\":\"^0.22.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"encoding_rs\",\"optional\":true,\"req\":\"^0.8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0.13\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.28\"},{\"default_features\":false,\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.28\"},{\"default_features\":false,\"features\":[\"std\",\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.28\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h2\",\"optional\":true,\"req\":\"^0.4\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h3\",\"optional\":true,\"req\":\"^0.0.8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h3-quinn\",\"optional\":true,\"req\":\"^0.0.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"tokio\"],\"name\":\"hickory-resolver\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"http\",\"req\":\"^1.1\"},{\"name\":\"http-body\",\"req\":\"^1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.2\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"http1\",\"client\"],\"name\":\"hyper\",\"req\":\"^1.1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"http1\",\"http2\",\"client\",\"server\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"http1\",\"tls12\"],\"name\":\"hyper-rustls\",\"optional\":true,\"req\":\"^0.27.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"hyper-tls\",\"optional\":true,\"req\":\"^0.6\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"http1\",\"client\",\"client-legacy\",\"client-proxy\",\"tokio\"],\"name\":\"hyper-util\",\"req\":\"^0.1.12\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"http1\",\"http2\",\"client\",\"client-legacy\",\"server-auto\",\"server-graceful\",\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1.12\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"js-sys\",\"req\":\"^0.3.77\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0\"},{\"name\":\"log\",\"req\":\"^0.4.17\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"mime\",\"optional\":true,\"req\":\"^0.3.16\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"mime_guess\",\"optional\":true,\"req\":\"^2.0\"},{\"name\":\"native-tls-crate\",\"optional\":true,\"package\":\"native-tls\",\"req\":\"^0.2.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"^1.0\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.18\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"rustls\",\"runtime-tokio\"],\"name\":\"quinn\",\"optional\":true,\"req\":\"^0.11.1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"std\",\"tls12\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.4\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"std\"],\"name\":\"rustls-pki-types\",\"optional\":true,\"req\":\"^1.9.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"serde_json\",\"req\":\"^1.0\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_urlencoded\",\"req\":\"^0.7.1\"},{\"features\":[\"futures\"],\"name\":\"sync_wrapper\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"net\",\"time\"],\"name\":\"tokio\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"tokio-native-tls\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"tls12\"],\"name\":\"tokio-rustls\",\"optional\":true,\"req\":\"^0.26\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"io\"],\"name\":\"tokio-util\",\"optional\":true,\"req\":\"^0.7.9\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"retry\",\"timeout\",\"util\"],\"name\":\"tower\",\"req\":\"^0.5.2\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"limit\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.5.2\"},{\"default_features\":false,\"features\":[\"follow-redirect\"],\"name\":\"tower-http\",\"req\":\"^0.6.8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"tower-service\",\"req\":\"^0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"url\",\"req\":\"^2.4\"},{\"name\":\"wasm-bindgen\",\"req\":\"^0.2.89\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"features\":[\"serde-serialize\"],\"kind\":\"dev\",\"name\":\"wasm-bindgen\",\"req\":\"^0.2.89\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"wasm-bindgen-futures\",\"req\":\"^0.4.18\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"wasm-streams\",\"optional\":true,\"req\":\"^0.4\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"features\":[\"AbortController\",\"AbortSignal\",\"Headers\",\"Request\",\"RequestInit\",\"RequestMode\",\"Response\",\"Window\",\"FormData\",\"Blob\",\"BlobPropertyBag\",\"ServiceWorkerGlobalScope\",\"RequestCredentials\",\"File\",\"ReadableStream\",\"RequestCache\"],\"name\":\"web-sys\",\"req\":\"^0.3.28\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"zstd_crate\",\"package\":\"zstd\",\"req\":\"^0.13\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"}],\"features\":{\"__rustls\":[\"dep:hyper-rustls\",\"dep:tokio-rustls\",\"dep:rustls\",\"__tls\"],\"__rustls-ring\":[\"hyper-rustls?/ring\",\"tokio-rustls?/ring\",\"rustls?/ring\",\"quinn?/ring\"],\"__tls\":[\"dep:rustls-pki-types\",\"tokio/io-util\"],\"blocking\":[\"dep:futures-channel\",\"futures-channel?/sink\",\"dep:futures-util\",\"futures-util?/io\",\"futures-util?/sink\",\"tokio/sync\"],\"brotli\":[\"tower-http/decompression-br\"],\"charset\":[\"dep:encoding_rs\",\"dep:mime\"],\"cookies\":[\"dep:cookie_crate\",\"dep:cookie_store\"],\"default\":[\"default-tls\",\"charset\",\"http2\",\"system-proxy\"],\"default-tls\":[\"dep:hyper-tls\",\"dep:native-tls-crate\",\"__tls\",\"dep:tokio-native-tls\"],\"deflate\":[\"tower-http/decompression-deflate\"],\"gzip\":[\"tower-http/decompression-gzip\"],\"hickory-dns\":[\"dep:hickory-resolver\",\"dep:once_cell\"],\"http2\":[\"h2\",\"hyper/http2\",\"hyper-util/http2\",\"hyper-rustls?/http2\"],\"http3\":[\"rustls-tls-manual-roots\",\"dep:h3\",\"dep:h3-quinn\",\"dep:quinn\",\"tokio/macros\"],\"json\":[\"dep:serde_json\"],\"macos-system-configuration\":[\"system-proxy\"],\"multipart\":[\"dep:mime_guess\",\"dep:futures-util\"],\"native-tls\":[\"default-tls\"],\"native-tls-alpn\":[\"native-tls\",\"native-tls-crate?/alpn\",\"hyper-tls?/alpn\"],\"native-tls-vendored\":[\"native-tls\",\"native-tls-crate?/vendored\"],\"rustls-tls\":[\"rustls-tls-webpki-roots\"],\"rustls-tls-manual-roots\":[\"rustls-tls-manual-roots-no-provider\",\"__rustls-ring\"],\"rustls-tls-manual-roots-no-provider\":[\"__rustls\"],\"rustls-tls-native-roots\":[\"rustls-tls-native-roots-no-provider\",\"__rustls-ring\"],\"rustls-tls-native-roots-no-provider\":[\"dep:rustls-native-certs\",\"hyper-rustls?/native-tokio\",\"__rustls\"],\"rustls-tls-no-provider\":[\"rustls-tls-manual-roots-no-provider\"],\"rustls-tls-webpki-roots\":[\"rustls-tls-webpki-roots-no-provider\",\"__rustls-ring\"],\"rustls-tls-webpki-roots-no-provider\":[\"dep:webpki-roots\",\"hyper-rustls?/webpki-tokio\",\"__rustls\"],\"socks\":[],\"stream\":[\"tokio/fs\",\"dep:futures-util\",\"dep:tokio-util\",\"dep:wasm-streams\"],\"system-proxy\":[\"hyper-util/client-proxy-system\"],\"trust-dns\":[],\"zstd\":[\"tower-http/decompression-zstd\"]}}",
"resolv-conf_0.7.6": "{\"dependencies\":[],\"features\":{\"system\":[]}}",
"ring_0.17.14": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.2.8\"},{\"default_features\":false,\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"getrandom\",\"req\":\"^0.2.10\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.148\",\"target\":\"cfg(all(any(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), all(target_arch = \\\"arm\\\", target_endian = \\\"little\\\")), any(target_os = \\\"android\\\", target_os = \\\"linux\\\")))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(all(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), target_vendor = \\\"apple\\\", any(target_os = \\\"ios\\\", target_os = \\\"macos\\\", target_os = \\\"tvos\\\", target_os = \\\"visionos\\\", target_os = \\\"watchos\\\")))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.148\",\"target\":\"cfg(any(unix, windows, target_os = \\\"wasi\\\"))\"},{\"name\":\"untrusted\",\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.37\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Threading\"],\"name\":\"windows-sys\",\"req\":\"^0.52\",\"target\":\"cfg(all(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), target_os = \\\"windows\\\"))\"}],\"features\":{\"alloc\":[],\"default\":[\"alloc\",\"dev_urandom_fallback\"],\"dev_urandom_fallback\":[],\"less-safe-getrandom-custom-or-rdrand\":[],\"less-safe-getrandom-espidf\":[],\"slow_tests\":[],\"std\":[\"alloc\"],\"test_logging\":[],\"unstable-testing-arm-no-hw\":[],\"unstable-testing-arm-no-neon\":[],\"wasm32_unknown_unknown_js\":[\"getrandom/js\"]}}",
- "rmcp-macros_0.14.0": "{\"dependencies\":[{\"name\":\"darling\",\"req\":\"^0.23\"},{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}",
- "rmcp_0.14.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"async-trait\",\"req\":\"^0.1.89\"},{\"kind\":\"dev\",\"name\":\"async-trait\",\"req\":\"^0.1\"},{\"name\":\"axum\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"serde\",\"clock\",\"std\",\"oldtime\"],\"name\":\"chrono\",\"req\":\"^0.4.38\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"serde\"],\"name\":\"chrono\",\"req\":\"^0.4.38\",\"target\":\"cfg(not(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\")))\"},{\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"http\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"http-body\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"http-body-util\",\"optional\":true,\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"reqwest\"],\"name\":\"oauth2\",\"optional\":true,\"req\":\"^5.0\"},{\"name\":\"pastey\",\"optional\":true,\"req\":\"^0.2.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"features\":[\"tokio1\"],\"name\":\"process-wrap\",\"optional\":true,\"req\":\"^9.0\"},{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"json\",\"stream\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"rmcp-macros\",\"optional\":true,\"req\":\"^0.14.0\"},{\"features\":[\"chrono04\"],\"name\":\"schemars\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"chrono04\"],\"kind\":\"dev\",\"name\":\"schemars\",\"req\":\"^1.1.0\"},{\"features\":[\"derive\",\"rc\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"sse-stream\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"sync\",\"macros\",\"rt\",\"time\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"name\":\"tokio-stream\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"name\":\"tower-service\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"env-filter\",\"std\",\"fmt\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"name\":\"url\",\"optional\":true,\"req\":\"^2.4\"},{\"features\":[\"v4\"],\"name\":\"uuid\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"__reqwest\":[\"dep:reqwest\"],\"auth\":[\"dep:oauth2\",\"__reqwest\",\"dep:url\"],\"client\":[\"dep:tokio-stream\"],\"client-side-sse\":[\"dep:sse-stream\",\"dep:http\"],\"default\":[\"base64\",\"macros\",\"server\"],\"elicitation\":[],\"macros\":[\"dep:rmcp-macros\",\"dep:pastey\"],\"reqwest\":[\"__reqwest\",\"reqwest?/rustls-tls\"],\"reqwest-tls-no-provider\":[\"__reqwest\",\"reqwest?/rustls-tls-no-provider\"],\"schemars\":[\"dep:schemars\"],\"server\":[\"transport-async-rw\",\"dep:schemars\"],\"server-side-http\":[\"uuid\",\"dep:rand\",\"dep:tokio-stream\",\"dep:http\",\"dep:http-body\",\"dep:http-body-util\",\"dep:bytes\",\"dep:sse-stream\",\"dep:axum\",\"tower\"],\"tower\":[\"dep:tower-service\"],\"transport-async-rw\":[\"tokio/io-util\",\"tokio-util/codec\"],\"transport-child-process\":[\"transport-async-rw\",\"tokio/process\",\"dep:process-wrap\"],\"transport-io\":[\"transport-async-rw\",\"tokio/io-std\"],\"transport-streamable-http-client\":[\"client-side-sse\",\"transport-worker\"],\"transport-streamable-http-client-reqwest\":[\"transport-streamable-http-client\",\"__reqwest\"],\"transport-streamable-http-server\":[\"transport-streamable-http-server-session\",\"server-side-http\",\"transport-worker\"],\"transport-streamable-http-server-session\":[\"transport-async-rw\",\"dep:tokio-stream\"],\"transport-worker\":[\"dep:tokio-stream\"]}}",
+ "rmcp-macros_0.15.0": "{\"dependencies\":[{\"name\":\"darling\",\"req\":\"^0.23\"},{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}",
+ "rmcp_0.15.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"async-trait\",\"req\":\"^0.1.89\"},{\"kind\":\"dev\",\"name\":\"async-trait\",\"req\":\"^0.1\"},{\"name\":\"axum\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"serde\",\"clock\",\"std\",\"oldtime\"],\"name\":\"chrono\",\"req\":\"^0.4.38\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"serde\"],\"name\":\"chrono\",\"req\":\"^0.4.38\",\"target\":\"cfg(not(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\")))\"},{\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"http\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"http-body\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"http-body-util\",\"optional\":true,\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"reqwest\"],\"name\":\"oauth2\",\"optional\":true,\"req\":\"^5.0\"},{\"name\":\"pastey\",\"optional\":true,\"req\":\"^0.2.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"features\":[\"tokio1\"],\"name\":\"process-wrap\",\"optional\":true,\"req\":\"^9.0\"},{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"json\",\"stream\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"rmcp-macros\",\"optional\":true,\"req\":\"^0.15.0\"},{\"features\":[\"chrono04\"],\"name\":\"schemars\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"chrono04\"],\"kind\":\"dev\",\"name\":\"schemars\",\"req\":\"^1.1.0\"},{\"features\":[\"derive\",\"rc\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"sse-stream\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"sync\",\"macros\",\"rt\",\"time\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"name\":\"tokio-stream\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"name\":\"tower-service\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"env-filter\",\"std\",\"fmt\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"name\":\"url\",\"optional\":true,\"req\":\"^2.4\"},{\"features\":[\"v4\"],\"name\":\"uuid\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"__reqwest\":[\"dep:reqwest\"],\"auth\":[\"dep:oauth2\",\"__reqwest\",\"dep:url\"],\"client\":[\"dep:tokio-stream\"],\"client-side-sse\":[\"dep:sse-stream\",\"dep:http\"],\"default\":[\"base64\",\"macros\",\"server\"],\"elicitation\":[\"dep:url\"],\"macros\":[\"dep:rmcp-macros\",\"dep:pastey\"],\"reqwest\":[\"__reqwest\",\"reqwest?/rustls-tls\"],\"reqwest-native-tls\":[\"__reqwest\",\"reqwest?/native-tls\"],\"reqwest-tls-no-provider\":[\"__reqwest\",\"reqwest?/rustls-tls-no-provider\"],\"schemars\":[\"dep:schemars\"],\"server\":[\"transport-async-rw\",\"dep:schemars\",\"dep:pastey\"],\"server-side-http\":[\"uuid\",\"dep:rand\",\"dep:tokio-stream\",\"dep:http\",\"dep:http-body\",\"dep:http-body-util\",\"dep:bytes\",\"dep:sse-stream\",\"dep:axum\",\"tower\"],\"tower\":[\"dep:tower-service\"],\"transport-async-rw\":[\"tokio/io-util\",\"tokio-util/codec\"],\"transport-child-process\":[\"transport-async-rw\",\"tokio/process\",\"dep:process-wrap\"],\"transport-io\":[\"transport-async-rw\",\"tokio/io-std\"],\"transport-streamable-http-client\":[\"client-side-sse\",\"transport-worker\"],\"transport-streamable-http-client-reqwest\":[\"transport-streamable-http-client\",\"__reqwest\"],\"transport-streamable-http-server\":[\"transport-streamable-http-server-session\",\"server-side-http\",\"transport-worker\"],\"transport-streamable-http-server-session\":[\"transport-async-rw\",\"dep:tokio-stream\"],\"transport-worker\":[\"dep:tokio-stream\"]}}",
"rsa_0.9.10": "{\"dependencies\":[{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"base64ct\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"const-oid\",\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"alloc\",\"oid\"],\"name\":\"digest\",\"req\":\"^0.10.5\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4.1\"},{\"default_features\":false,\"features\":[\"i128\",\"prime\",\"zeroize\"],\"name\":\"num-bigint\",\"package\":\"num-bigint-dig\",\"req\":\"^0.8.6\"},{\"default_features\":false,\"name\":\"num-integer\",\"req\":\"^0.1.39\"},{\"default_features\":false,\"features\":[\"libm\"],\"name\":\"num-traits\",\"req\":\"^0.2.9\"},{\"default_features\":false,\"features\":[\"alloc\",\"pkcs8\"],\"name\":\"pkcs1\",\"req\":\"^0.7.5\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"pkcs8\",\"req\":\"^0.10.2\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand_chacha\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"rand_core\",\"req\":\"^0.6.4\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"rand_xorshift\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.184\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.89\"},{\"default_features\":false,\"features\":[\"oid\"],\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10.5\"},{\"default_features\":false,\"features\":[\"oid\"],\"kind\":\"dev\",\"name\":\"sha1\",\"req\":\"^0.10.5\"},{\"default_features\":false,\"features\":[\"oid\"],\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10.6\"},{\"default_features\":false,\"features\":[\"oid\"],\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10.6\"},{\"default_features\":false,\"features\":[\"oid\"],\"kind\":\"dev\",\"name\":\"sha3\",\"req\":\"^0.10.7\"},{\"default_features\":false,\"features\":[\"alloc\",\"digest\",\"rand_core\"],\"name\":\"signature\",\"req\":\">2.0, <2.3\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"spki\",\"req\":\"^0.7.3\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.1.1\"},{\"features\":[\"alloc\"],\"name\":\"zeroize\",\"req\":\"^1.5\"}],\"features\":{\"default\":[\"std\",\"pem\",\"u64_digit\"],\"getrandom\":[\"rand_core/getrandom\"],\"hazmat\":[],\"nightly\":[\"num-bigint/nightly\"],\"pem\":[\"pkcs1/pem\",\"pkcs8/pem\"],\"pkcs5\":[\"pkcs8/encryption\"],\"serde\":[\"dep:serde\",\"num-bigint/serde\"],\"std\":[\"digest/std\",\"pkcs1/std\",\"pkcs8/std\",\"rand_core/std\",\"signature/std\"],\"u64_digit\":[\"num-bigint/u64_digit\"]}}",
"rust-embed-impl_8.11.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"name\":\"rust-embed-utils\",\"req\":\"^8.11.0\"},{\"name\":\"shellexpand\",\"optional\":true,\"req\":\"^3\"},{\"default_features\":false,\"features\":[\"derive\",\"parsing\",\"proc-macro\",\"printing\"],\"name\":\"syn\",\"req\":\"^2\"},{\"name\":\"walkdir\",\"req\":\"^2.3.1\"}],\"features\":{\"compression\":[],\"debug-embed\":[],\"deterministic-timestamps\":[],\"include-exclude\":[\"rust-embed-utils/include-exclude\"],\"interpolate-folder-path\":[\"shellexpand\"],\"mime-guess\":[\"rust-embed-utils/mime-guess\"]}}",
"rust-embed-utils_8.11.0": "{\"dependencies\":[{\"name\":\"globset\",\"optional\":true,\"req\":\"^0.4.8\"},{\"name\":\"mime_guess\",\"optional\":true,\"req\":\"^2.0.4\"},{\"name\":\"sha2\",\"req\":\"^0.10.5\"},{\"name\":\"walkdir\",\"req\":\"^2.3.1\"}],\"features\":{\"debug-embed\":[],\"include-exclude\":[\"globset\"],\"mime-guess\":[\"mime_guess\"]}}",
diff --git a/README.md b/README.md
index ae5073beae..1e44875f21 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,7 @@
If you want Codex in your code editor (VS Code, Cursor, Windsurf), install in your IDE.
+If you want the desktop app experience, run codex app or visit the Codex App page.
If you are looking for the cloud-based agent from OpenAI, Codex Web, go to chatgpt.com/codex.
---
diff --git a/codex-cli/bin/rg b/codex-cli/bin/rg
index 5a992570a9..426df75d71 100755
--- a/codex-cli/bin/rg
+++ b/codex-cli/bin/rg
@@ -4,74 +4,74 @@
"name": "rg",
"platforms": {
"macos-aarch64": {
- "size": 1787248,
- "hash": "blake3",
- "digest": "8d9942032585ea8ee805937634238d9aee7b210069f4703c88fbe568e26fb78a",
+ "size": 1777930,
+ "hash": "sha256",
+ "digest": "378e973289176ca0c6054054ee7f631a065874a352bf43f0fa60ef079b6ba715",
"format": "tar.gz",
- "path": "ripgrep-14.1.1-aarch64-apple-darwin/rg",
+ "path": "ripgrep-15.1.0-aarch64-apple-darwin/rg",
"providers": [
{
- "url": "https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-aarch64-apple-darwin.tar.gz"
+ "url": "https://github.com/BurntSushi/ripgrep/releases/download/15.1.0/ripgrep-15.1.0-aarch64-apple-darwin.tar.gz"
}
]
},
"linux-aarch64": {
- "size": 2047405,
- "hash": "blake3",
- "digest": "0b670b8fa0a3df2762af2fc82cc4932f684ca4c02dbd1260d4f3133fd4b2a515",
+ "size": 1869959,
+ "hash": "sha256",
+ "digest": "2b661c6ef508e902f388e9098d9c4c5aca72c87b55922d94abdba830b4dc885e",
"format": "tar.gz",
- "path": "ripgrep-14.1.1-aarch64-unknown-linux-gnu/rg",
+ "path": "ripgrep-15.1.0-aarch64-unknown-linux-gnu/rg",
"providers": [
{
- "url": "https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-aarch64-unknown-linux-gnu.tar.gz"
+ "url": "https://github.com/BurntSushi/ripgrep/releases/download/15.1.0/ripgrep-15.1.0-aarch64-unknown-linux-gnu.tar.gz"
}
]
},
"macos-x86_64": {
- "size": 2082672,
- "hash": "blake3",
- "digest": "e9b862fc8da3127f92791f0ff6a799504154ca9d36c98bf3e60a81c6b1f7289e",
+ "size": 1894127,
+ "hash": "sha256",
+ "digest": "64811cb24e77cac3057d6c40b63ac9becf9082eedd54ca411b475b755d334882",
"format": "tar.gz",
- "path": "ripgrep-14.1.1-x86_64-apple-darwin/rg",
+ "path": "ripgrep-15.1.0-x86_64-apple-darwin/rg",
"providers": [
{
- "url": "https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-x86_64-apple-darwin.tar.gz"
+ "url": "https://github.com/BurntSushi/ripgrep/releases/download/15.1.0/ripgrep-15.1.0-x86_64-apple-darwin.tar.gz"
}
]
},
"linux-x86_64": {
- "size": 2566310,
- "hash": "blake3",
- "digest": "f73cca4e54d78c31f832c7f6e2c0b4db8b04fa3eaa747915727d570893dbee76",
+ "size": 2263077,
+ "hash": "sha256",
+ "digest": "1c9297be4a084eea7ecaedf93eb03d058d6faae29bbc57ecdaf5063921491599",
"format": "tar.gz",
- "path": "ripgrep-14.1.1-x86_64-unknown-linux-musl/rg",
+ "path": "ripgrep-15.1.0-x86_64-unknown-linux-musl/rg",
"providers": [
{
- "url": "https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-x86_64-unknown-linux-musl.tar.gz"
+ "url": "https://github.com/BurntSushi/ripgrep/releases/download/15.1.0/ripgrep-15.1.0-x86_64-unknown-linux-musl.tar.gz"
}
]
},
"windows-x86_64": {
- "size": 2058893,
- "hash": "blake3",
- "digest": "a8ce1a6fed4f8093ee997e57f33254e94b2cd18e26358b09db599c89882eadbd",
+ "size": 1810687,
+ "hash": "sha256",
+ "digest": "124510b94b6baa3380d051fdf4650eaa80a302c876d611e9dba0b2e18d87493a",
"format": "zip",
- "path": "ripgrep-14.1.1-x86_64-pc-windows-msvc/rg.exe",
+ "path": "ripgrep-15.1.0-x86_64-pc-windows-msvc/rg.exe",
"providers": [
{
- "url": "https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-x86_64-pc-windows-msvc.zip"
+ "url": "https://github.com/BurntSushi/ripgrep/releases/download/15.1.0/ripgrep-15.1.0-x86_64-pc-windows-msvc.zip"
}
]
},
"windows-aarch64": {
- "size": 1667740,
- "hash": "blake3",
- "digest": "47b971a8c4fca1d23a4e7c19bd4d88465ebc395598458133139406d3bf85f3fa",
+ "size": 1675460,
+ "hash": "sha256",
+ "digest": "00d931fb5237c9696ca49308818edb76d8eb6fc132761cb2a1bd616b2df02f8e",
"format": "zip",
- "path": "rg.exe",
+ "path": "ripgrep-15.1.0-aarch64-pc-windows-msvc/rg.exe",
"providers": [
{
- "url": "https://github.com/microsoft/ripgrep-prebuilt/releases/download/v13.0.0-13/ripgrep-v13.0.0-13-aarch64-pc-windows-msvc.zip"
+ "url": "https://github.com/BurntSushi/ripgrep/releases/download/15.1.0/ripgrep-15.1.0-aarch64-pc-windows-msvc.zip"
}
]
}
diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock
index 224502c38a..189d9468d2 100644
--- a/codex-rs/Cargo.lock
+++ b/codex-rs/Cargo.lock
@@ -423,16 +423,16 @@ dependencies = [
"objc2-foundation",
"parking_lot",
"percent-encoding",
- "windows-sys 0.60.2",
+ "windows-sys 0.52.0",
"wl-clipboard-rs",
"x11rb",
]
[[package]]
name = "arc-swap"
-version = "1.8.0"
+version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e"
+checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5"
dependencies = [
"rustversion",
]
@@ -1227,9 +1227,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.5.56"
+version = "4.5.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e"
+checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806"
dependencies = [
"clap_builder",
"clap_derive",
@@ -1237,9 +1237,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.5.56"
+version = "4.5.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0"
+checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2"
dependencies = [
"anstream",
"anstyle",
@@ -1271,9 +1271,9 @@ dependencies = [
[[package]]
name = "clap_lex"
-version = "0.7.7"
+version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
+checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "clipboard-win"
@@ -1712,6 +1712,7 @@ dependencies = [
"include_dir",
"indexmap 2.13.0",
"indoc",
+ "insta",
"keyring",
"landlock",
"libc",
@@ -3292,7 +3293,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.2",
- "windows-sys 0.61.2",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -3488,9 +3489,9 @@ checksum = "dbfd0e7fc632dec5e6c9396a27bc9f9975b4e039720e1fd3e34021d3ce28c415"
[[package]]
name = "env_filter"
-version = "0.1.4"
+version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
+checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f"
dependencies = [
"log",
"regex",
@@ -3504,9 +3505,9 @@ checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
[[package]]
name = "env_logger"
-version = "0.11.8"
+version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
+checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d"
dependencies = [
"anstream",
"anstyle",
@@ -3537,7 +3538,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -4930,7 +4931,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -5638,7 +5639,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -6204,7 +6205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
- "windows-sys 0.61.2",
+ "windows-sys 0.45.0",
]
[[package]]
@@ -6765,7 +6766,7 @@ dependencies = [
"once_cell",
"socket2 0.6.2",
"tracing",
- "windows-sys 0.60.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -7591,7 +7592,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.11.0",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -8968,7 +8969,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix 1.1.3",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -10328,7 +10329,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys 0.48.0",
]
[[package]]
diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml
index 75b3d18d3e..7ad0566efd 100644
--- a/codex-rs/Cargo.toml
+++ b/codex-rs/Cargo.toml
@@ -170,7 +170,7 @@ dotenvy = "0.15.7"
dunce = "1.0.4"
encoding_rs = "0.8.35"
env-flags = "0.1.1"
-env_logger = "0.11.5"
+env_logger = "0.11.9"
eventsource-stream = "0.2.3"
futures = { version = "0.3", default-features = false }
globset = "0.4"
diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json
index 4eec3e9d44..d5ce1e2f87 100644
--- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json
+++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json
@@ -711,6 +711,15 @@
],
"type": "object"
},
+ "HazelnutScope": {
+ "enum": [
+ "example",
+ "workspace-shared",
+ "all-shared",
+ "personal"
+ ],
+ "type": "string"
+ },
"InitializeCapabilities": {
"description": "Client-declared capabilities negotiated during initialize.",
"properties": {
@@ -1128,6 +1137,13 @@
"null"
]
},
+ "includeHidden": {
+ "description": "When true, include models that are hidden from the default picker list.",
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
"limit": {
"description": "Optional page size; defaults to a reasonable server-side value.",
"format": "uint32",
@@ -1243,6 +1259,15 @@
],
"type": "string"
},
+ "ProductSurface": {
+ "enum": [
+ "chatgpt",
+ "codex",
+ "api",
+ "atlas"
+ ],
+ "type": "string"
+ },
"ReadOnlyAccess": {
"oneOf": [
{
@@ -2414,20 +2439,38 @@
"type": "object"
},
"SkillsRemoteReadParams": {
+ "properties": {
+ "enabled": {
+ "default": false,
+ "type": "boolean"
+ },
+ "hazelnutScope": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/HazelnutScope"
+ }
+ ],
+ "default": "example"
+ },
+ "productSurface": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/ProductSurface"
+ }
+ ],
+ "default": "codex"
+ }
+ },
"type": "object"
},
"SkillsRemoteWriteParams": {
"properties": {
"hazelnutId": {
"type": "string"
- },
- "isPreload": {
- "type": "boolean"
}
},
"required": [
- "hazelnutId",
- "isPreload"
+ "hazelnutId"
],
"type": "object"
},
@@ -3592,9 +3635,9 @@
},
"method": {
"enum": [
- "skills/remote/read"
+ "skills/remote/list"
],
- "title": "Skills/remote/readRequestMethod",
+ "title": "Skills/remote/listRequestMethod",
"type": "string"
},
"params": {
@@ -3606,7 +3649,7 @@
"method",
"params"
],
- "title": "Skills/remote/readRequest",
+ "title": "Skills/remote/listRequest",
"type": "object"
},
{
@@ -3616,9 +3659,9 @@
},
"method": {
"enum": [
- "skills/remote/write"
+ "skills/remote/export"
],
- "title": "Skills/remote/writeRequestMethod",
+ "title": "Skills/remote/exportRequestMethod",
"type": "string"
},
"params": {
@@ -3630,7 +3673,7 @@
"method",
"params"
],
- "title": "Skills/remote/writeRequest",
+ "title": "Skills/remote/exportRequest",
"type": "object"
},
{
@@ -4713,4 +4756,4 @@
}
],
"title": "ClientRequest"
-}
\ No newline at end of file
+}
diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json
index 6af66294c8..5ec61285ca 100644
--- a/codex-rs/app-server-protocol/schema/json/EventMsg.json
+++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json
@@ -473,6 +473,35 @@
"title": "WarningEventMsg",
"type": "object"
},
+ {
+ "description": "Model routing changed from the requested model to a different model.",
+ "properties": {
+ "from_model": {
+ "type": "string"
+ },
+ "reason": {
+ "$ref": "#/definitions/ModelRerouteReason"
+ },
+ "to_model": {
+ "type": "string"
+ },
+ "type": {
+ "enum": [
+ "model_reroute"
+ ],
+ "title": "ModelRerouteEventMsgType",
+ "type": "string"
+ }
+ },
+ "required": [
+ "from_model",
+ "reason",
+ "to_model",
+ "type"
+ ],
+ "title": "ModelRerouteEventMsg",
+ "type": "object"
+ },
{
"description": "Conversation history was compacted (either automatically or manually).",
"properties": {
@@ -1438,6 +1467,17 @@
"description": "The command's working directory.",
"type": "string"
},
+ "network_approval_context": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/NetworkApprovalContext"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional network context for a blocked request that can be approved."
+ },
"parsed_cmd": {
"items": {
"$ref": "#/definitions/ParsedCommand"
@@ -3307,6 +3347,12 @@
],
"type": "string"
},
+ "ModelRerouteReason": {
+ "enum": [
+ "high_risk_cyber_activity"
+ ],
+ "type": "string"
+ },
"NetworkAccess": {
"description": "Represents whether outbound network access is available to the agent.",
"enum": [
@@ -3315,6 +3361,30 @@
],
"type": "string"
},
+ "NetworkApprovalContext": {
+ "properties": {
+ "host": {
+ "type": "string"
+ },
+ "protocol": {
+ "$ref": "#/definitions/NetworkApprovalProtocol"
+ }
+ },
+ "required": [
+ "host",
+ "protocol"
+ ],
+ "type": "object"
+ },
+ "NetworkApprovalProtocol": {
+ "enum": [
+ "http",
+ "https",
+ "socks5_tcp",
+ "socks5_udp"
+ ],
+ "type": "string"
+ },
"ParsedCommand": {
"oneOf": [
{
@@ -5343,6 +5413,35 @@
"title": "WarningEventMsg",
"type": "object"
},
+ {
+ "description": "Model routing changed from the requested model to a different model.",
+ "properties": {
+ "from_model": {
+ "type": "string"
+ },
+ "reason": {
+ "$ref": "#/definitions/ModelRerouteReason"
+ },
+ "to_model": {
+ "type": "string"
+ },
+ "type": {
+ "enum": [
+ "model_reroute"
+ ],
+ "title": "ModelRerouteEventMsgType",
+ "type": "string"
+ }
+ },
+ "required": [
+ "from_model",
+ "reason",
+ "to_model",
+ "type"
+ ],
+ "title": "ModelRerouteEventMsg",
+ "type": "object"
+ },
{
"description": "Conversation history was compacted (either automatically or manually).",
"properties": {
@@ -6308,6 +6407,17 @@
"description": "The command's working directory.",
"type": "string"
},
+ "network_approval_context": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/NetworkApprovalContext"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional network context for a blocked request that can be approved."
+ },
"parsed_cmd": {
"items": {
"$ref": "#/definitions/ParsedCommand"
diff --git a/codex-rs/app-server-protocol/schema/json/FuzzyFileSearchSessionCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/FuzzyFileSearchSessionCompletedNotification.json
new file mode 100644
index 0000000000..c8924e77ca
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/json/FuzzyFileSearchSessionCompletedNotification.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "properties": {
+ "sessionId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "sessionId"
+ ],
+ "title": "FuzzyFileSearchSessionCompletedNotification",
+ "type": "object"
+}
\ No newline at end of file
diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json
index 3277e5650f..085bbee050 100644
--- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json
+++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json
@@ -165,9 +165,71 @@
}
]
},
+ "AppBranding": {
+ "description": "EXPERIMENTAL - app metadata returned by app-list APIs.",
+ "properties": {
+ "category": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "developer": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "isDiscoverableApp": {
+ "type": "boolean"
+ },
+ "privacyPolicy": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "termsOfService": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "website": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "required": [
+ "isDiscoverableApp"
+ ],
+ "type": "object"
+ },
"AppInfo": {
"description": "EXPERIMENTAL - app metadata returned by app-list APIs.",
"properties": {
+ "appMetadata": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/AppMetadata"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "branding": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/AppBranding"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"description": {
"type": [
"string",
@@ -198,6 +260,15 @@
"description": "Whether this app is enabled in config.toml. Example: ```toml [apps.bad_app] enabled = false ```",
"type": "boolean"
},
+ "labels": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": [
+ "object",
+ "null"
+ ]
+ },
"logoUrl": {
"type": [
"string",
@@ -235,6 +306,130 @@
],
"type": "object"
},
+ "AppMetadata": {
+ "properties": {
+ "categories": {
+ "items": {
+ "type": "string"
+ },
+ "type": [
+ "array",
+ "null"
+ ]
+ },
+ "developer": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "firstPartyRequiresInstall": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "firstPartyType": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "review": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/AppReview"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "screenshots": {
+ "items": {
+ "$ref": "#/definitions/AppScreenshot"
+ },
+ "type": [
+ "array",
+ "null"
+ ]
+ },
+ "seoDescription": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "showInComposerWhenUnlinked": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "subCategories": {
+ "items": {
+ "type": "string"
+ },
+ "type": [
+ "array",
+ "null"
+ ]
+ },
+ "version": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "versionId": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "versionNotes": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "type": "object"
+ },
+ "AppReview": {
+ "properties": {
+ "status": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "status"
+ ],
+ "type": "object"
+ },
+ "AppScreenshot": {
+ "properties": {
+ "fileId": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "userPrompt": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "userPrompt"
+ ],
+ "type": "object"
+ },
"AskForApproval": {
"description": "Determines the conditions under which the user is consulted to approve running the command proposed by Codex.",
"oneOf": [
@@ -1094,6 +1289,35 @@
"title": "WarningEventMsg",
"type": "object"
},
+ {
+ "description": "Model routing changed from the requested model to a different model.",
+ "properties": {
+ "from_model": {
+ "type": "string"
+ },
+ "reason": {
+ "$ref": "#/definitions/ModelRerouteReason2"
+ },
+ "to_model": {
+ "type": "string"
+ },
+ "type": {
+ "enum": [
+ "model_reroute"
+ ],
+ "title": "ModelRerouteEventMsgType",
+ "type": "string"
+ }
+ },
+ "required": [
+ "from_model",
+ "reason",
+ "to_model",
+ "type"
+ ],
+ "title": "ModelRerouteEventMsg",
+ "type": "object"
+ },
{
"description": "Conversation history was compacted (either automatically or manually).",
"properties": {
@@ -2059,6 +2283,17 @@
"description": "The command's working directory.",
"type": "string"
},
+ "network_approval_context": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/NetworkApprovalContext"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional network context for a blocked request that can be approved."
+ },
"parsed_cmd": {
"items": {
"$ref": "#/definitions/ParsedCommand"
@@ -3752,6 +3987,17 @@
],
"type": "object"
},
+ "FuzzyFileSearchSessionCompletedNotification": {
+ "properties": {
+ "sessionId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "sessionId"
+ ],
+ "type": "object"
+ },
"FuzzyFileSearchSessionUpdatedNotification": {
"properties": {
"files": {
@@ -4188,6 +4434,45 @@
],
"type": "string"
},
+ "ModelRerouteReason": {
+ "enum": [
+ "highRiskCyberActivity"
+ ],
+ "type": "string"
+ },
+ "ModelRerouteReason2": {
+ "enum": [
+ "high_risk_cyber_activity"
+ ],
+ "type": "string"
+ },
+ "ModelReroutedNotification": {
+ "properties": {
+ "fromModel": {
+ "type": "string"
+ },
+ "reason": {
+ "$ref": "#/definitions/ModelRerouteReason"
+ },
+ "threadId": {
+ "type": "string"
+ },
+ "toModel": {
+ "type": "string"
+ },
+ "turnId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "fromModel",
+ "reason",
+ "threadId",
+ "toModel",
+ "turnId"
+ ],
+ "type": "object"
+ },
"NetworkAccess": {
"description": "Represents whether outbound network access is available to the agent.",
"enum": [
@@ -4196,6 +4481,30 @@
],
"type": "string"
},
+ "NetworkApprovalContext": {
+ "properties": {
+ "host": {
+ "type": "string"
+ },
+ "protocol": {
+ "$ref": "#/definitions/NetworkApprovalProtocol"
+ }
+ },
+ "required": [
+ "host",
+ "protocol"
+ ],
+ "type": "object"
+ },
+ "NetworkApprovalProtocol": {
+ "enum": [
+ "http",
+ "https",
+ "socks5_tcp",
+ "socks5_udp"
+ ],
+ "type": "string"
+ },
"ParsedCommand": {
"oneOf": [
{
@@ -8230,6 +8539,26 @@
"title": "Thread/compactedNotification",
"type": "object"
},
+ {
+ "properties": {
+ "method": {
+ "enum": [
+ "model/rerouted"
+ ],
+ "title": "Model/reroutedNotificationMethod",
+ "type": "string"
+ },
+ "params": {
+ "$ref": "#/definitions/ModelReroutedNotification"
+ }
+ },
+ "required": [
+ "method",
+ "params"
+ ],
+ "title": "Model/reroutedNotification",
+ "type": "object"
+ },
{
"properties": {
"method": {
@@ -8290,6 +8619,26 @@
"title": "FuzzyFileSearch/sessionUpdatedNotification",
"type": "object"
},
+ {
+ "properties": {
+ "method": {
+ "enum": [
+ "fuzzyFileSearch/sessionCompleted"
+ ],
+ "title": "FuzzyFileSearch/sessionCompletedNotificationMethod",
+ "type": "string"
+ },
+ "params": {
+ "$ref": "#/definitions/FuzzyFileSearchSessionCompletedNotification"
+ }
+ },
+ "required": [
+ "method",
+ "params"
+ ],
+ "title": "FuzzyFileSearch/sessionCompletedNotification",
+ "type": "object"
+ },
{
"description": "Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox.",
"properties": {
diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
index d3a65af96e..4e2756e486 100644
--- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
+++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
@@ -755,9 +755,9 @@
},
"method": {
"enum": [
- "skills/remote/read"
+ "skills/remote/list"
],
- "title": "Skills/remote/readRequestMethod",
+ "title": "Skills/remote/listRequestMethod",
"type": "string"
},
"params": {
@@ -769,7 +769,7 @@
"method",
"params"
],
- "title": "Skills/remote/readRequest",
+ "title": "Skills/remote/listRequest",
"type": "object"
},
{
@@ -779,9 +779,9 @@
},
"method": {
"enum": [
- "skills/remote/write"
+ "skills/remote/export"
],
- "title": "Skills/remote/writeRequestMethod",
+ "title": "Skills/remote/exportRequestMethod",
"type": "string"
},
"params": {
@@ -793,7 +793,7 @@
"method",
"params"
],
- "title": "Skills/remote/writeRequest",
+ "title": "Skills/remote/exportRequest",
"type": "object"
},
{
@@ -2486,6 +2486,35 @@
"title": "WarningEventMsg",
"type": "object"
},
+ {
+ "description": "Model routing changed from the requested model to a different model.",
+ "properties": {
+ "from_model": {
+ "type": "string"
+ },
+ "reason": {
+ "$ref": "#/definitions/v2/ModelRerouteReason"
+ },
+ "to_model": {
+ "type": "string"
+ },
+ "type": {
+ "enum": [
+ "model_reroute"
+ ],
+ "title": "ModelRerouteEventMsgType",
+ "type": "string"
+ }
+ },
+ "required": [
+ "from_model",
+ "reason",
+ "to_model",
+ "type"
+ ],
+ "title": "ModelRerouteEventMsg",
+ "type": "object"
+ },
{
"description": "Conversation history was compacted (either automatically or manually).",
"properties": {
@@ -3451,6 +3480,17 @@
"description": "The command's working directory.",
"type": "string"
},
+ "network_approval_context": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/NetworkApprovalContext"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional network context for a blocked request that can be approved."
+ },
"parsed_cmd": {
"items": {
"$ref": "#/definitions/ParsedCommand"
@@ -5412,6 +5452,19 @@
],
"type": "object"
},
+ "FuzzyFileSearchSessionCompletedNotification": {
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "properties": {
+ "sessionId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "sessionId"
+ ],
+ "title": "FuzzyFileSearchSessionCompletedNotification",
+ "type": "object"
+ },
"FuzzyFileSearchSessionUpdatedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -6242,6 +6295,12 @@
],
"type": "string"
},
+ "ModelRerouteReason": {
+ "enum": [
+ "high_risk_cyber_activity"
+ ],
+ "type": "string"
+ },
"NetworkAccess": {
"description": "Represents whether outbound network access is available to the agent.",
"enum": [
@@ -6250,6 +6309,30 @@
],
"type": "string"
},
+ "NetworkApprovalContext": {
+ "properties": {
+ "host": {
+ "type": "string"
+ },
+ "protocol": {
+ "$ref": "#/definitions/NetworkApprovalProtocol"
+ }
+ },
+ "required": [
+ "host",
+ "protocol"
+ ],
+ "type": "object"
+ },
+ "NetworkApprovalProtocol": {
+ "enum": [
+ "http",
+ "https",
+ "socks5_tcp",
+ "socks5_udp"
+ ],
+ "type": "string"
+ },
"NewConversationParams": {
"properties": {
"approvalPolicy": {
@@ -8443,6 +8526,26 @@
"title": "Thread/compactedNotification",
"type": "object"
},
+ {
+ "properties": {
+ "method": {
+ "enum": [
+ "model/rerouted"
+ ],
+ "title": "Model/reroutedNotificationMethod",
+ "type": "string"
+ },
+ "params": {
+ "$ref": "#/definitions/v2/ModelReroutedNotification"
+ }
+ },
+ "required": [
+ "method",
+ "params"
+ ],
+ "title": "Model/reroutedNotification",
+ "type": "object"
+ },
{
"properties": {
"method": {
@@ -8503,6 +8606,26 @@
"title": "FuzzyFileSearch/sessionUpdatedNotification",
"type": "object"
},
+ {
+ "properties": {
+ "method": {
+ "enum": [
+ "fuzzyFileSearch/sessionCompleted"
+ ],
+ "title": "FuzzyFileSearch/sessionCompletedNotificationMethod",
+ "type": "string"
+ },
+ "params": {
+ "$ref": "#/definitions/FuzzyFileSearchSessionCompletedNotification"
+ }
+ },
+ "required": [
+ "method",
+ "params"
+ ],
+ "title": "FuzzyFileSearch/sessionCompletedNotification",
+ "type": "object"
+ },
{
"description": "Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox.",
"properties": {
@@ -10148,6 +10271,48 @@
},
"type": "object"
},
+ "AppBranding": {
+ "description": "EXPERIMENTAL - app metadata returned by app-list APIs.",
+ "properties": {
+ "category": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "developer": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "isDiscoverableApp": {
+ "type": "boolean"
+ },
+ "privacyPolicy": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "termsOfService": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "website": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "required": [
+ "isDiscoverableApp"
+ ],
+ "type": "object"
+ },
"AppConfig": {
"properties": {
"disable_destructive": {
@@ -10200,6 +10365,26 @@
"AppInfo": {
"description": "EXPERIMENTAL - app metadata returned by app-list APIs.",
"properties": {
+ "appMetadata": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/v2/AppMetadata"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "branding": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/v2/AppBranding"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"description": {
"type": [
"string",
@@ -10230,6 +10415,15 @@
"description": "Whether this app is enabled in config.toml. Example: ```toml [apps.bad_app] enabled = false ```",
"type": "boolean"
},
+ "labels": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": [
+ "object",
+ "null"
+ ]
+ },
"logoUrl": {
"type": [
"string",
@@ -10269,6 +10463,130 @@
"title": "AppListUpdatedNotification",
"type": "object"
},
+ "AppMetadata": {
+ "properties": {
+ "categories": {
+ "items": {
+ "type": "string"
+ },
+ "type": [
+ "array",
+ "null"
+ ]
+ },
+ "developer": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "firstPartyRequiresInstall": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "firstPartyType": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "review": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/v2/AppReview"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "screenshots": {
+ "items": {
+ "$ref": "#/definitions/v2/AppScreenshot"
+ },
+ "type": [
+ "array",
+ "null"
+ ]
+ },
+ "seoDescription": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "showInComposerWhenUnlinked": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "subCategories": {
+ "items": {
+ "type": "string"
+ },
+ "type": [
+ "array",
+ "null"
+ ]
+ },
+ "version": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "versionId": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "versionNotes": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "type": "object"
+ },
+ "AppReview": {
+ "properties": {
+ "status": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "status"
+ ],
+ "type": "object"
+ },
+ "AppScreenshot": {
+ "properties": {
+ "fileId": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "userPrompt": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "userPrompt"
+ ],
+ "type": "object"
+ },
"AppToolApproval": {
"enum": [
"auto",
@@ -12164,6 +12482,15 @@
},
"type": "object"
},
+ "HazelnutScope": {
+ "enum": [
+ "example",
+ "workspace-shared",
+ "all-shared",
+ "personal"
+ ],
+ "type": "string"
+ },
"InputModality": {
"description": "Canonical user-input modality tags advertised by a model.",
"oneOf": [
@@ -12696,6 +13023,9 @@
"displayName": {
"type": "string"
},
+ "hidden": {
+ "type": "boolean"
+ },
"id": {
"type": "string"
},
@@ -12736,6 +13066,7 @@
"defaultReasoningEffort",
"description",
"displayName",
+ "hidden",
"id",
"isDefault",
"model",
@@ -12753,6 +13084,13 @@
"null"
]
},
+ "includeHidden": {
+ "description": "When true, include models that are hidden from the default picker list.",
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
"limit": {
"description": "Optional page size; defaults to a reasonable server-side value.",
"format": "uint32",
@@ -12789,6 +13127,41 @@
"title": "ModelListResponse",
"type": "object"
},
+ "ModelRerouteReason": {
+ "enum": [
+ "highRiskCyberActivity"
+ ],
+ "type": "string"
+ },
+ "ModelReroutedNotification": {
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "properties": {
+ "fromModel": {
+ "type": "string"
+ },
+ "reason": {
+ "$ref": "#/definitions/v2/ModelRerouteReason"
+ },
+ "threadId": {
+ "type": "string"
+ },
+ "toModel": {
+ "type": "string"
+ },
+ "turnId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "fromModel",
+ "reason",
+ "threadId",
+ "toModel",
+ "turnId"
+ ],
+ "title": "ModelReroutedNotification",
+ "type": "object"
+ },
"NetworkAccess": {
"enum": [
"restricted",
@@ -13006,6 +13379,15 @@
],
"type": "string"
},
+ "ProductSurface": {
+ "enum": [
+ "chatgpt",
+ "codex",
+ "api",
+ "atlas"
+ ],
+ "type": "string"
+ },
"ProfileV2": {
"additionalProperties": true,
"properties": {
@@ -14537,6 +14919,28 @@
},
"SkillsRemoteReadParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
+ "properties": {
+ "enabled": {
+ "default": false,
+ "type": "boolean"
+ },
+ "hazelnutScope": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/v2/HazelnutScope"
+ }
+ ],
+ "default": "example"
+ },
+ "productSurface": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/v2/ProductSurface"
+ }
+ ],
+ "default": "codex"
+ }
+ },
"title": "SkillsRemoteReadParams",
"type": "object"
},
@@ -14561,14 +14965,10 @@
"properties": {
"hazelnutId": {
"type": "string"
- },
- "isPreload": {
- "type": "boolean"
}
},
"required": [
- "hazelnutId",
- "isPreload"
+ "hazelnutId"
],
"title": "SkillsRemoteWriteParams",
"type": "object"
@@ -14579,16 +14979,12 @@
"id": {
"type": "string"
},
- "name": {
- "type": "string"
- },
"path": {
"type": "string"
}
},
"required": [
"id",
- "name",
"path"
],
"title": "SkillsRemoteWriteResponse",
diff --git a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json
index 12db241e1a..a73c550f69 100644
--- a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json
@@ -473,6 +473,35 @@
"title": "WarningEventMsg",
"type": "object"
},
+ {
+ "description": "Model routing changed from the requested model to a different model.",
+ "properties": {
+ "from_model": {
+ "type": "string"
+ },
+ "reason": {
+ "$ref": "#/definitions/ModelRerouteReason"
+ },
+ "to_model": {
+ "type": "string"
+ },
+ "type": {
+ "enum": [
+ "model_reroute"
+ ],
+ "title": "ModelRerouteEventMsgType",
+ "type": "string"
+ }
+ },
+ "required": [
+ "from_model",
+ "reason",
+ "to_model",
+ "type"
+ ],
+ "title": "ModelRerouteEventMsg",
+ "type": "object"
+ },
{
"description": "Conversation history was compacted (either automatically or manually).",
"properties": {
@@ -1438,6 +1467,17 @@
"description": "The command's working directory.",
"type": "string"
},
+ "network_approval_context": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/NetworkApprovalContext"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional network context for a blocked request that can be approved."
+ },
"parsed_cmd": {
"items": {
"$ref": "#/definitions/ParsedCommand"
@@ -3307,6 +3347,12 @@
],
"type": "string"
},
+ "ModelRerouteReason": {
+ "enum": [
+ "high_risk_cyber_activity"
+ ],
+ "type": "string"
+ },
"NetworkAccess": {
"description": "Represents whether outbound network access is available to the agent.",
"enum": [
@@ -3315,6 +3361,30 @@
],
"type": "string"
},
+ "NetworkApprovalContext": {
+ "properties": {
+ "host": {
+ "type": "string"
+ },
+ "protocol": {
+ "$ref": "#/definitions/NetworkApprovalProtocol"
+ }
+ },
+ "required": [
+ "host",
+ "protocol"
+ ],
+ "type": "object"
+ },
+ "NetworkApprovalProtocol": {
+ "enum": [
+ "http",
+ "https",
+ "socks5_tcp",
+ "socks5_udp"
+ ],
+ "type": "string"
+ },
"ParsedCommand": {
"oneOf": [
{
diff --git a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json
index dd7606b5e0..6dc02934d3 100644
--- a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json
@@ -473,6 +473,35 @@
"title": "WarningEventMsg",
"type": "object"
},
+ {
+ "description": "Model routing changed from the requested model to a different model.",
+ "properties": {
+ "from_model": {
+ "type": "string"
+ },
+ "reason": {
+ "$ref": "#/definitions/ModelRerouteReason"
+ },
+ "to_model": {
+ "type": "string"
+ },
+ "type": {
+ "enum": [
+ "model_reroute"
+ ],
+ "title": "ModelRerouteEventMsgType",
+ "type": "string"
+ }
+ },
+ "required": [
+ "from_model",
+ "reason",
+ "to_model",
+ "type"
+ ],
+ "title": "ModelRerouteEventMsg",
+ "type": "object"
+ },
{
"description": "Conversation history was compacted (either automatically or manually).",
"properties": {
@@ -1438,6 +1467,17 @@
"description": "The command's working directory.",
"type": "string"
},
+ "network_approval_context": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/NetworkApprovalContext"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional network context for a blocked request that can be approved."
+ },
"parsed_cmd": {
"items": {
"$ref": "#/definitions/ParsedCommand"
@@ -3307,6 +3347,12 @@
],
"type": "string"
},
+ "ModelRerouteReason": {
+ "enum": [
+ "high_risk_cyber_activity"
+ ],
+ "type": "string"
+ },
"NetworkAccess": {
"description": "Represents whether outbound network access is available to the agent.",
"enum": [
@@ -3315,6 +3361,30 @@
],
"type": "string"
},
+ "NetworkApprovalContext": {
+ "properties": {
+ "host": {
+ "type": "string"
+ },
+ "protocol": {
+ "$ref": "#/definitions/NetworkApprovalProtocol"
+ }
+ },
+ "required": [
+ "host",
+ "protocol"
+ ],
+ "type": "object"
+ },
+ "NetworkApprovalProtocol": {
+ "enum": [
+ "http",
+ "https",
+ "socks5_tcp",
+ "socks5_udp"
+ ],
+ "type": "string"
+ },
"ParsedCommand": {
"oneOf": [
{
diff --git a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json
index 89ab825d32..69967e6853 100644
--- a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json
+++ b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json
@@ -473,6 +473,35 @@
"title": "WarningEventMsg",
"type": "object"
},
+ {
+ "description": "Model routing changed from the requested model to a different model.",
+ "properties": {
+ "from_model": {
+ "type": "string"
+ },
+ "reason": {
+ "$ref": "#/definitions/ModelRerouteReason"
+ },
+ "to_model": {
+ "type": "string"
+ },
+ "type": {
+ "enum": [
+ "model_reroute"
+ ],
+ "title": "ModelRerouteEventMsgType",
+ "type": "string"
+ }
+ },
+ "required": [
+ "from_model",
+ "reason",
+ "to_model",
+ "type"
+ ],
+ "title": "ModelRerouteEventMsg",
+ "type": "object"
+ },
{
"description": "Conversation history was compacted (either automatically or manually).",
"properties": {
@@ -1438,6 +1467,17 @@
"description": "The command's working directory.",
"type": "string"
},
+ "network_approval_context": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/NetworkApprovalContext"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional network context for a blocked request that can be approved."
+ },
"parsed_cmd": {
"items": {
"$ref": "#/definitions/ParsedCommand"
@@ -3307,6 +3347,12 @@
],
"type": "string"
},
+ "ModelRerouteReason": {
+ "enum": [
+ "high_risk_cyber_activity"
+ ],
+ "type": "string"
+ },
"NetworkAccess": {
"description": "Represents whether outbound network access is available to the agent.",
"enum": [
@@ -3315,6 +3361,30 @@
],
"type": "string"
},
+ "NetworkApprovalContext": {
+ "properties": {
+ "host": {
+ "type": "string"
+ },
+ "protocol": {
+ "$ref": "#/definitions/NetworkApprovalProtocol"
+ }
+ },
+ "required": [
+ "host",
+ "protocol"
+ ],
+ "type": "object"
+ },
+ "NetworkApprovalProtocol": {
+ "enum": [
+ "http",
+ "https",
+ "socks5_tcp",
+ "socks5_udp"
+ ],
+ "type": "string"
+ },
"ParsedCommand": {
"oneOf": [
{
diff --git a/codex-rs/app-server-protocol/schema/json/v2/AppListUpdatedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/AppListUpdatedNotification.json
index 1638472c4e..0813ed6f56 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/AppListUpdatedNotification.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/AppListUpdatedNotification.json
@@ -1,9 +1,71 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
+ "AppBranding": {
+ "description": "EXPERIMENTAL - app metadata returned by app-list APIs.",
+ "properties": {
+ "category": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "developer": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "isDiscoverableApp": {
+ "type": "boolean"
+ },
+ "privacyPolicy": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "termsOfService": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "website": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "required": [
+ "isDiscoverableApp"
+ ],
+ "type": "object"
+ },
"AppInfo": {
"description": "EXPERIMENTAL - app metadata returned by app-list APIs.",
"properties": {
+ "appMetadata": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/AppMetadata"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "branding": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/AppBranding"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"description": {
"type": [
"string",
@@ -34,6 +96,15 @@
"description": "Whether this app is enabled in config.toml. Example: ```toml [apps.bad_app] enabled = false ```",
"type": "boolean"
},
+ "labels": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": [
+ "object",
+ "null"
+ ]
+ },
"logoUrl": {
"type": [
"string",
@@ -55,6 +126,130 @@
"name"
],
"type": "object"
+ },
+ "AppMetadata": {
+ "properties": {
+ "categories": {
+ "items": {
+ "type": "string"
+ },
+ "type": [
+ "array",
+ "null"
+ ]
+ },
+ "developer": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "firstPartyRequiresInstall": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "firstPartyType": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "review": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/AppReview"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "screenshots": {
+ "items": {
+ "$ref": "#/definitions/AppScreenshot"
+ },
+ "type": [
+ "array",
+ "null"
+ ]
+ },
+ "seoDescription": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "showInComposerWhenUnlinked": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "subCategories": {
+ "items": {
+ "type": "string"
+ },
+ "type": [
+ "array",
+ "null"
+ ]
+ },
+ "version": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "versionId": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "versionNotes": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "type": "object"
+ },
+ "AppReview": {
+ "properties": {
+ "status": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "status"
+ ],
+ "type": "object"
+ },
+ "AppScreenshot": {
+ "properties": {
+ "fileId": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "userPrompt": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "userPrompt"
+ ],
+ "type": "object"
}
},
"description": "EXPERIMENTAL - notification emitted when the app list changes.",
diff --git a/codex-rs/app-server-protocol/schema/json/v2/AppsListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/AppsListResponse.json
index cec3da5ccc..4697b34e12 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/AppsListResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/AppsListResponse.json
@@ -1,9 +1,71 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
+ "AppBranding": {
+ "description": "EXPERIMENTAL - app metadata returned by app-list APIs.",
+ "properties": {
+ "category": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "developer": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "isDiscoverableApp": {
+ "type": "boolean"
+ },
+ "privacyPolicy": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "termsOfService": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "website": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "required": [
+ "isDiscoverableApp"
+ ],
+ "type": "object"
+ },
"AppInfo": {
"description": "EXPERIMENTAL - app metadata returned by app-list APIs.",
"properties": {
+ "appMetadata": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/AppMetadata"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "branding": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/AppBranding"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"description": {
"type": [
"string",
@@ -34,6 +96,15 @@
"description": "Whether this app is enabled in config.toml. Example: ```toml [apps.bad_app] enabled = false ```",
"type": "boolean"
},
+ "labels": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": [
+ "object",
+ "null"
+ ]
+ },
"logoUrl": {
"type": [
"string",
@@ -55,6 +126,130 @@
"name"
],
"type": "object"
+ },
+ "AppMetadata": {
+ "properties": {
+ "categories": {
+ "items": {
+ "type": "string"
+ },
+ "type": [
+ "array",
+ "null"
+ ]
+ },
+ "developer": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "firstPartyRequiresInstall": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "firstPartyType": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "review": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/AppReview"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "screenshots": {
+ "items": {
+ "$ref": "#/definitions/AppScreenshot"
+ },
+ "type": [
+ "array",
+ "null"
+ ]
+ },
+ "seoDescription": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "showInComposerWhenUnlinked": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "subCategories": {
+ "items": {
+ "type": "string"
+ },
+ "type": [
+ "array",
+ "null"
+ ]
+ },
+ "version": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "versionId": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "versionNotes": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "type": "object"
+ },
+ "AppReview": {
+ "properties": {
+ "status": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "status"
+ ],
+ "type": "object"
+ },
+ "AppScreenshot": {
+ "properties": {
+ "fileId": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "userPrompt": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "userPrompt"
+ ],
+ "type": "object"
}
},
"description": "EXPERIMENTAL - app list response.",
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ModelListParams.json b/codex-rs/app-server-protocol/schema/json/v2/ModelListParams.json
index 13bb29977e..11a3476240 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ModelListParams.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ModelListParams.json
@@ -8,6 +8,13 @@
"null"
]
},
+ "includeHidden": {
+ "description": "When true, include models that are hidden from the default picker list.",
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
"limit": {
"description": "Optional page size; defaults to a reasonable server-side value.",
"format": "uint32",
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json
index a1b3f7114c..2afa018dc5 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json
@@ -31,6 +31,9 @@
"displayName": {
"type": "string"
},
+ "hidden": {
+ "type": "boolean"
+ },
"id": {
"type": "string"
},
@@ -71,6 +74,7 @@
"defaultReasoningEffort",
"description",
"displayName",
+ "hidden",
"id",
"isDefault",
"model",
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ModelReroutedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ModelReroutedNotification.json
new file mode 100644
index 0000000000..b9bcc491b0
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/json/v2/ModelReroutedNotification.json
@@ -0,0 +1,37 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "ModelRerouteReason": {
+ "enum": [
+ "highRiskCyberActivity"
+ ],
+ "type": "string"
+ }
+ },
+ "properties": {
+ "fromModel": {
+ "type": "string"
+ },
+ "reason": {
+ "$ref": "#/definitions/ModelRerouteReason"
+ },
+ "threadId": {
+ "type": "string"
+ },
+ "toModel": {
+ "type": "string"
+ },
+ "turnId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "fromModel",
+ "reason",
+ "threadId",
+ "toModel",
+ "turnId"
+ ],
+ "title": "ModelReroutedNotification",
+ "type": "object"
+}
\ No newline at end of file
diff --git a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteReadParams.json b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteReadParams.json
index ace2fb5ba7..3fdf78f92b 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteReadParams.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteReadParams.json
@@ -1,5 +1,47 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "HazelnutScope": {
+ "enum": [
+ "example",
+ "workspace-shared",
+ "all-shared",
+ "personal"
+ ],
+ "type": "string"
+ },
+ "ProductSurface": {
+ "enum": [
+ "chatgpt",
+ "codex",
+ "api",
+ "atlas"
+ ],
+ "type": "string"
+ }
+ },
+ "properties": {
+ "enabled": {
+ "default": false,
+ "type": "boolean"
+ },
+ "hazelnutScope": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/HazelnutScope"
+ }
+ ],
+ "default": "example"
+ },
+ "productSurface": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/ProductSurface"
+ }
+ ],
+ "default": "codex"
+ }
+ },
"title": "SkillsRemoteReadParams",
"type": "object"
-}
\ No newline at end of file
+}
diff --git a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteParams.json b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteParams.json
index 871a5a428a..f1a70eeeb0 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteParams.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteParams.json
@@ -3,14 +3,10 @@
"properties": {
"hazelnutId": {
"type": "string"
- },
- "isPreload": {
- "type": "boolean"
}
},
"required": [
- "hazelnutId",
- "isPreload"
+ "hazelnutId"
],
"title": "SkillsRemoteWriteParams",
"type": "object"
diff --git a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteResponse.json b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteResponse.json
index 1a9473d054..d1e85bc5ba 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteResponse.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/SkillsRemoteWriteResponse.json
@@ -4,18 +4,14 @@
"id": {
"type": "string"
},
- "name": {
- "type": "string"
- },
"path": {
"type": "string"
}
},
"required": [
"id",
- "name",
"path"
],
"title": "SkillsRemoteWriteResponse",
"type": "object"
-}
\ No newline at end of file
+}
diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts
index 3b5f6787b1..40757d9d95 100644
--- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts
@@ -57,4 +57,4 @@ import type { TurnSteerParams } from "./v2/TurnSteerParams";
/**
* Request from the client to the server.
*/
-export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/remote/read", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/write", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "newConversation", id: RequestId, params: NewConversationParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "listConversations", id: RequestId, params: ListConversationsParams, } | { "method": "resumeConversation", id: RequestId, params: ResumeConversationParams, } | { "method": "forkConversation", id: RequestId, params: ForkConversationParams, } | { "method": "archiveConversation", id: RequestId, params: ArchiveConversationParams, } | { "method": "sendUserMessage", id: RequestId, params: SendUserMessageParams, } | { "method": "sendUserTurn", id: RequestId, params: SendUserTurnParams, } | { "method": "interruptConversation", id: RequestId, params: InterruptConversationParams, } | { "method": "addConversationListener", id: RequestId, params: AddConversationListenerParams, } | { "method": "removeConversationListener", id: RequestId, params: RemoveConversationListenerParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "loginApiKey", id: RequestId, params: LoginApiKeyParams, } | { "method": "loginChatGpt", id: RequestId, params: undefined, } | { "method": "cancelLoginChatGpt", id: RequestId, params: CancelLoginChatGptParams, } | { "method": "logoutChatGpt", id: RequestId, params: undefined, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "getUserSavedConfig", id: RequestId, params: undefined, } | { "method": "setDefaultModel", id: RequestId, params: SetDefaultModelParams, } | { "method": "getUserAgent", id: RequestId, params: undefined, } | { "method": "userInfo", id: RequestId, params: undefined, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, } | { "method": "execOneOffCommand", id: RequestId, params: ExecOneOffCommandParams, };
+export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/remote/list", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/export", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "newConversation", id: RequestId, params: NewConversationParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "listConversations", id: RequestId, params: ListConversationsParams, } | { "method": "resumeConversation", id: RequestId, params: ResumeConversationParams, } | { "method": "forkConversation", id: RequestId, params: ForkConversationParams, } | { "method": "archiveConversation", id: RequestId, params: ArchiveConversationParams, } | { "method": "sendUserMessage", id: RequestId, params: SendUserMessageParams, } | { "method": "sendUserTurn", id: RequestId, params: SendUserTurnParams, } | { "method": "interruptConversation", id: RequestId, params: InterruptConversationParams, } | { "method": "addConversationListener", id: RequestId, params: AddConversationListenerParams, } | { "method": "removeConversationListener", id: RequestId, params: RemoveConversationListenerParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "loginApiKey", id: RequestId, params: LoginApiKeyParams, } | { "method": "loginChatGpt", id: RequestId, params: undefined, } | { "method": "cancelLoginChatGpt", id: RequestId, params: CancelLoginChatGptParams, } | { "method": "logoutChatGpt", id: RequestId, params: undefined, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "getUserSavedConfig", id: RequestId, params: undefined, } | { "method": "setDefaultModel", id: RequestId, params: SetDefaultModelParams, } | { "method": "getUserAgent", id: RequestId, params: undefined, } | { "method": "userInfo", id: RequestId, params: undefined, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, } | { "method": "execOneOffCommand", id: RequestId, params: ExecOneOffCommandParams, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts b/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts
index 2c0514870f..5ed3ca2529 100644
--- a/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/EventMsg.ts
@@ -42,6 +42,7 @@ import type { McpStartupCompleteEvent } from "./McpStartupCompleteEvent";
import type { McpStartupUpdateEvent } from "./McpStartupUpdateEvent";
import type { McpToolCallBeginEvent } from "./McpToolCallBeginEvent";
import type { McpToolCallEndEvent } from "./McpToolCallEndEvent";
+import type { ModelRerouteEvent } from "./ModelRerouteEvent";
import type { PatchApplyBeginEvent } from "./PatchApplyBeginEvent";
import type { PatchApplyEndEvent } from "./PatchApplyEndEvent";
import type { PlanDeltaEvent } from "./PlanDeltaEvent";
@@ -74,4 +75,4 @@ import type { WebSearchEndEvent } from "./WebSearchEndEvent";
* Response event from the agent
* NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.
*/
-export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent | { "type": "collab_resume_begin" } & CollabResumeBeginEvent | { "type": "collab_resume_end" } & CollabResumeEndEvent;
+export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "model_reroute" } & ModelRerouteEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent | { "type": "collab_resume_begin" } & CollabResumeBeginEvent | { "type": "collab_resume_end" } & CollabResumeEndEvent;
diff --git a/codex-rs/app-server-protocol/schema/typescript/ExecApprovalRequestEvent.ts b/codex-rs/app-server-protocol/schema/typescript/ExecApprovalRequestEvent.ts
index 66db8fd40a..542ed879b0 100644
--- a/codex-rs/app-server-protocol/schema/typescript/ExecApprovalRequestEvent.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/ExecApprovalRequestEvent.ts
@@ -2,6 +2,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ExecPolicyAmendment } from "./ExecPolicyAmendment";
+import type { NetworkApprovalContext } from "./NetworkApprovalContext";
import type { ParsedCommand } from "./ParsedCommand";
export type ExecApprovalRequestEvent = {
@@ -26,6 +27,10 @@ cwd: string,
* Optional human-readable reason for the approval (e.g. retry without sandbox).
*/
reason: string | null,
+/**
+ * Optional network context for a blocked request that can be approved.
+ */
+network_approval_context?: NetworkApprovalContext,
/**
* Proposed execpolicy amendment that can be applied to allow future runs.
*/
diff --git a/codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchSessionCompletedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchSessionCompletedNotification.ts
new file mode 100644
index 0000000000..f4dc7fac11
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchSessionCompletedNotification.ts
@@ -0,0 +1,5 @@
+// GENERATED CODE! DO NOT MODIFY BY HAND!
+
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
+export type FuzzyFileSearchSessionCompletedNotification = { sessionId: string, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/ModelRerouteEvent.ts b/codex-rs/app-server-protocol/schema/typescript/ModelRerouteEvent.ts
new file mode 100644
index 0000000000..23a4e1efb6
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/ModelRerouteEvent.ts
@@ -0,0 +1,6 @@
+// GENERATED CODE! DO NOT MODIFY BY HAND!
+
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+import type { ModelRerouteReason } from "./ModelRerouteReason";
+
+export type ModelRerouteEvent = { from_model: string, to_model: string, reason: ModelRerouteReason, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/ModelRerouteReason.ts b/codex-rs/app-server-protocol/schema/typescript/ModelRerouteReason.ts
new file mode 100644
index 0000000000..f5e1abf1e3
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/ModelRerouteReason.ts
@@ -0,0 +1,5 @@
+// GENERATED CODE! DO NOT MODIFY BY HAND!
+
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
+export type ModelRerouteReason = "high_risk_cyber_activity";
diff --git a/codex-rs/app-server-protocol/schema/typescript/NetworkApprovalContext.ts b/codex-rs/app-server-protocol/schema/typescript/NetworkApprovalContext.ts
new file mode 100644
index 0000000000..b4b78e473c
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/NetworkApprovalContext.ts
@@ -0,0 +1,6 @@
+// GENERATED CODE! DO NOT MODIFY BY HAND!
+
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+import type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol";
+
+export type NetworkApprovalContext = { host: string, protocol: NetworkApprovalProtocol, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/NetworkApprovalProtocol.ts b/codex-rs/app-server-protocol/schema/typescript/NetworkApprovalProtocol.ts
new file mode 100644
index 0000000000..a33eab566f
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/NetworkApprovalProtocol.ts
@@ -0,0 +1,5 @@
+// GENERATED CODE! DO NOT MODIFY BY HAND!
+
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
+export type NetworkApprovalProtocol = "http" | "https" | "socks5_tcp" | "socks5_udp";
diff --git a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts
index 0e49c060c0..171da74257 100644
--- a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts
@@ -2,6 +2,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AuthStatusChangeNotification } from "./AuthStatusChangeNotification";
+import type { FuzzyFileSearchSessionCompletedNotification } from "./FuzzyFileSearchSessionCompletedNotification";
import type { FuzzyFileSearchSessionUpdatedNotification } from "./FuzzyFileSearchSessionUpdatedNotification";
import type { LoginChatGptCompleteNotification } from "./LoginChatGptCompleteNotification";
import type { SessionConfiguredNotification } from "./SessionConfiguredNotification";
@@ -20,6 +21,7 @@ import type { ItemCompletedNotification } from "./v2/ItemCompletedNotification";
import type { ItemStartedNotification } from "./v2/ItemStartedNotification";
import type { McpServerOauthLoginCompletedNotification } from "./v2/McpServerOauthLoginCompletedNotification";
import type { McpToolCallProgressNotification } from "./v2/McpToolCallProgressNotification";
+import type { ModelReroutedNotification } from "./v2/ModelReroutedNotification";
import type { PlanDeltaNotification } from "./v2/PlanDeltaNotification";
import type { RawResponseItemCompletedNotification } from "./v2/RawResponseItemCompletedNotification";
import type { ReasoningSummaryPartAddedNotification } from "./v2/ReasoningSummaryPartAddedNotification";
@@ -38,4 +40,4 @@ import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldW
/**
* Notification sent from the server to the client.
*/
-export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification } | { "method": "authStatusChange", "params": AuthStatusChangeNotification } | { "method": "loginChatGptComplete", "params": LoginChatGptCompleteNotification } | { "method": "sessionConfigured", "params": SessionConfiguredNotification };
+export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification } | { "method": "authStatusChange", "params": AuthStatusChangeNotification } | { "method": "loginChatGptComplete", "params": LoginChatGptCompleteNotification } | { "method": "sessionConfigured", "params": SessionConfiguredNotification };
diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts
index 2e7446a3db..2a626d3638 100644
--- a/codex-rs/app-server-protocol/schema/typescript/index.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/index.ts
@@ -78,6 +78,7 @@ export type { FunctionCallOutputPayload } from "./FunctionCallOutputPayload";
export type { FuzzyFileSearchParams } from "./FuzzyFileSearchParams";
export type { FuzzyFileSearchResponse } from "./FuzzyFileSearchResponse";
export type { FuzzyFileSearchResult } from "./FuzzyFileSearchResult";
+export type { FuzzyFileSearchSessionCompletedNotification } from "./FuzzyFileSearchSessionCompletedNotification";
export type { FuzzyFileSearchSessionUpdatedNotification } from "./FuzzyFileSearchSessionUpdatedNotification";
export type { GetAuthStatusParams } from "./GetAuthStatusParams";
export type { GetAuthStatusResponse } from "./GetAuthStatusResponse";
@@ -124,7 +125,11 @@ export type { McpToolCallBeginEvent } from "./McpToolCallBeginEvent";
export type { McpToolCallEndEvent } from "./McpToolCallEndEvent";
export type { MessagePhase } from "./MessagePhase";
export type { ModeKind } from "./ModeKind";
+export type { ModelRerouteEvent } from "./ModelRerouteEvent";
+export type { ModelRerouteReason } from "./ModelRerouteReason";
export type { NetworkAccess } from "./NetworkAccess";
+export type { NetworkApprovalContext } from "./NetworkApprovalContext";
+export type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol";
export type { NewConversationParams } from "./NewConversationParams";
export type { NewConversationResponse } from "./NewConversationResponse";
export type { ParsedCommand } from "./ParsedCommand";
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppBranding.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppBranding.ts
new file mode 100644
index 0000000000..873398db67
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppBranding.ts
@@ -0,0 +1,8 @@
+// GENERATED CODE! DO NOT MODIFY BY HAND!
+
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
+/**
+ * EXPERIMENTAL - app metadata returned by app-list APIs.
+ */
+export type AppBranding = { category: string | null, developer: string | null, website: string | null, privacyPolicy: string | null, termsOfService: string | null, isDiscoverableApp: boolean, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppInfo.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppInfo.ts
index 2f65c18327..0c9a13b124 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/AppInfo.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppInfo.ts
@@ -1,11 +1,13 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+import type { AppBranding } from "./AppBranding";
+import type { AppMetadata } from "./AppMetadata";
/**
* EXPERIMENTAL - app metadata returned by app-list APIs.
*/
-export type AppInfo = { id: string, name: string, description: string | null, logoUrl: string | null, logoUrlDark: string | null, distributionChannel: string | null, installUrl: string | null, isAccessible: boolean,
+export type AppInfo = { id: string, name: string, description: string | null, logoUrl: string | null, logoUrlDark: string | null, distributionChannel: string | null, branding: AppBranding | null, appMetadata: AppMetadata | null, labels: { [key in string]?: string } | null, installUrl: string | null, isAccessible: boolean,
/**
* Whether this app is enabled in config.toml.
* Example:
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppMetadata.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppMetadata.ts
new file mode 100644
index 0000000000..f1a5001eb1
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppMetadata.ts
@@ -0,0 +1,7 @@
+// GENERATED CODE! DO NOT MODIFY BY HAND!
+
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+import type { AppReview } from "./AppReview";
+import type { AppScreenshot } from "./AppScreenshot";
+
+export type AppMetadata = { review: AppReview | null, categories: Array | null, subCategories: Array | null, seoDescription: string | null, screenshots: Array | null, developer: string | null, version: string | null, versionId: string | null, versionNotes: string | null, firstPartyType: string | null, firstPartyRequiresInstall: boolean | null, showInComposerWhenUnlinked: boolean | null, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppReview.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppReview.ts
new file mode 100644
index 0000000000..10fd95f09d
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppReview.ts
@@ -0,0 +1,5 @@
+// GENERATED CODE! DO NOT MODIFY BY HAND!
+
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
+export type AppReview = { status: string, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppScreenshot.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppScreenshot.ts
new file mode 100644
index 0000000000..0d264246f3
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppScreenshot.ts
@@ -0,0 +1,5 @@
+// GENERATED CODE! DO NOT MODIFY BY HAND!
+
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
+export type AppScreenshot = { url: string | null, fileId: string | null, userPrompt: string, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/HazelnutScope.ts b/codex-rs/app-server-protocol/schema/typescript/v2/HazelnutScope.ts
new file mode 100644
index 0000000000..e623f1860b
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/HazelnutScope.ts
@@ -0,0 +1,5 @@
+// GENERATED CODE! DO NOT MODIFY BY HAND!
+
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
+export type HazelnutScope = "example" | "workspace-shared" | "all-shared" | "personal";
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/Model.ts b/codex-rs/app-server-protocol/schema/typescript/v2/Model.ts
index 7528a8fad3..de7b240173 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/Model.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/Model.ts
@@ -5,4 +5,4 @@ import type { InputModality } from "../InputModality";
import type { ReasoningEffort } from "../ReasoningEffort";
import type { ReasoningEffortOption } from "./ReasoningEffortOption";
-export type Model = { id: string, model: string, upgrade: string | null, displayName: string, description: string, supportedReasoningEfforts: Array, defaultReasoningEffort: ReasoningEffort, inputModalities: Array, supportsPersonality: boolean, isDefault: boolean, };
+export type Model = { id: string, model: string, upgrade: string | null, displayName: string, description: string, hidden: boolean, supportedReasoningEfforts: Array, defaultReasoningEffort: ReasoningEffort, inputModalities: Array, supportsPersonality: boolean, isDefault: boolean, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ModelListParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ModelListParams.ts
index b0bc5326c1..a1e7b8fa74 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/ModelListParams.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/ModelListParams.ts
@@ -10,4 +10,8 @@ cursor?: string | null,
/**
* Optional page size; defaults to a reasonable server-side value.
*/
-limit?: number | null, };
+limit?: number | null,
+/**
+ * When true, include models that are hidden from the default picker list.
+ */
+includeHidden?: boolean | null, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ModelRerouteReason.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ModelRerouteReason.ts
new file mode 100644
index 0000000000..e780e7f95d
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/ModelRerouteReason.ts
@@ -0,0 +1,5 @@
+// GENERATED CODE! DO NOT MODIFY BY HAND!
+
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
+export type ModelRerouteReason = "highRiskCyberActivity";
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ModelReroutedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ModelReroutedNotification.ts
new file mode 100644
index 0000000000..9b6b2e524a
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/ModelReroutedNotification.ts
@@ -0,0 +1,6 @@
+// GENERATED CODE! DO NOT MODIFY BY HAND!
+
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+import type { ModelRerouteReason } from "./ModelRerouteReason";
+
+export type ModelReroutedNotification = { threadId: string, turnId: string, fromModel: string, toModel: string, reason: ModelRerouteReason, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ProductSurface.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ProductSurface.ts
new file mode 100644
index 0000000000..9998c727a8
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/ProductSurface.ts
@@ -0,0 +1,5 @@
+// GENERATED CODE! DO NOT MODIFY BY HAND!
+
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
+export type ProductSurface = "chatgpt" | "codex" | "api" | "atlas";
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteReadParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteReadParams.ts
index 9f91787686..1257f0d791 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteReadParams.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteReadParams.ts
@@ -1,5 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+import type { HazelnutScope } from "./HazelnutScope";
+import type { ProductSurface } from "./ProductSurface";
-export type SkillsRemoteReadParams = Record;
+export type SkillsRemoteReadParams = { hazelnutScope: HazelnutScope, productSurface: ProductSurface, enabled: boolean, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteParams.ts
index 857b609ef1..ea42595bfd 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteParams.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteParams.ts
@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-export type SkillsRemoteWriteParams = { hazelnutId: string, isPreload: boolean, };
+export type SkillsRemoteWriteParams = { hazelnutId: string, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteResponse.ts
index cf1665ab97..228b723b19 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteResponse.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsRemoteWriteResponse.ts
@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
-export type SkillsRemoteWriteResponse = { id: string, name: string, path: string, };
+export type SkillsRemoteWriteResponse = { id: string, path: string, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts
index 54322bf839..1ac4e8f3c2 100644
--- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts
@@ -6,9 +6,13 @@ export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUp
export type { AccountUpdatedNotification } from "./AccountUpdatedNotification";
export type { AgentMessageDeltaNotification } from "./AgentMessageDeltaNotification";
export type { AnalyticsConfig } from "./AnalyticsConfig";
+export type { AppBranding } from "./AppBranding";
export type { AppDisabledReason } from "./AppDisabledReason";
export type { AppInfo } from "./AppInfo";
export type { AppListUpdatedNotification } from "./AppListUpdatedNotification";
+export type { AppMetadata } from "./AppMetadata";
+export type { AppReview } from "./AppReview";
+export type { AppScreenshot } from "./AppScreenshot";
export type { AppToolApproval } from "./AppToolApproval";
export type { AppToolDefaults } from "./AppToolDefaults";
export type { AppToolsConfig } from "./AppToolsConfig";
@@ -74,6 +78,7 @@ export type { GetAccountParams } from "./GetAccountParams";
export type { GetAccountRateLimitsResponse } from "./GetAccountRateLimitsResponse";
export type { GetAccountResponse } from "./GetAccountResponse";
export type { GitInfo } from "./GitInfo";
+export type { HazelnutScope } from "./HazelnutScope";
export type { ItemCompletedNotification } from "./ItemCompletedNotification";
export type { ItemStartedNotification } from "./ItemStartedNotification";
export type { ListMcpServerStatusParams } from "./ListMcpServerStatusParams";
@@ -95,12 +100,15 @@ export type { MergeStrategy } from "./MergeStrategy";
export type { Model } from "./Model";
export type { ModelListParams } from "./ModelListParams";
export type { ModelListResponse } from "./ModelListResponse";
+export type { ModelRerouteReason } from "./ModelRerouteReason";
+export type { ModelReroutedNotification } from "./ModelReroutedNotification";
export type { NetworkAccess } from "./NetworkAccess";
export type { NetworkRequirements } from "./NetworkRequirements";
export type { OverriddenMetadata } from "./OverriddenMetadata";
export type { PatchApplyStatus } from "./PatchApplyStatus";
export type { PatchChangeKind } from "./PatchChangeKind";
export type { PlanDeltaNotification } from "./PlanDeltaNotification";
+export type { ProductSurface } from "./ProductSurface";
export type { ProfileV2 } from "./ProfileV2";
export type { RateLimitSnapshot } from "./RateLimitSnapshot";
export type { RateLimitWindow } from "./RateLimitWindow";
diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs
index bc91341166..88a17390de 100644
--- a/codex-rs/app-server-protocol/src/protocol/common.rs
+++ b/codex-rs/app-server-protocol/src/protocol/common.rs
@@ -239,11 +239,11 @@ client_request_definitions! {
params: v2::SkillsListParams,
response: v2::SkillsListResponse,
},
- SkillsRemoteRead => "skills/remote/read" {
+ SkillsRemoteList => "skills/remote/list" {
params: v2::SkillsRemoteReadParams,
response: v2::SkillsRemoteReadResponse,
},
- SkillsRemoteWrite => "skills/remote/write" {
+ SkillsRemoteExport => "skills/remote/export" {
params: v2::SkillsRemoteWriteParams,
response: v2::SkillsRemoteWriteResponse,
},
@@ -758,6 +758,13 @@ pub struct FuzzyFileSearchSessionUpdatedNotification {
pub files: Vec,
}
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(rename_all = "camelCase")]
+pub struct FuzzyFileSearchSessionCompletedNotification {
+ pub session_id: String,
+}
+
server_notification_definitions! {
/// NEW NOTIFICATIONS
Error => "error" (v2::ErrorNotification),
@@ -788,9 +795,11 @@ server_notification_definitions! {
ReasoningTextDelta => "item/reasoning/textDelta" (v2::ReasoningTextDeltaNotification),
/// Deprecated: Use `ContextCompaction` item type instead.
ContextCompacted => "thread/compacted" (v2::ContextCompactedNotification),
+ ModelRerouted => "model/rerouted" (v2::ModelReroutedNotification),
DeprecationNotice => "deprecationNotice" (v2::DeprecationNoticeNotification),
ConfigWarning => "configWarning" (v2::ConfigWarningNotification),
FuzzyFileSearchSessionUpdated => "fuzzyFileSearch/sessionUpdated" (FuzzyFileSearchSessionUpdatedNotification),
+ FuzzyFileSearchSessionCompleted => "fuzzyFileSearch/sessionCompleted" (FuzzyFileSearchSessionCompletedNotification),
/// Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox.
WindowsWorldWritableWarning => "windows/worldWritableWarning" (v2::WindowsWorldWritableWarningNotification),
@@ -1227,7 +1236,8 @@ mod tests {
"id": 6,
"params": {
"limit": null,
- "cursor": null
+ "cursor": null,
+ "includeHidden": null
}
}),
serde_json::to_value(&request)?,
diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs
index 4152f5da20..6c1947c6dd 100644
--- a/codex-rs/app-server-protocol/src/protocol/v2.rs
+++ b/codex-rs/app-server-protocol/src/protocol/v2.rs
@@ -30,6 +30,7 @@ use codex_protocol::protocol::AskForApproval as CoreAskForApproval;
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot;
use codex_protocol::protocol::ExecCommandStatus as CoreExecCommandStatus;
+use codex_protocol::protocol::ModelRerouteReason as CoreModelRerouteReason;
use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess;
use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus;
use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot;
@@ -229,6 +230,12 @@ v2_enum_from_core!(
}
);
+v2_enum_from_core!(
+ pub enum ModelRerouteReason from CoreModelRerouteReason {
+ HighRiskCyberActivity
+ }
+);
+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type")]
@@ -1164,6 +1171,9 @@ pub struct ModelListParams {
/// Optional page size; defaults to a reasonable server-side value.
#[ts(optional = nullable)]
pub limit: Option,
+ /// When true, include models that are hidden from the default picker list.
+ #[ts(optional = nullable)]
+ pub include_hidden: Option,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -1175,6 +1185,7 @@ pub struct Model {
pub upgrade: Option,
pub display_name: String,
pub description: String,
+ pub hidden: bool,
pub supported_reasoning_efforts: Vec,
pub default_reasoning_effort: ReasoningEffort,
#[serde(default = "default_input_modalities")]
@@ -1330,6 +1341,55 @@ pub struct AppsListParams {
pub force_refetch: bool,
}
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export_to = "v2/")]
+/// EXPERIMENTAL - app metadata returned by app-list APIs.
+pub struct AppBranding {
+ pub category: Option,
+ pub developer: Option,
+ pub website: Option,
+ pub privacy_policy: Option,
+ pub terms_of_service: Option,
+ pub is_discoverable_app: bool,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export_to = "v2/")]
+pub struct AppReview {
+ pub status: String,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export_to = "v2/")]
+pub struct AppScreenshot {
+ pub url: Option,
+ #[serde(alias = "file_id")]
+ pub file_id: Option,
+ #[serde(alias = "user_prompt")]
+ pub user_prompt: String,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export_to = "v2/")]
+pub struct AppMetadata {
+ pub review: Option,
+ pub categories: Option>,
+ pub sub_categories: Option>,
+ pub seo_description: Option,
+ pub screenshots: Option>,
+ pub developer: Option,
+ pub version: Option,
+ pub version_id: Option,
+ pub version_notes: Option,
+ pub first_party_type: Option,
+ pub first_party_requires_install: Option,
+ pub show_in_composer_when_unlinked: Option,
+}
+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -1341,6 +1401,9 @@ pub struct AppInfo {
pub logo_url: Option,
pub logo_url_dark: Option,
pub distribution_channel: Option,
+ pub branding: Option,
+ pub app_metadata: Option,
+ pub labels: Option>,
pub install_url: Option,
#[serde(default)]
pub is_accessible: bool,
@@ -1880,7 +1943,38 @@ pub struct SkillsListResponse {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
-pub struct SkillsRemoteReadParams {}
+pub struct SkillsRemoteReadParams {
+ #[serde(default)]
+ pub hazelnut_scope: HazelnutScope,
+ #[serde(default)]
+ pub product_surface: ProductSurface,
+ #[serde(default)]
+ pub enabled: bool,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS, Default)]
+#[serde(rename_all = "kebab-case")]
+#[ts(rename_all = "kebab-case")]
+#[ts(export_to = "v2/")]
+pub enum HazelnutScope {
+ #[default]
+ Example,
+ WorkspaceShared,
+ AllShared,
+ Personal,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS, Default)]
+#[serde(rename_all = "lowercase")]
+#[ts(rename_all = "lowercase")]
+#[ts(export_to = "v2/")]
+pub enum ProductSurface {
+ Chatgpt,
+ #[default]
+ Codex,
+ Api,
+ Atlas,
+}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
@@ -1903,7 +1997,6 @@ pub struct SkillsRemoteReadResponse {
#[ts(export_to = "v2/")]
pub struct SkillsRemoteWriteParams {
pub hazelnut_id: String,
- pub is_preload: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -1911,7 +2004,6 @@ pub struct SkillsRemoteWriteParams {
#[ts(export_to = "v2/")]
pub struct SkillsRemoteWriteResponse {
pub id: String,
- pub name: String,
pub path: PathBuf,
}
@@ -3355,6 +3447,17 @@ pub struct AccountLoginCompletedNotification {
pub error: Option,
}
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export_to = "v2/")]
+pub struct ModelReroutedNotification {
+ pub thread_id: String,
+ pub turn_id: String,
+ pub from_model: String,
+ pub to_model: String,
+ pub reason: ModelRerouteReason,
+}
+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
diff --git a/codex-rs/app-server-test-client/README.md b/codex-rs/app-server-test-client/README.md
index 5ef6e2344c..ab11f12353 100644
--- a/codex-rs/app-server-test-client/README.md
+++ b/codex-rs/app-server-test-client/README.md
@@ -17,3 +17,33 @@ cargo run -p codex-app-server-test-client -- \
# 3) Call app-server (defaults to ws://127.0.0.1:4222)
cargo run -p codex-app-server-test-client -- model-list
```
+
+## Testing Thread Rejoin Behavior
+
+Build and start an app server using commands above. The app-server log is written to `/tmp/codex-app-server-test-client/app-server.log`
+
+### 1) Get a thread id
+
+Create at least one thread, then list threads:
+
+```bash
+cargo run -p codex-app-server-test-client -- send-message-v2 "seed thread for rejoin test"
+cargo run -p codex-app-server-test-client -- thread-list --limit 5
+```
+
+Copy a thread id from the `thread-list` output.
+
+### 2) Rejoin while a turn is in progress (two terminals)
+
+Terminal A:
+
+```bash
+cargo run --bin codex-app-server-test-client -- \
+ resume-message-v2 "respond with thorough docs on the rust core"
+```
+
+Terminal B (while Terminal A is still streaming):
+
+```bash
+cargo run --bin codex-app-server-test-client -- thread-resume
+```
diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md
index c798073056..ef34e3f6a7 100644
--- a/codex-rs/app-server/README.md
+++ b/codex-rs/app-server/README.md
@@ -131,12 +131,12 @@ Example with notification opt-out:
- `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`.
- `review/start` — kick off Codex’s automated reviewer for a thread; responds like `turn/start` and emits `item/started`/`item/completed` notifications with `enteredReviewMode` and `exitedReviewMode` items, plus a final assistant `agentMessage` containing the review.
- `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation).
-- `model/list` — list available models (with reasoning effort options and optional `upgrade` model ids).
+- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options and optional `upgrade` model ids.
- `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. For non-beta flags, `displayName`/`description`/`announcement` are `null`.
- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination).
- `skills/list` — list skills for one or more `cwd` values (optional `forceReload`).
-- `skills/remote/read` — list public remote skills (**under development; do not call from production clients yet**).
-- `skills/remote/write` — download a public remote skill by `hazelnutId`; `isPreload=true` writes to `.codex/vendor_imports/skills` under `codex_home` (**under development; do not call from production clients yet**).
+- `skills/remote/list` — list public remote skills (**under development; do not call from production clients yet**).
+- `skills/remote/export` — download a remote skill by `hazelnutId` into `skills` under `codex_home` (**under development; do not call from production clients yet**).
- `app/list` — list available apps.
- `skills/config/write` — write user-level skill config by path.
- `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `mcpServer/oauthLogin/completed` once the browser flow finishes.
@@ -535,6 +535,13 @@ Examples:
- Opt out of legacy session setup event: `codex/event/session_configured`
- Opt out of streamed agent text deltas: `item/agentMessage/delta`
+### Fuzzy file search events (experimental)
+
+The fuzzy file search session API emits per-query notifications:
+
+- `fuzzyFileSearch/sessionUpdated` — `{ sessionId, query, files }` with the current matching files for the active query.
+- `fuzzyFileSearch/sessionCompleted` — `{ sessionId, query }` once indexing/matching for that query has completed.
+
### Turn events
The app-server streams JSON-RPC notifications while a turn is running. Each turn starts with `turn/started` (initial `turn`) and ends with `turn/completed` (final `turn` status). Token usage events stream separately via `thread/tokenUsage/updated`. Clients subscribe to the events they care about, rendering each item incrementally as updates arrive. The per-item lifecycle is always: `item/started` → zero or more item-specific deltas → `item/completed`.
@@ -543,6 +550,7 @@ The app-server streams JSON-RPC notifications while a turn is running. Each turn
- `turn/completed` — `{ turn }` where `turn.status` is `completed`, `interrupted`, or `failed`; failures carry `{ error: { message, codexErrorInfo?, additionalDetails? } }`.
- `turn/diff/updated` — `{ threadId, turnId, diff }` represents the up-to-date snapshot of the turn-level unified diff, emitted after every FileChange item. `diff` is the latest aggregated unified diff across every file change in the turn. UIs can render this to show the full "what changed" view without stitching individual `fileChange` items.
- `turn/plan/updated` — `{ turnId, explanation?, plan }` whenever the agent shares or changes its plan; each `plan` entry is `{ step, status }` with `status` in `pending`, `inProgress`, or `completed`.
+- `model/rerouted` — `{ threadId, turnId, fromModel, toModel, reason }` when the backend reroutes a request to a different model (for example, due to high-risk cyber safety checks).
Today both notifications carry an empty `items` array even when item events were streamed; rely on `item/*` notifications for the canonical item list until this is fixed.
@@ -764,7 +772,7 @@ To enable or disable a skill by path:
## Apps
-Use `app/list` to fetch available apps (connectors). Each entry includes metadata like the app `id`, display `name`, `installUrl`, whether it is currently accessible, and whether it is enabled in config.
+Use `app/list` to fetch available apps (connectors). Each entry includes metadata like the app `id`, display `name`, `installUrl`, `branding`, `appMetadata`, `labels`, whether it is currently accessible, and whether it is enabled in config.
```json
{ "method": "app/list", "id": 50, "params": {
@@ -782,6 +790,9 @@ Use `app/list` to fetch available apps (connectors). Each entry includes metadat
"logoUrl": "https://example.com/demo-app.png",
"logoUrlDark": null,
"distributionChannel": null,
+ "branding": null,
+ "appMetadata": null,
+ "labels": null,
"installUrl": "https://chatgpt.com/apps/demo-app/demo-app",
"isAccessible": true,
"isEnabled": true
@@ -809,6 +820,9 @@ The server also emits `app/list/updated` notifications whenever either source (a
"logoUrl": "https://example.com/demo-app.png",
"logoUrlDark": null,
"distributionChannel": null,
+ "branding": null,
+ "appMetadata": null,
+ "labels": null,
"installUrl": "https://chatgpt.com/apps/demo-app/demo-app",
"isAccessible": true,
"isEnabled": true
diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs
index c1f36e1ef3..3acdefc731 100644
--- a/codex-rs/app-server/src/bespoke_event_handling.rs
+++ b/codex-rs/app-server/src/bespoke_event_handling.rs
@@ -41,6 +41,7 @@ use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::McpToolCallError;
use codex_app_server_protocol::McpToolCallResult;
use codex_app_server_protocol::McpToolCallStatus;
+use codex_app_server_protocol::ModelReroutedNotification;
use codex_app_server_protocol::PatchApplyStatus;
use codex_app_server_protocol::PatchChangeKind as V2PatchChangeKind;
use codex_app_server_protocol::PlanDeltaNotification;
@@ -122,6 +123,21 @@ pub(crate) async fn apply_bespoke_event_handling(
EventMsg::TurnComplete(_ev) => {
handle_turn_complete(conversation_id, event_turn_id, &outgoing, &thread_state).await;
}
+ EventMsg::Warning(_warning_event) => {}
+ EventMsg::ModelReroute(event) => {
+ if let ApiVersion::V2 = api_version {
+ let notification = ModelReroutedNotification {
+ thread_id: conversation_id.to_string(),
+ turn_id: event_turn_id.clone(),
+ from_model: event.from_model,
+ to_model: event.to_model,
+ reason: event.reason.into(),
+ };
+ outgoing
+ .send_server_notification(ServerNotification::ModelRerouted(notification))
+ .await;
+ }
+ }
EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
call_id,
turn_id,
@@ -206,6 +222,7 @@ pub(crate) async fn apply_bespoke_event_handling(
reason,
proposed_execpolicy_amendment,
parsed_cmd,
+ ..
}) => match api_version {
ApiVersion::V1 => {
let params = ExecCommandApprovalParams {
diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs
index 198892d0a2..45d8af7746 100644
--- a/codex-rs/app-server/src/codex_message_processor.rs
+++ b/codex-rs/app-server/src/codex_message_processor.rs
@@ -66,6 +66,7 @@ use codex_app_server_protocol::GetUserAgentResponse;
use codex_app_server_protocol::GetUserSavedConfigResponse;
use codex_app_server_protocol::GitDiffToRemoteResponse;
use codex_app_server_protocol::GitInfo as ApiGitInfo;
+use codex_app_server_protocol::HazelnutScope as ApiHazelnutScope;
use codex_app_server_protocol::InputItem as WireInputItem;
use codex_app_server_protocol::InterruptConversationParams;
use codex_app_server_protocol::JSONRPCErrorError;
@@ -92,6 +93,7 @@ use codex_app_server_protocol::ModelListParams;
use codex_app_server_protocol::ModelListResponse;
use codex_app_server_protocol::NewConversationParams;
use codex_app_server_protocol::NewConversationResponse;
+use codex_app_server_protocol::ProductSurface as ApiProductSurface;
use codex_app_server_protocol::RemoveConversationListenerParams;
use codex_app_server_protocol::RemoveConversationSubscriptionResponse;
use codex_app_server_protocol::ResumeConversationParams;
@@ -207,7 +209,7 @@ use codex_core::read_head_for_summary;
use codex_core::read_session_meta_line;
use codex_core::rollout_date_parts;
use codex_core::sandboxing::SandboxPermissions;
-use codex_core::skills::remote::download_remote_skill;
+use codex_core::skills::remote::export_remote_skill;
use codex_core::skills::remote::list_remote_skills;
use codex_core::state_db::StateDbHandle;
use codex_core::state_db::get_state_db;
@@ -229,6 +231,8 @@ use codex_protocol::protocol::GitInfo as CoreGitInfo;
use codex_protocol::protocol::McpAuthStatus as CoreMcpAuthStatus;
use codex_protocol::protocol::McpServerRefreshConfig;
use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot;
+use codex_protocol::protocol::RemoteSkillHazelnutScope;
+use codex_protocol::protocol::RemoteSkillProductSurface;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::SessionMetaLine;
use codex_protocol::protocol::USER_MESSAGE_BEGIN;
@@ -291,6 +295,24 @@ enum AppListLoadResult {
Directory(Result, String>),
}
+fn convert_remote_scope(scope: ApiHazelnutScope) -> RemoteSkillHazelnutScope {
+ match scope {
+ ApiHazelnutScope::WorkspaceShared => RemoteSkillHazelnutScope::WorkspaceShared,
+ ApiHazelnutScope::AllShared => RemoteSkillHazelnutScope::AllShared,
+ ApiHazelnutScope::Personal => RemoteSkillHazelnutScope::Personal,
+ ApiHazelnutScope::Example => RemoteSkillHazelnutScope::Example,
+ }
+}
+
+fn convert_remote_product_surface(product_surface: ApiProductSurface) -> RemoteSkillProductSurface {
+ match product_surface {
+ ApiProductSurface::Chatgpt => RemoteSkillProductSurface::Chatgpt,
+ ApiProductSurface::Codex => RemoteSkillProductSurface::Codex,
+ ApiProductSurface::Api => RemoteSkillProductSurface::Api,
+ ApiProductSurface::Atlas => RemoteSkillProductSurface::Atlas,
+ }
+}
+
impl Drop for ActiveLogin {
fn drop(&mut self) {
self.shutdown_handle.shutdown();
@@ -549,12 +571,12 @@ impl CodexMessageProcessor {
self.skills_list(to_connection_request_id(request_id), params)
.await;
}
- ClientRequest::SkillsRemoteRead { request_id, params } => {
- self.skills_remote_read(to_connection_request_id(request_id), params)
+ ClientRequest::SkillsRemoteList { request_id, params } => {
+ self.skills_remote_list(to_connection_request_id(request_id), params)
.await;
}
- ClientRequest::SkillsRemoteWrite { request_id, params } => {
- self.skills_remote_write(to_connection_request_id(request_id), params)
+ ClientRequest::SkillsRemoteExport { request_id, params } => {
+ self.skills_remote_export(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::AppsList { request_id, params } => {
@@ -599,11 +621,10 @@ impl CodexMessageProcessor {
ClientRequest::ModelList { request_id, params } => {
let outgoing = self.outgoing.clone();
let thread_manager = self.thread_manager.clone();
- let config = self.config.clone();
let request_id = to_connection_request_id(request_id);
tokio::spawn(async move {
- Self::list_models(outgoing, thread_manager, config, request_id, params).await;
+ Self::list_models(outgoing, thread_manager, request_id, params).await;
});
}
ClientRequest::ExperimentalFeatureList { request_id, params } => {
@@ -1688,8 +1709,18 @@ impl CodexMessageProcessor {
let timeout_ms = params
.timeout_ms
.and_then(|timeout_ms| u64::try_from(timeout_ms).ok());
+ let managed_network_requirements_enabled =
+ self.config.managed_network_requirements_enabled();
let started_network_proxy = match self.config.permissions.network.as_ref() {
- Some(spec) => match spec.start_proxy().await {
+ Some(spec) => match spec
+ .start_proxy(
+ self.config.permissions.sandbox_policy.get(),
+ None,
+ None,
+ managed_network_requirements_enabled,
+ )
+ .await
+ {
Ok(started) => Some(started),
Err(err) => {
let error = JSONRPCErrorError {
@@ -1712,6 +1743,7 @@ impl CodexMessageProcessor {
network: started_network_proxy
.as_ref()
.map(codex_core::config::StartedNetworkProxy::proxy),
+ network_attempt_id: None,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level,
justification: None,
@@ -2689,6 +2721,13 @@ impl CodexMessageProcessor {
}
async fn thread_resume(&mut self, request_id: ConnectionRequestId, params: ThreadResumeParams) {
+ if self
+ .resume_running_thread(request_id.clone(), ¶ms)
+ .await
+ {
+ return;
+ }
+
let ThreadResumeParams {
thread_id,
history,
@@ -2706,77 +2745,21 @@ impl CodexMessageProcessor {
} = params;
let thread_history = if let Some(history) = history {
- if history.is_empty() {
- self.send_invalid_request_error(
- request_id,
- "history must not be empty".to_string(),
- )
- .await;
+ let Some(thread_history) = self
+ .resume_thread_from_history(request_id.clone(), history.as_slice())
+ .await
+ else {
return;
- }
- InitialHistory::Forked(history.into_iter().map(RolloutItem::ResponseItem).collect())
- } else if let Some(path) = path {
- match RolloutRecorder::get_rollout_history(&path).await {
- Ok(initial_history) => initial_history,
- Err(err) => {
- self.send_invalid_request_error(
- request_id,
- format!("failed to load rollout `{}`: {err}", path.display()),
- )
- .await;
- return;
- }
- }
+ };
+ thread_history
} else {
- let existing_thread_id = match ThreadId::from_string(&thread_id) {
- Ok(id) => id,
- Err(err) => {
- let error = JSONRPCErrorError {
- code: INVALID_REQUEST_ERROR_CODE,
- message: format!("invalid thread id: {err}"),
- data: None,
- };
- self.outgoing.send_error(request_id, error).await;
- return;
- }
+ let Some(thread_history) = self
+ .resume_thread_from_rollout(request_id.clone(), &thread_id, path.as_ref())
+ .await
+ else {
+ return;
};
-
- let path = match find_thread_path_by_id_str(
- &self.config.codex_home,
- &existing_thread_id.to_string(),
- )
- .await
- {
- Ok(Some(p)) => p,
- Ok(None) => {
- self.send_invalid_request_error(
- request_id,
- format!("no rollout found for thread id {existing_thread_id}"),
- )
- .await;
- return;
- }
- Err(err) => {
- self.send_invalid_request_error(
- request_id,
- format!("failed to locate thread id {existing_thread_id}: {err}"),
- )
- .await;
- return;
- }
- };
-
- match RolloutRecorder::get_rollout_history(&path).await {
- Ok(initial_history) => initial_history,
- Err(err) => {
- self.send_invalid_request_error(
- request_id,
- format!("failed to load rollout `{}`: {err}", path.display()),
- )
- .await;
- return;
- }
- }
+ thread_history
};
let history_cwd = thread_history.session_cwd();
@@ -2857,41 +2840,17 @@ impl CodexMessageProcessor {
);
}
- let mut thread = match read_summary_from_rollout(
- rollout_path.as_path(),
- fallback_model_provider.as_str(),
- )
- .await
- {
- Ok(summary) => summary_to_thread(summary),
- Err(err) => {
- self.send_internal_error(
- request_id,
- format!(
- "failed to load rollout `{}` for thread {thread_id}: {err}",
- rollout_path.display()
- ),
- )
- .await;
- return;
- }
+ let Some(thread) = self
+ .load_thread_from_rollout_or_send_internal(
+ request_id.clone(),
+ thread_id,
+ rollout_path.as_path(),
+ fallback_model_provider.as_str(),
+ )
+ .await
+ else {
+ return;
};
- match read_rollout_items_from_rollout(rollout_path.as_path()).await {
- Ok(items) => {
- thread.turns = build_turns_from_rollout_items(&items);
- }
- Err(err) => {
- self.send_internal_error(
- request_id,
- format!(
- "failed to load rollout `{}` for thread {thread_id}: {err}",
- rollout_path.display()
- ),
- )
- .await;
- return;
- }
- }
let response = ThreadResumeResponse {
thread,
@@ -2916,6 +2875,278 @@ impl CodexMessageProcessor {
}
}
+ async fn resume_running_thread(
+ &mut self,
+ request_id: ConnectionRequestId,
+ params: &ThreadResumeParams,
+ ) -> bool {
+ if let Ok(existing_thread_id) = ThreadId::from_string(¶ms.thread_id)
+ && let Ok(existing_thread) = self.thread_manager.get_thread(existing_thread_id).await
+ {
+ if params.history.is_some() {
+ self.send_invalid_request_error(
+ request_id,
+ format!(
+ "cannot resume thread {existing_thread_id} with history while it is already running"
+ ),
+ )
+ .await;
+ return true;
+ }
+
+ let rollout_path = if let Some(path) = existing_thread.rollout_path() {
+ if path.exists() {
+ path
+ } else {
+ match find_thread_path_by_id_str(
+ &self.config.codex_home,
+ &existing_thread_id.to_string(),
+ )
+ .await
+ {
+ Ok(Some(path)) => path,
+ Ok(None) => {
+ self.send_invalid_request_error(
+ request_id,
+ format!("no rollout found for thread id {existing_thread_id}"),
+ )
+ .await;
+ return true;
+ }
+ Err(err) => {
+ self.send_invalid_request_error(
+ request_id,
+ format!("failed to locate thread id {existing_thread_id}: {err}"),
+ )
+ .await;
+ return true;
+ }
+ }
+ }
+ } else {
+ match find_thread_path_by_id_str(
+ &self.config.codex_home,
+ &existing_thread_id.to_string(),
+ )
+ .await
+ {
+ Ok(Some(path)) => path,
+ Ok(None) => {
+ self.send_invalid_request_error(
+ request_id,
+ format!("no rollout found for thread id {existing_thread_id}"),
+ )
+ .await;
+ return true;
+ }
+ Err(err) => {
+ self.send_invalid_request_error(
+ request_id,
+ format!("failed to locate thread id {existing_thread_id}: {err}"),
+ )
+ .await;
+ return true;
+ }
+ }
+ };
+
+ if let Some(requested_path) = params.path.as_ref()
+ && requested_path != &rollout_path
+ {
+ self.send_invalid_request_error(
+ request_id,
+ format!(
+ "cannot resume running thread {existing_thread_id} with mismatched path: requested `{}`, active `{}`",
+ requested_path.display(),
+ rollout_path.display()
+ ),
+ )
+ .await;
+ return true;
+ }
+
+ if let Err(err) = self
+ .ensure_conversation_listener(
+ existing_thread_id,
+ request_id.connection_id,
+ false,
+ ApiVersion::V2,
+ )
+ .await
+ {
+ tracing::warn!(
+ "failed to attach listener for thread {}: {}",
+ existing_thread_id,
+ err.message
+ );
+ }
+
+ let config_snapshot = existing_thread.config_snapshot().await;
+ let mismatch_details = collect_resume_override_mismatches(params, &config_snapshot);
+ if !mismatch_details.is_empty() {
+ tracing::warn!(
+ "thread/resume overrides ignored for running thread {}: {}",
+ existing_thread_id,
+ mismatch_details.join("; ")
+ );
+ }
+
+ let Some(thread) = self
+ .load_thread_from_rollout_or_send_internal(
+ request_id.clone(),
+ existing_thread_id,
+ rollout_path.as_path(),
+ config_snapshot.model_provider_id.as_str(),
+ )
+ .await
+ else {
+ return true;
+ };
+
+ let ThreadConfigSnapshot {
+ model,
+ model_provider_id,
+ approval_policy,
+ sandbox_policy,
+ cwd,
+ reasoning_effort,
+ ..
+ } = config_snapshot;
+ let response = ThreadResumeResponse {
+ thread,
+ model,
+ model_provider: model_provider_id,
+ cwd,
+ approval_policy: approval_policy.into(),
+ sandbox: sandbox_policy.into(),
+ reasoning_effort,
+ };
+ self.outgoing.send_response(request_id, response).await;
+ return true;
+ }
+ false
+ }
+
+ async fn resume_thread_from_history(
+ &self,
+ request_id: ConnectionRequestId,
+ history: &[ResponseItem],
+ ) -> Option {
+ if history.is_empty() {
+ self.send_invalid_request_error(request_id, "history must not be empty".to_string())
+ .await;
+ return None;
+ }
+ Some(InitialHistory::Forked(
+ history
+ .iter()
+ .cloned()
+ .map(RolloutItem::ResponseItem)
+ .collect(),
+ ))
+ }
+
+ async fn resume_thread_from_rollout(
+ &self,
+ request_id: ConnectionRequestId,
+ thread_id: &str,
+ path: Option<&PathBuf>,
+ ) -> Option {
+ let rollout_path = if let Some(path) = path {
+ path.clone()
+ } else {
+ let existing_thread_id = match ThreadId::from_string(thread_id) {
+ Ok(id) => id,
+ Err(err) => {
+ let error = JSONRPCErrorError {
+ code: INVALID_REQUEST_ERROR_CODE,
+ message: format!("invalid thread id: {err}"),
+ data: None,
+ };
+ self.outgoing.send_error(request_id, error).await;
+ return None;
+ }
+ };
+
+ match find_thread_path_by_id_str(
+ &self.config.codex_home,
+ &existing_thread_id.to_string(),
+ )
+ .await
+ {
+ Ok(Some(path)) => path,
+ Ok(None) => {
+ self.send_invalid_request_error(
+ request_id,
+ format!("no rollout found for thread id {existing_thread_id}"),
+ )
+ .await;
+ return None;
+ }
+ Err(err) => {
+ self.send_invalid_request_error(
+ request_id,
+ format!("failed to locate thread id {existing_thread_id}: {err}"),
+ )
+ .await;
+ return None;
+ }
+ }
+ };
+
+ match RolloutRecorder::get_rollout_history(&rollout_path).await {
+ Ok(initial_history) => Some(initial_history),
+ Err(err) => {
+ self.send_invalid_request_error(
+ request_id,
+ format!("failed to load rollout `{}`: {err}", rollout_path.display()),
+ )
+ .await;
+ None
+ }
+ }
+ }
+
+ async fn load_thread_from_rollout_or_send_internal(
+ &self,
+ request_id: ConnectionRequestId,
+ thread_id: ThreadId,
+ rollout_path: &Path,
+ fallback_provider: &str,
+ ) -> Option {
+ let mut thread = match read_summary_from_rollout(rollout_path, fallback_provider).await {
+ Ok(summary) => summary_to_thread(summary),
+ Err(err) => {
+ self.send_internal_error(
+ request_id,
+ format!(
+ "failed to load rollout `{}` for thread {thread_id}: {err}",
+ rollout_path.display()
+ ),
+ )
+ .await;
+ return None;
+ }
+ };
+ match read_rollout_items_from_rollout(rollout_path).await {
+ Ok(items) => {
+ thread.turns = build_turns_from_rollout_items(&items);
+ Some(thread)
+ }
+ Err(err) => {
+ self.send_internal_error(
+ request_id,
+ format!(
+ "failed to load rollout `{}` for thread {thread_id}: {err}",
+ rollout_path.display()
+ ),
+ )
+ .await;
+ None
+ }
+ }
+ }
+
async fn thread_fork(&mut self, request_id: ConnectionRequestId, params: ThreadForkParams) {
let ThreadForkParams {
thread_id,
@@ -3389,14 +3620,15 @@ impl CodexMessageProcessor {
async fn list_models(
outgoing: Arc,
thread_manager: Arc,
- config: Arc,
request_id: ConnectionRequestId,
params: ModelListParams,
) {
- let ModelListParams { limit, cursor } = params;
- let mut config = (*config).clone();
- config.features.enable(Feature::RemoteModels);
- let models = supported_models(thread_manager, &config).await;
+ let ModelListParams {
+ limit,
+ cursor,
+ include_hidden,
+ } = params;
+ let models = supported_models(thread_manager, include_hidden.unwrap_or(false)).await;
let total = models.len();
if total == 0 {
@@ -4833,12 +5065,25 @@ impl CodexMessageProcessor {
.await;
}
- async fn skills_remote_read(
+ async fn skills_remote_list(
&self,
request_id: ConnectionRequestId,
- _params: SkillsRemoteReadParams,
+ params: SkillsRemoteReadParams,
) {
- match list_remote_skills(&self.config).await {
+ let hazelnut_scope = convert_remote_scope(params.hazelnut_scope);
+ let product_surface = convert_remote_product_surface(params.product_surface);
+ let enabled = if params.enabled { Some(true) } else { None };
+
+ let auth = self.auth_manager.auth().await;
+ match list_remote_skills(
+ &self.config,
+ auth.as_ref(),
+ hazelnut_scope,
+ product_surface,
+ enabled,
+ )
+ .await
+ {
Ok(skills) => {
let data = skills
.into_iter()
@@ -4855,23 +5100,21 @@ impl CodexMessageProcessor {
Err(err) => {
self.send_internal_error(
request_id,
- format!("failed to read remote skills: {err}"),
+ format!("failed to list remote skills: {err}"),
)
.await;
}
}
}
- async fn skills_remote_write(
+ async fn skills_remote_export(
&self,
request_id: ConnectionRequestId,
params: SkillsRemoteWriteParams,
) {
- let SkillsRemoteWriteParams {
- hazelnut_id,
- is_preload,
- } = params;
- let response = download_remote_skill(&self.config, hazelnut_id.as_str(), is_preload).await;
+ let SkillsRemoteWriteParams { hazelnut_id } = params;
+ let auth = self.auth_manager.auth().await;
+ let response = export_remote_skill(&self.config, auth.as_ref(), hazelnut_id.as_str()).await;
match response {
Ok(downloaded) => {
@@ -4880,7 +5123,6 @@ impl CodexMessageProcessor {
request_id,
SkillsRemoteWriteResponse {
id: downloaded.id,
- name: downloaded.name,
path: downloaded.path,
},
)
@@ -5808,6 +6050,101 @@ impl CodexMessageProcessor {
}
}
+fn collect_resume_override_mismatches(
+ request: &ThreadResumeParams,
+ config_snapshot: &ThreadConfigSnapshot,
+) -> Vec {
+ let mut mismatch_details = Vec::new();
+
+ if let Some(requested_model) = request.model.as_deref()
+ && requested_model != config_snapshot.model
+ {
+ mismatch_details.push(format!(
+ "model requested={requested_model} active={}",
+ config_snapshot.model
+ ));
+ }
+ if let Some(requested_provider) = request.model_provider.as_deref()
+ && requested_provider != config_snapshot.model_provider_id
+ {
+ mismatch_details.push(format!(
+ "model_provider requested={requested_provider} active={}",
+ config_snapshot.model_provider_id
+ ));
+ }
+ if let Some(requested_cwd) = request.cwd.as_deref() {
+ let requested_cwd_path = std::path::PathBuf::from(requested_cwd);
+ if requested_cwd_path != config_snapshot.cwd {
+ mismatch_details.push(format!(
+ "cwd requested={} active={}",
+ requested_cwd_path.display(),
+ config_snapshot.cwd.display()
+ ));
+ }
+ }
+ if let Some(requested_approval) = request.approval_policy.as_ref() {
+ let active_approval: AskForApproval = config_snapshot.approval_policy.into();
+ if requested_approval != &active_approval {
+ mismatch_details.push(format!(
+ "approval_policy requested={requested_approval:?} active={active_approval:?}"
+ ));
+ }
+ }
+ if let Some(requested_sandbox) = request.sandbox.as_ref() {
+ let sandbox_matches = matches!(
+ (requested_sandbox, &config_snapshot.sandbox_policy),
+ (
+ SandboxMode::ReadOnly,
+ codex_protocol::protocol::SandboxPolicy::ReadOnly { .. }
+ ) | (
+ SandboxMode::WorkspaceWrite,
+ codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. }
+ ) | (
+ SandboxMode::DangerFullAccess,
+ codex_protocol::protocol::SandboxPolicy::DangerFullAccess
+ ) | (
+ SandboxMode::DangerFullAccess,
+ codex_protocol::protocol::SandboxPolicy::ExternalSandbox { .. }
+ )
+ );
+ if !sandbox_matches {
+ mismatch_details.push(format!(
+ "sandbox requested={requested_sandbox:?} active={:?}",
+ config_snapshot.sandbox_policy
+ ));
+ }
+ }
+ if let Some(requested_personality) = request.personality.as_ref()
+ && config_snapshot.personality.as_ref() != Some(requested_personality)
+ {
+ mismatch_details.push(format!(
+ "personality requested={requested_personality:?} active={:?}",
+ config_snapshot.personality
+ ));
+ }
+
+ if request.config.is_some() {
+ mismatch_details
+ .push("config overrides were provided and ignored while running".to_string());
+ }
+ if request.base_instructions.is_some() {
+ mismatch_details
+ .push("baseInstructions override was provided and ignored while running".to_string());
+ }
+ if request.developer_instructions.is_some() {
+ mismatch_details.push(
+ "developerInstructions override was provided and ignored while running".to_string(),
+ );
+ }
+ if request.persist_extended_history {
+ mismatch_details.push(
+ "persistExtendedHistory override was provided and ignored while running".to_string(),
+ );
+ }
+
+ mismatch_details
+}
+
fn skills_to_info(
skills: &[codex_core::skills::SkillMetadata],
disabled_paths: &std::collections::HashSet,
diff --git a/codex-rs/app-server/src/fuzzy_file_search.rs b/codex-rs/app-server/src/fuzzy_file_search.rs
index cb1f61c362..d40d3fc242 100644
--- a/codex-rs/app-server/src/fuzzy_file_search.rs
+++ b/codex-rs/app-server/src/fuzzy_file_search.rs
@@ -6,6 +6,7 @@ use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use codex_app_server_protocol::FuzzyFileSearchResult;
+use codex_app_server_protocol::FuzzyFileSearchSessionCompletedNotification;
use codex_app_server_protocol::FuzzyFileSearchSessionUpdatedNotification;
use codex_app_server_protocol::ServerNotification;
use codex_file_search as file_search;
@@ -195,6 +196,20 @@ impl SessionReporterImpl {
outgoing.send_server_notification(notification).await;
});
}
+
+ fn send_complete(&self) {
+ if self.shared.canceled.load(Ordering::Relaxed) {
+ return;
+ }
+ let session_id = self.shared.session_id.clone();
+ let outgoing = self.shared.outgoing.clone();
+ self.shared.runtime.spawn(async move {
+ let notification = ServerNotification::FuzzyFileSearchSessionCompleted(
+ FuzzyFileSearchSessionCompletedNotification { session_id },
+ );
+ outgoing.send_server_notification(notification).await;
+ });
+ }
}
impl file_search::SessionReporter for SessionReporterImpl {
@@ -202,7 +217,9 @@ impl file_search::SessionReporter for SessionReporterImpl {
self.send_snapshot(snapshot);
}
- fn on_complete(&self) {}
+ fn on_complete(&self) {
+ self.send_complete();
+ }
}
fn collect_files(snapshot: &file_search::FileSearchSnapshot) -> Vec {
diff --git a/codex-rs/app-server/src/models.rs b/codex-rs/app-server/src/models.rs
index 350b86f92a..1594c66229 100644
--- a/codex-rs/app-server/src/models.rs
+++ b/codex-rs/app-server/src/models.rs
@@ -3,17 +3,19 @@ use std::sync::Arc;
use codex_app_server_protocol::Model;
use codex_app_server_protocol::ReasoningEffortOption;
use codex_core::ThreadManager;
-use codex_core::config::Config;
use codex_core::models_manager::manager::RefreshStrategy;
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::openai_models::ReasoningEffortPreset;
-pub async fn supported_models(thread_manager: Arc, config: &Config) -> Vec {
+pub async fn supported_models(
+ thread_manager: Arc,
+ include_hidden: bool,
+) -> Vec {
thread_manager
- .list_models(config, RefreshStrategy::OnlineIfUncached)
+ .list_models(RefreshStrategy::OnlineIfUncached)
.await
.into_iter()
- .filter(|preset| preset.show_in_picker)
+ .filter(|preset| include_hidden || preset.show_in_picker)
.map(model_from_preset)
.collect()
}
@@ -25,6 +27,7 @@ fn model_from_preset(preset: ModelPreset) -> Model {
upgrade: preset.upgrade.map(|upgrade| upgrade.id),
display_name: preset.display_name.to_string(),
description: preset.description.to_string(),
+ hidden: !preset.show_in_picker,
supported_reasoning_efforts: reasoning_efforts_from_preset(
preset.supported_reasoning_efforts,
),
diff --git a/codex-rs/app-server/src/outgoing_message.rs b/codex-rs/app-server/src/outgoing_message.rs
index 2d8b748c90..810182db0a 100644
--- a/codex-rs/app-server/src/outgoing_message.rs
+++ b/codex-rs/app-server/src/outgoing_message.rs
@@ -399,6 +399,8 @@ mod tests {
use codex_app_server_protocol::AuthMode;
use codex_app_server_protocol::ConfigWarningNotification;
use codex_app_server_protocol::LoginChatGptCompleteNotification;
+ use codex_app_server_protocol::ModelRerouteReason;
+ use codex_app_server_protocol::ModelReroutedNotification;
use codex_app_server_protocol::RateLimitSnapshot;
use codex_app_server_protocol::RateLimitWindow;
use codex_protocol::ThreadId;
@@ -546,6 +548,34 @@ mod tests {
);
}
+ #[test]
+ fn verify_model_rerouted_notification_serialization() {
+ let notification = ServerNotification::ModelRerouted(ModelReroutedNotification {
+ thread_id: "thread-1".to_string(),
+ turn_id: "turn-1".to_string(),
+ from_model: "gpt-5.3-codex".to_string(),
+ to_model: "gpt-5.2".to_string(),
+ reason: ModelRerouteReason::HighRiskCyberActivity,
+ });
+
+ let jsonrpc_notification = OutgoingMessage::AppServerNotification(notification);
+ assert_eq!(
+ json!({
+ "method": "model/rerouted",
+ "params": {
+ "threadId": "thread-1",
+ "turnId": "turn-1",
+ "fromModel": "gpt-5.3-codex",
+ "toModel": "gpt-5.2",
+ "reason": "highRiskCyberActivity",
+ },
+ }),
+ serde_json::to_value(jsonrpc_notification)
+ .expect("ensure the notification serializes correctly"),
+ "ensure the notification serializes correctly"
+ );
+ }
+
#[tokio::test]
async fn send_response_routes_to_target_connection() {
let (tx, mut rx) = mpsc::channel::(4);
diff --git a/codex-rs/app-server/tests/common/config.rs b/codex-rs/app-server/tests/common/config.rs
index 09471b4a69..bffb35aa02 100644
--- a/codex-rs/app-server/tests/common/config.rs
+++ b/codex-rs/app-server/tests/common/config.rs
@@ -13,7 +13,7 @@ pub fn write_mock_responses_config_toml(
compact_prompt: &str,
) -> std::io::Result<()> {
// Phase 1: build the features block for config.toml.
- let mut features = BTreeMap::from([(Feature::RemoteModels, false)]);
+ let mut features = BTreeMap::new();
for (feature, enabled) in feature_flags {
features.insert(*feature, *enabled);
}
diff --git a/codex-rs/app-server/tests/common/models_cache.rs b/codex-rs/app-server/tests/common/models_cache.rs
index 242a71f98d..5095bf0f3d 100644
--- a/codex-rs/app-server/tests/common/models_cache.rs
+++ b/codex-rs/app-server/tests/common/models_cache.rs
@@ -41,6 +41,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
experimental_supported_tools: Vec::new(),
input_modalities: default_input_modalities(),
prefer_websockets: false,
+ used_fallback_model_metadata: false,
}
}
diff --git a/codex-rs/app-server/tests/suite/fuzzy_file_search.rs b/codex-rs/app-server/tests/suite/fuzzy_file_search.rs
index ffa4fa2cca..b6932560f0 100644
--- a/codex-rs/app-server/tests/suite/fuzzy_file_search.rs
+++ b/codex-rs/app-server/tests/suite/fuzzy_file_search.rs
@@ -1,6 +1,7 @@
use anyhow::Result;
use anyhow::anyhow;
use app_test_support::McpProcess;
+use codex_app_server_protocol::FuzzyFileSearchSessionCompletedNotification;
use codex_app_server_protocol::FuzzyFileSearchSessionUpdatedNotification;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
@@ -13,6 +14,7 @@ const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs
const SHORT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(500);
const STOP_GRACE_PERIOD: std::time::Duration = std::time::Duration::from_millis(250);
const SESSION_UPDATED_METHOD: &str = "fuzzyFileSearch/sessionUpdated";
+const SESSION_COMPLETED_METHOD: &str = "fuzzyFileSearch/sessionCompleted";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FileExpectation {
@@ -60,6 +62,29 @@ async fn wait_for_session_updated(
);
}
+async fn wait_for_session_completed(
+ mcp: &mut McpProcess,
+ session_id: &str,
+) -> Result {
+ for _ in 0..20 {
+ let notification = timeout(
+ DEFAULT_READ_TIMEOUT,
+ mcp.read_stream_until_notification_message(SESSION_COMPLETED_METHOD),
+ )
+ .await??;
+ let params = notification
+ .params
+ .ok_or_else(|| anyhow!("missing notification params"))?;
+ let payload =
+ serde_json::from_value::(params)?;
+ if payload.session_id == session_id {
+ return Ok(payload);
+ }
+ }
+
+ anyhow::bail!("did not receive expected session completion for sessionId={session_id}");
+}
+
async fn assert_update_request_fails_for_missing_session(
mcp: &mut McpProcess,
session_id: &str,
@@ -271,12 +296,41 @@ async fn test_fuzzy_file_search_session_streams_updates() -> Result<()> {
assert_eq!(payload.files.len(), 1);
assert_eq!(payload.files[0].root, root_path);
assert_eq!(payload.files[0].path, "alpha.txt");
+ let completed = wait_for_session_completed(&mut mcp, session_id).await?;
+ assert_eq!(completed.session_id, session_id);
mcp.stop_fuzzy_file_search_session(session_id).await?;
Ok(())
}
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn test_fuzzy_file_search_session_no_updates_after_complete_until_query_edited() -> Result<()>
+{
+ let codex_home = TempDir::new()?;
+ let root = TempDir::new()?;
+ std::fs::write(root.path().join("alpha.txt"), "contents")?;
+ let mut mcp = initialized_mcp(&codex_home).await?;
+
+ let root_path = root.path().to_string_lossy().to_string();
+ let session_id = "session-complete-invariant";
+ mcp.start_fuzzy_file_search_session(session_id, vec![root_path])
+ .await?;
+
+ mcp.update_fuzzy_file_search_session(session_id, "alp")
+ .await?;
+ wait_for_session_updated(&mut mcp, session_id, "alp", FileExpectation::NonEmpty).await?;
+ wait_for_session_completed(&mut mcp, session_id).await?;
+ assert_no_session_updates_for(&mut mcp, session_id, STOP_GRACE_PERIOD, SHORT_READ_TIMEOUT)
+ .await?;
+
+ mcp.update_fuzzy_file_search_session(session_id, "alpha")
+ .await?;
+ wait_for_session_updated(&mut mcp, session_id, "alpha", FileExpectation::NonEmpty).await?;
+
+ Ok(())
+}
+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_fuzzy_file_search_session_update_before_start_errors() -> Result<()> {
let codex_home = TempDir::new()?;
@@ -345,6 +399,7 @@ async fn test_fuzzy_file_search_session_multiple_query_updates_work() -> Result<
alp_payload.files.iter().all(|file| file.root == root_path),
true
);
+ wait_for_session_completed(&mut mcp, session_id).await?;
mcp.update_fuzzy_file_search_session(session_id, "zzzz")
.await?;
@@ -352,6 +407,7 @@ async fn test_fuzzy_file_search_session_multiple_query_updates_work() -> Result<
wait_for_session_updated(&mut mcp, session_id, "zzzz", FileExpectation::Any).await?;
assert_eq!(zzzz_payload.query, "zzzz");
assert_eq!(zzzz_payload.files.is_empty(), true);
+ wait_for_session_completed(&mut mcp, session_id).await?;
Ok(())
}
diff --git a/codex-rs/app-server/tests/suite/send_message.rs b/codex-rs/app-server/tests/suite/send_message.rs
index d5e0154121..25dad0e0ae 100644
--- a/codex-rs/app-server/tests/suite/send_message.rs
+++ b/codex-rs/app-server/tests/suite/send_message.rs
@@ -477,7 +477,6 @@ fn assert_permissions_message(item: &ResponseItem) {
&SandboxPolicy::DangerFullAccess,
AskForApproval::Never,
&Policy::empty(),
- false,
&PathBuf::from("/tmp"),
)
.into_text();
diff --git a/codex-rs/app-server/tests/suite/v2/app_list.rs b/codex-rs/app-server/tests/suite/v2/app_list.rs
index 079eba4794..1fd3f5a447 100644
--- a/codex-rs/app-server/tests/suite/v2/app_list.rs
+++ b/codex-rs/app-server/tests/suite/v2/app_list.rs
@@ -1,4 +1,5 @@
use std::borrow::Cow;
+use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex as StdMutex;
use std::time::Duration;
@@ -16,8 +17,12 @@ use axum::http::HeaderMap;
use axum::http::StatusCode;
use axum::http::header::AUTHORIZATION;
use axum::routing::get;
+use codex_app_server_protocol::AppBranding;
use codex_app_server_protocol::AppInfo;
use codex_app_server_protocol::AppListUpdatedNotification;
+use codex_app_server_protocol::AppMetadata;
+use codex_app_server_protocol::AppReview;
+use codex_app_server_protocol::AppScreenshot;
use codex_app_server_protocol::AppsListParams;
use codex_app_server_protocol::AppsListResponse;
use codex_app_server_protocol::JSONRPCError;
@@ -85,6 +90,9 @@ async fn list_apps_uses_thread_feature_flag_when_thread_id_is_provided() -> Resu
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: None,
is_accessible: false,
is_enabled: true,
@@ -184,6 +192,9 @@ async fn list_apps_reports_is_enabled_from_config() -> Result<()> {
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: None,
is_accessible: false,
is_enabled: true,
@@ -249,6 +260,39 @@ enabled = false
#[tokio::test]
async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<()> {
+ let alpha_branding = Some(AppBranding {
+ category: Some("PRODUCTIVITY".to_string()),
+ developer: Some("Acme".to_string()),
+ website: Some("https://acme.example".to_string()),
+ privacy_policy: Some("https://acme.example/privacy".to_string()),
+ terms_of_service: Some("https://acme.example/terms".to_string()),
+ is_discoverable_app: true,
+ });
+ let alpha_app_metadata = Some(AppMetadata {
+ review: Some(AppReview {
+ status: "APPROVED".to_string(),
+ }),
+ categories: Some(vec!["PRODUCTIVITY".to_string()]),
+ sub_categories: Some(vec!["WRITING".to_string()]),
+ seo_description: Some("Alpha connector".to_string()),
+ screenshots: Some(vec![AppScreenshot {
+ url: Some("https://example.com/alpha-screenshot.png".to_string()),
+ file_id: Some("file_123".to_string()),
+ user_prompt: "Summarize this draft".to_string(),
+ }]),
+ developer: Some("Acme".to_string()),
+ version: Some("1.2.3".to_string()),
+ version_id: Some("version_123".to_string()),
+ version_notes: Some("Fixes and improvements".to_string()),
+ first_party_type: Some("internal".to_string()),
+ first_party_requires_install: Some(true),
+ show_in_composer_when_unlinked: Some(true),
+ });
+ let alpha_labels = Some(HashMap::from([
+ ("feature".to_string(), "beta".to_string()),
+ ("source".to_string(), "directory".to_string()),
+ ]));
+
let connectors = vec![
AppInfo {
id: "alpha".to_string(),
@@ -257,6 +301,9 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<(
logo_url: Some("https://example.com/alpha.png".to_string()),
logo_url_dark: None,
distribution_channel: None,
+ branding: alpha_branding.clone(),
+ app_metadata: alpha_app_metadata.clone(),
+ labels: alpha_labels.clone(),
install_url: None,
is_accessible: false,
is_enabled: true,
@@ -268,6 +315,9 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<(
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: None,
is_accessible: false,
is_enabled: true,
@@ -313,6 +363,9 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<(
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: Some("https://chatgpt.com/apps/beta-app/beta".to_string()),
is_accessible: true,
is_enabled: true,
@@ -329,6 +382,9 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<(
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: Some("https://chatgpt.com/apps/beta/beta".to_string()),
is_accessible: true,
is_enabled: true,
@@ -340,6 +396,9 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<(
logo_url: Some("https://example.com/alpha.png".to_string()),
logo_url_dark: None,
distribution_channel: None,
+ branding: alpha_branding,
+ app_metadata: alpha_app_metadata,
+ labels: alpha_labels,
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
is_accessible: false,
is_enabled: true,
@@ -377,6 +436,9 @@ async fn list_apps_returns_connectors_with_accessible_flags() -> Result<()> {
logo_url: Some("https://example.com/alpha.png".to_string()),
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: None,
is_accessible: false,
is_enabled: true,
@@ -388,6 +450,9 @@ async fn list_apps_returns_connectors_with_accessible_flags() -> Result<()> {
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: None,
is_accessible: false,
is_enabled: true,
@@ -437,6 +502,9 @@ async fn list_apps_returns_connectors_with_accessible_flags() -> Result<()> {
logo_url: Some("https://example.com/alpha.png".to_string()),
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
is_accessible: false,
is_enabled: true,
@@ -448,6 +516,9 @@ async fn list_apps_returns_connectors_with_accessible_flags() -> Result<()> {
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: Some("https://chatgpt.com/apps/beta/beta".to_string()),
is_accessible: false,
is_enabled: true,
@@ -463,6 +534,9 @@ async fn list_apps_returns_connectors_with_accessible_flags() -> Result<()> {
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: Some("https://chatgpt.com/apps/beta/beta".to_string()),
is_accessible: true,
is_enabled: true,
@@ -474,6 +548,9 @@ async fn list_apps_returns_connectors_with_accessible_flags() -> Result<()> {
logo_url: Some("https://example.com/alpha.png".to_string()),
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
is_accessible: false,
is_enabled: true,
@@ -506,6 +583,9 @@ async fn list_apps_paginates_results() -> Result<()> {
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: None,
is_accessible: false,
is_enabled: true,
@@ -517,6 +597,9 @@ async fn list_apps_paginates_results() -> Result<()> {
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: None,
is_accessible: false,
is_enabled: true,
@@ -571,6 +654,9 @@ async fn list_apps_paginates_results() -> Result<()> {
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: Some("https://chatgpt.com/apps/beta/beta".to_string()),
is_accessible: true,
is_enabled: true,
@@ -611,6 +697,9 @@ async fn list_apps_paginates_results() -> Result<()> {
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
is_accessible: false,
is_enabled: true,
@@ -632,6 +721,9 @@ async fn list_apps_force_refetch_preserves_previous_cache_on_failure() -> Result
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: None,
is_accessible: false,
is_enabled: true,
@@ -733,6 +825,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: None,
is_accessible: false,
is_enabled: true,
@@ -744,6 +839,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: None,
is_accessible: false,
is_enabled: true,
@@ -790,6 +888,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: Some("https://chatgpt.com/apps/beta-app/beta".to_string()),
is_accessible: true,
is_enabled: true,
@@ -807,6 +908,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: Some("https://chatgpt.com/apps/beta-app/beta".to_string()),
is_accessible: true,
is_enabled: true,
@@ -818,6 +922,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
is_accessible: false,
is_enabled: true,
@@ -844,6 +951,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: None,
is_accessible: false,
is_enabled: true,
@@ -870,6 +980,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
is_accessible: false,
is_enabled: true,
@@ -881,6 +994,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: Some("https://chatgpt.com/apps/beta-app/beta".to_string()),
is_accessible: false,
is_enabled: true,
@@ -895,6 +1011,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
is_accessible: false,
is_enabled: true,
diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs
index 48622acddb..1eacc2a84e 100644
--- a/codex-rs/app-server/tests/suite/v2/mod.rs
+++ b/codex-rs/app-server/tests/suite/v2/mod.rs
@@ -15,6 +15,7 @@ mod plan_item;
mod rate_limits;
mod request_user_input;
mod review;
+mod safety_check_downgrade;
mod skills_list;
mod thread_archive;
mod thread_fork;
diff --git a/codex-rs/app-server/tests/suite/v2/model_list.rs b/codex-rs/app-server/tests/suite/v2/model_list.rs
index c2431d24d0..ebcbdf99e7 100644
--- a/codex-rs/app-server/tests/suite/v2/model_list.rs
+++ b/codex-rs/app-server/tests/suite/v2/model_list.rs
@@ -33,6 +33,7 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
.send_list_models_request(ModelListParams {
limit: Some(100),
cursor: None,
+ include_hidden: None,
})
.await?;
@@ -54,6 +55,7 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
upgrade: None,
display_name: "gpt-5.2-codex".to_string(),
description: "Latest frontier agentic coding model.".to_string(),
+ hidden: false,
supported_reasoning_efforts: vec![
ReasoningEffortOption {
reasoning_effort: ReasoningEffort::Low,
@@ -84,6 +86,7 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
upgrade: Some("gpt-5.2-codex".to_string()),
display_name: "gpt-5.1-codex-max".to_string(),
description: "Codex-optimized flagship for deep and fast reasoning.".to_string(),
+ hidden: false,
supported_reasoning_efforts: vec![
ReasoningEffortOption {
reasoning_effort: ReasoningEffort::Low,
@@ -114,6 +117,7 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
upgrade: Some("gpt-5.2-codex".to_string()),
display_name: "gpt-5.1-codex-mini".to_string(),
description: "Optimized for codex. Cheaper, faster, but less capable.".to_string(),
+ hidden: false,
supported_reasoning_efforts: vec![
ReasoningEffortOption {
reasoning_effort: ReasoningEffort::Medium,
@@ -138,6 +142,7 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
description:
"Latest frontier model with improvements across knowledge, reasoning and coding"
.to_string(),
+ hidden: false,
supported_reasoning_efforts: vec![
ReasoningEffortOption {
reasoning_effort: ReasoningEffort::Low,
@@ -173,6 +178,38 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
Ok(())
}
+#[tokio::test]
+async fn list_models_includes_hidden_models() -> Result<()> {
+ let codex_home = TempDir::new()?;
+ write_models_cache(codex_home.path())?;
+ let mut mcp = McpProcess::new(codex_home.path()).await?;
+
+ timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
+
+ let request_id = mcp
+ .send_list_models_request(ModelListParams {
+ limit: Some(100),
+ cursor: None,
+ include_hidden: Some(true),
+ })
+ .await?;
+
+ let response: JSONRPCResponse = timeout(
+ DEFAULT_TIMEOUT,
+ mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
+ )
+ .await??;
+
+ let ModelListResponse {
+ data: items,
+ next_cursor,
+ } = to_response::(response)?;
+
+ assert!(items.iter().any(|item| item.hidden));
+ assert!(next_cursor.is_none());
+ Ok(())
+}
+
#[tokio::test]
async fn list_models_pagination_works() -> Result<()> {
let codex_home = TempDir::new()?;
@@ -185,6 +222,7 @@ async fn list_models_pagination_works() -> Result<()> {
.send_list_models_request(ModelListParams {
limit: Some(1),
cursor: None,
+ include_hidden: None,
})
.await?;
@@ -207,6 +245,7 @@ async fn list_models_pagination_works() -> Result<()> {
.send_list_models_request(ModelListParams {
limit: Some(1),
cursor: Some(next_cursor.clone()),
+ include_hidden: None,
})
.await?;
@@ -229,6 +268,7 @@ async fn list_models_pagination_works() -> Result<()> {
.send_list_models_request(ModelListParams {
limit: Some(1),
cursor: Some(third_cursor.clone()),
+ include_hidden: None,
})
.await?;
@@ -251,6 +291,7 @@ async fn list_models_pagination_works() -> Result<()> {
.send_list_models_request(ModelListParams {
limit: Some(1),
cursor: Some(fourth_cursor.clone()),
+ include_hidden: None,
})
.await?;
@@ -283,6 +324,7 @@ async fn list_models_rejects_invalid_cursor() -> Result<()> {
.send_list_models_request(ModelListParams {
limit: None,
cursor: Some("invalid".to_string()),
+ include_hidden: None,
})
.await?;
diff --git a/codex-rs/app-server/tests/suite/v2/plan_item.rs b/codex-rs/app-server/tests/suite/v2/plan_item.rs
index d138954ac4..881bbb54c8 100644
--- a/codex-rs/app-server/tests/suite/v2/plan_item.rs
+++ b/codex-rs/app-server/tests/suite/v2/plan_item.rs
@@ -215,10 +215,7 @@ async fn collect_turn_notifications(
}
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> {
- let features = BTreeMap::from([
- (Feature::RemoteModels, false),
- (Feature::CollaborationModes, true),
- ]);
+ let features = BTreeMap::from([(Feature::CollaborationModes, true)]);
let feature_entries = features
.into_iter()
.map(|(feature, enabled)| {
diff --git a/codex-rs/app-server/tests/suite/v2/review.rs b/codex-rs/app-server/tests/suite/v2/review.rs
index 05397d485d..219812d6b1 100644
--- a/codex-rs/app-server/tests/suite/v2/review.rs
+++ b/codex-rs/app-server/tests/suite/v2/review.rs
@@ -437,7 +437,6 @@ sandbox_mode = "read-only"
model_provider = "mock_provider"
[features]
-remote_models = false
shell_snapshot = false
[model_providers.mock_provider]
diff --git a/codex-rs/app-server/tests/suite/v2/safety_check_downgrade.rs b/codex-rs/app-server/tests/suite/v2/safety_check_downgrade.rs
new file mode 100644
index 0000000000..53acd492b6
--- /dev/null
+++ b/codex-rs/app-server/tests/suite/v2/safety_check_downgrade.rs
@@ -0,0 +1,244 @@
+use anyhow::Result;
+use app_test_support::McpProcess;
+use app_test_support::to_response;
+use codex_app_server_protocol::ItemCompletedNotification;
+use codex_app_server_protocol::ItemStartedNotification;
+use codex_app_server_protocol::JSONRPCMessage;
+use codex_app_server_protocol::JSONRPCResponse;
+use codex_app_server_protocol::ModelRerouteReason;
+use codex_app_server_protocol::ModelReroutedNotification;
+use codex_app_server_protocol::RequestId;
+use codex_app_server_protocol::ThreadItem;
+use codex_app_server_protocol::ThreadStartParams;
+use codex_app_server_protocol::ThreadStartResponse;
+use codex_app_server_protocol::TurnStartParams;
+use codex_app_server_protocol::TurnStartResponse;
+use codex_app_server_protocol::UserInput;
+use core_test_support::responses;
+use core_test_support::skip_if_no_network;
+use pretty_assertions::assert_eq;
+use tempfile::TempDir;
+use tokio::time::timeout;
+
+const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
+const REQUESTED_MODEL: &str = "gpt-5.1-codex-max";
+const SERVER_MODEL: &str = "gpt-5.2-codex";
+
+#[tokio::test]
+async fn openai_model_header_mismatch_emits_model_rerouted_notification_v2() -> Result<()> {
+ skip_if_no_network!(Ok(()));
+
+ let server = responses::start_mock_server().await;
+ let body = responses::sse(vec![
+ responses::ev_response_created("resp-1"),
+ responses::ev_assistant_message("msg-1", "Done"),
+ responses::ev_completed("resp-1"),
+ ]);
+ let response = responses::sse_response(body).insert_header("OpenAI-Model", SERVER_MODEL);
+ let _response_mock = responses::mount_response_once(&server, response).await;
+
+ let codex_home = TempDir::new()?;
+ create_config_toml(codex_home.path(), &server.uri())?;
+
+ let mut mcp = McpProcess::new(codex_home.path()).await?;
+ timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
+
+ let thread_req = mcp
+ .send_thread_start_request(ThreadStartParams {
+ model: Some(REQUESTED_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::(thread_resp)?;
+
+ let turn_req = mcp
+ .send_turn_start_request(TurnStartParams {
+ thread_id: thread.id.clone(),
+ input: vec![UserInput::Text {
+ text: "trigger safeguard".to_string(),
+ text_elements: Vec::new(),
+ }],
+ ..Default::default()
+ })
+ .await?;
+ let turn_resp: JSONRPCResponse = timeout(
+ DEFAULT_READ_TIMEOUT,
+ mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
+ )
+ .await??;
+ let turn_start: TurnStartResponse = to_response(turn_resp)?;
+
+ let rerouted = collect_turn_notifications_and_validate_no_warning_item(&mut mcp).await?;
+ assert_eq!(
+ rerouted,
+ ModelReroutedNotification {
+ thread_id: thread.id,
+ turn_id: turn_start.turn.id,
+ from_model: REQUESTED_MODEL.to_string(),
+ to_model: SERVER_MODEL.to_string(),
+ reason: ModelRerouteReason::HighRiskCyberActivity,
+ }
+ );
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn response_model_field_mismatch_emits_model_rerouted_notification_v2_when_header_matches_requested()
+-> Result<()> {
+ skip_if_no_network!(Ok(()));
+
+ let server = responses::start_mock_server().await;
+ let body = responses::sse(vec![
+ serde_json::json!({
+ "type": "response.created",
+ "response": {
+ "id": "resp-1",
+ "model": SERVER_MODEL,
+ }
+ }),
+ responses::ev_assistant_message("msg-1", "Done"),
+ responses::ev_completed("resp-1"),
+ ]);
+ let response = responses::sse_response(body).insert_header("OpenAI-Model", REQUESTED_MODEL);
+ let _response_mock = responses::mount_response_once(&server, response).await;
+
+ let codex_home = TempDir::new()?;
+ create_config_toml(codex_home.path(), &server.uri())?;
+
+ let mut mcp = McpProcess::new(codex_home.path()).await?;
+ timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
+
+ let thread_req = mcp
+ .send_thread_start_request(ThreadStartParams {
+ model: Some(REQUESTED_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::(thread_resp)?;
+
+ let turn_req = mcp
+ .send_turn_start_request(TurnStartParams {
+ thread_id: thread.id.clone(),
+ input: vec![UserInput::Text {
+ text: "trigger response model check".to_string(),
+ text_elements: Vec::new(),
+ }],
+ ..Default::default()
+ })
+ .await?;
+ let turn_resp: JSONRPCResponse = timeout(
+ DEFAULT_READ_TIMEOUT,
+ mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
+ )
+ .await??;
+ let turn_start: TurnStartResponse = to_response(turn_resp)?;
+
+ let rerouted = collect_turn_notifications_and_validate_no_warning_item(&mut mcp).await?;
+ assert_eq!(
+ rerouted,
+ ModelReroutedNotification {
+ thread_id: thread.id,
+ turn_id: turn_start.turn.id,
+ from_model: REQUESTED_MODEL.to_string(),
+ to_model: SERVER_MODEL.to_string(),
+ reason: ModelRerouteReason::HighRiskCyberActivity,
+ }
+ );
+
+ Ok(())
+}
+
+async fn collect_turn_notifications_and_validate_no_warning_item(
+ mcp: &mut McpProcess,
+) -> Result {
+ let mut rerouted = None;
+
+ loop {
+ let message = timeout(DEFAULT_READ_TIMEOUT, mcp.read_next_message()).await??;
+ let JSONRPCMessage::Notification(notification) = message else {
+ continue;
+ };
+ match notification.method.as_str() {
+ "model/rerouted" => {
+ let params = notification.params.ok_or_else(|| {
+ anyhow::anyhow!("model/rerouted notifications must include params")
+ })?;
+ let payload: ModelReroutedNotification = serde_json::from_value(params)?;
+ rerouted = Some(payload);
+ }
+ "item/started" => {
+ let params = notification.params.ok_or_else(|| {
+ anyhow::anyhow!("item/started notifications must include params")
+ })?;
+ let payload: ItemStartedNotification = serde_json::from_value(params)?;
+ assert!(!is_warning_user_message_item(&payload.item));
+ }
+ "item/completed" => {
+ let params = notification.params.ok_or_else(|| {
+ anyhow::anyhow!("item/completed notifications must include params")
+ })?;
+ let payload: ItemCompletedNotification = serde_json::from_value(params)?;
+ assert!(!is_warning_user_message_item(&payload.item));
+ }
+ "turn/completed" => {
+ return rerouted.ok_or_else(|| {
+ anyhow::anyhow!("expected model/rerouted notification before turn/completed")
+ });
+ }
+ _ => {}
+ }
+ }
+}
+
+fn warning_text_from_item(item: &ThreadItem) -> Option<&str> {
+ let ThreadItem::UserMessage { content, .. } = item else {
+ return None;
+ };
+
+ content.iter().find_map(|input| match input {
+ UserInput::Text { text, .. } if text.starts_with("Warning: ") => Some(text.as_str()),
+ _ => None,
+ })
+}
+
+fn is_warning_user_message_item(item: &ThreadItem) -> bool {
+ warning_text_from_item(item).is_some()
+}
+
+fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()> {
+ let config_toml = codex_home.join("config.toml");
+ std::fs::write(
+ config_toml,
+ format!(
+ r#"
+model = "{REQUESTED_MODEL}"
+approval_policy = "never"
+sandbox_mode = "read-only"
+
+model_provider = "mock_provider"
+
+[features]
+remote_models = false
+personality = true
+
+[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
+"#
+ ),
+ )
+}
diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs
index a92d24ed08..3976d3eea6 100644
--- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs
+++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs
@@ -209,18 +209,412 @@ async fn thread_resume_without_overrides_does_not_change_updated_at_or_mtime() -
Ok(())
}
+#[tokio::test]
+async fn thread_resume_keeps_in_flight_turn_streaming() -> Result<()> {
+ let server = create_mock_responses_server_repeating_assistant("Done").await;
+ let codex_home = TempDir::new()?;
+ create_config_toml(codex_home.path(), &server.uri())?;
+
+ let mut primary = McpProcess::new(codex_home.path()).await?;
+ timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??;
+
+ let start_id = primary
+ .send_thread_start_request(ThreadStartParams {
+ model: Some("gpt-5.1-codex-max".to_string()),
+ ..Default::default()
+ })
+ .await?;
+ let start_resp: JSONRPCResponse = timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_response_message(RequestId::Integer(start_id)),
+ )
+ .await??;
+ let ThreadStartResponse { thread, .. } = to_response::(start_resp)?;
+
+ let seed_turn_id = primary
+ .send_turn_start_request(TurnStartParams {
+ thread_id: thread.id.clone(),
+ input: vec![UserInput::Text {
+ text: "seed history".to_string(),
+ text_elements: Vec::new(),
+ }],
+ ..Default::default()
+ })
+ .await?;
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_response_message(RequestId::Integer(seed_turn_id)),
+ )
+ .await??;
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_notification_message("turn/completed"),
+ )
+ .await??;
+ primary.clear_message_buffer();
+
+ let mut secondary = McpProcess::new(codex_home.path()).await?;
+ timeout(DEFAULT_READ_TIMEOUT, secondary.initialize()).await??;
+
+ let turn_id = primary
+ .send_turn_start_request(TurnStartParams {
+ thread_id: thread.id.clone(),
+ input: vec![UserInput::Text {
+ text: "respond with docs".to_string(),
+ text_elements: Vec::new(),
+ }],
+ ..Default::default()
+ })
+ .await?;
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_response_message(RequestId::Integer(turn_id)),
+ )
+ .await??;
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_notification_message("turn/started"),
+ )
+ .await??;
+
+ let resume_id = secondary
+ .send_thread_resume_request(ThreadResumeParams {
+ thread_id: thread.id,
+ ..Default::default()
+ })
+ .await?;
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ secondary.read_stream_until_response_message(RequestId::Integer(resume_id)),
+ )
+ .await??;
+
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_notification_message("turn/completed"),
+ )
+ .await??;
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn thread_resume_rejects_history_when_thread_is_running() -> Result<()> {
+ let server = responses::start_mock_server().await;
+ let first_body = responses::sse(vec![
+ responses::ev_response_created("resp-1"),
+ responses::ev_assistant_message("msg-1", "Done"),
+ responses::ev_completed("resp-1"),
+ ]);
+ let second_body = responses::sse(vec![responses::ev_response_created("resp-2")]);
+ let _first_response_mock = responses::mount_sse_once(&server, first_body).await;
+ let _second_response_mock = responses::mount_sse_once(&server, second_body).await;
+ let codex_home = TempDir::new()?;
+ create_config_toml(codex_home.path(), &server.uri())?;
+
+ let mut primary = McpProcess::new(codex_home.path()).await?;
+ timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??;
+
+ let start_id = primary
+ .send_thread_start_request(ThreadStartParams {
+ model: Some("gpt-5.1-codex-max".to_string()),
+ ..Default::default()
+ })
+ .await?;
+ let start_resp: JSONRPCResponse = timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_response_message(RequestId::Integer(start_id)),
+ )
+ .await??;
+ let ThreadStartResponse { thread, .. } = to_response::(start_resp)?;
+
+ let seed_turn_id = primary
+ .send_turn_start_request(TurnStartParams {
+ thread_id: thread.id.clone(),
+ input: vec![UserInput::Text {
+ text: "seed history".to_string(),
+ text_elements: Vec::new(),
+ }],
+ ..Default::default()
+ })
+ .await?;
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_response_message(RequestId::Integer(seed_turn_id)),
+ )
+ .await??;
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_notification_message("turn/completed"),
+ )
+ .await??;
+ primary.clear_message_buffer();
+
+ let running_turn_id = primary
+ .send_turn_start_request(TurnStartParams {
+ thread_id: thread.id.clone(),
+ input: vec![UserInput::Text {
+ text: "keep running".to_string(),
+ text_elements: Vec::new(),
+ }],
+ ..Default::default()
+ })
+ .await?;
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_response_message(RequestId::Integer(running_turn_id)),
+ )
+ .await??;
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_notification_message("turn/started"),
+ )
+ .await??;
+
+ let resume_id = primary
+ .send_thread_resume_request(ThreadResumeParams {
+ thread_id: thread.id,
+ history: Some(vec![ResponseItem::Message {
+ id: None,
+ role: "user".to_string(),
+ content: vec![ContentItem::InputText {
+ text: "history override".to_string(),
+ }],
+ end_turn: None,
+ phase: None,
+ }]),
+ ..Default::default()
+ })
+ .await?;
+ let resume_err: JSONRPCError = timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_error_message(RequestId::Integer(resume_id)),
+ )
+ .await??;
+ assert!(
+ resume_err.error.message.contains("cannot resume thread")
+ && resume_err.error.message.contains("with history")
+ && resume_err.error.message.contains("running"),
+ "unexpected resume error: {}",
+ resume_err.error.message
+ );
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn thread_resume_rejects_mismatched_path_when_thread_is_running() -> Result<()> {
+ let server = responses::start_mock_server().await;
+ let first_body = responses::sse(vec![
+ responses::ev_response_created("resp-1"),
+ responses::ev_assistant_message("msg-1", "Done"),
+ responses::ev_completed("resp-1"),
+ ]);
+ let second_body = responses::sse(vec![responses::ev_response_created("resp-2")]);
+ let _first_response_mock = responses::mount_sse_once(&server, first_body).await;
+ let _second_response_mock = responses::mount_sse_once(&server, second_body).await;
+ let codex_home = TempDir::new()?;
+ create_config_toml(codex_home.path(), &server.uri())?;
+
+ let mut primary = McpProcess::new(codex_home.path()).await?;
+ timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??;
+
+ let start_id = primary
+ .send_thread_start_request(ThreadStartParams {
+ model: Some("gpt-5.1-codex-max".to_string()),
+ ..Default::default()
+ })
+ .await?;
+ let start_resp: JSONRPCResponse = timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_response_message(RequestId::Integer(start_id)),
+ )
+ .await??;
+ let ThreadStartResponse { thread, .. } = to_response::(start_resp)?;
+
+ let seed_turn_id = primary
+ .send_turn_start_request(TurnStartParams {
+ thread_id: thread.id.clone(),
+ input: vec![UserInput::Text {
+ text: "seed history".to_string(),
+ text_elements: Vec::new(),
+ }],
+ ..Default::default()
+ })
+ .await?;
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_response_message(RequestId::Integer(seed_turn_id)),
+ )
+ .await??;
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_notification_message("turn/completed"),
+ )
+ .await??;
+ primary.clear_message_buffer();
+
+ let running_turn_id = primary
+ .send_turn_start_request(TurnStartParams {
+ thread_id: thread.id.clone(),
+ input: vec![UserInput::Text {
+ text: "keep running".to_string(),
+ text_elements: Vec::new(),
+ }],
+ ..Default::default()
+ })
+ .await?;
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_response_message(RequestId::Integer(running_turn_id)),
+ )
+ .await??;
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_notification_message("turn/started"),
+ )
+ .await??;
+
+ let resume_id = primary
+ .send_thread_resume_request(ThreadResumeParams {
+ thread_id: thread.id,
+ path: Some(PathBuf::from("/tmp/does-not-match-running-rollout.jsonl")),
+ ..Default::default()
+ })
+ .await?;
+ let resume_err: JSONRPCError = timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_error_message(RequestId::Integer(resume_id)),
+ )
+ .await??;
+ assert!(
+ resume_err.error.message.contains("mismatched path"),
+ "unexpected resume error: {}",
+ resume_err.error.message
+ );
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn thread_resume_rejoins_running_thread_even_with_override_mismatch() -> Result<()> {
+ let server = responses::start_mock_server().await;
+ let first_body = responses::sse(vec![
+ responses::ev_response_created("resp-1"),
+ responses::ev_assistant_message("msg-1", "Done"),
+ responses::ev_completed("resp-1"),
+ ]);
+ let second_body = responses::sse(vec![responses::ev_response_created("resp-2")]);
+ let _response_mock =
+ responses::mount_sse_sequence(&server, vec![first_body, second_body]).await;
+ let codex_home = TempDir::new()?;
+ create_config_toml(codex_home.path(), &server.uri())?;
+
+ let mut primary = McpProcess::new(codex_home.path()).await?;
+ timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??;
+
+ let start_id = primary
+ .send_thread_start_request(ThreadStartParams {
+ model: Some("gpt-5.1-codex-max".to_string()),
+ ..Default::default()
+ })
+ .await?;
+ let start_resp: JSONRPCResponse = timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_response_message(RequestId::Integer(start_id)),
+ )
+ .await??;
+ let ThreadStartResponse { thread, .. } = to_response::(start_resp)?;
+
+ let seed_turn_id = primary
+ .send_turn_start_request(TurnStartParams {
+ thread_id: thread.id.clone(),
+ input: vec![UserInput::Text {
+ text: "seed history".to_string(),
+ text_elements: Vec::new(),
+ }],
+ ..Default::default()
+ })
+ .await?;
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_response_message(RequestId::Integer(seed_turn_id)),
+ )
+ .await??;
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_notification_message("turn/completed"),
+ )
+ .await??;
+ primary.clear_message_buffer();
+
+ let running_turn_id = primary
+ .send_turn_start_request(TurnStartParams {
+ thread_id: thread.id.clone(),
+ input: vec![UserInput::Text {
+ text: "keep running".to_string(),
+ text_elements: Vec::new(),
+ }],
+ ..Default::default()
+ })
+ .await?;
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_response_message(RequestId::Integer(running_turn_id)),
+ )
+ .await??;
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_notification_message("turn/started"),
+ )
+ .await??;
+
+ let resume_id = primary
+ .send_thread_resume_request(ThreadResumeParams {
+ thread_id: thread.id.clone(),
+ model: Some("not-the-running-model".to_string()),
+ cwd: Some("/tmp".to_string()),
+ ..Default::default()
+ })
+ .await?;
+ let resume_resp: JSONRPCResponse = timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_response_message(RequestId::Integer(resume_id)),
+ )
+ .await??;
+ let ThreadResumeResponse { model, .. } = to_response::(resume_resp)?;
+ assert_eq!(model, "gpt-5.1-codex-max");
+
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ primary.read_stream_until_notification_message("turn/completed"),
+ )
+ .await??;
+
+ Ok(())
+}
+
#[tokio::test]
async fn thread_resume_with_overrides_defers_updated_at_until_turn_start() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
- let rollout = setup_rollout_fixture(codex_home.path(), &server.uri())?;
+ create_config_toml(codex_home.path(), &server.uri())?;
- let mut mcp = McpProcess::new(codex_home.path()).await?;
- timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
+ let RestartedThreadFixture {
+ mut mcp,
+ thread_id,
+ rollout_file_path,
+ } = start_materialized_thread_and_restart(codex_home.path(), "materialize").await?;
+ let expected_updated_at_rfc3339 = "2025-01-07T00:00:00Z";
+ set_rollout_mtime(rollout_file_path.as_path(), expected_updated_at_rfc3339)?;
+ let before_modified = std::fs::metadata(&rollout_file_path)?.modified()?;
+ let expected_updated_at = chrono::DateTime::parse_from_rfc3339(expected_updated_at_rfc3339)?
+ .with_timezone(&Utc)
+ .timestamp();
let resume_id = mcp
.send_thread_resume_request(ThreadResumeParams {
- thread_id: rollout.conversation_id.clone(),
+ thread_id,
model: Some("mock-model".to_string()),
..Default::default()
})
@@ -230,16 +624,19 @@ async fn thread_resume_with_overrides_defers_updated_at_until_turn_start() -> Re
mcp.read_stream_until_response_message(RequestId::Integer(resume_id)),
)
.await??;
- let ThreadResumeResponse { thread, .. } = to_response::(resume_resp)?;
+ let ThreadResumeResponse {
+ thread: resumed_thread,
+ ..
+ } = to_response::(resume_resp)?;
- assert_eq!(thread.updated_at, rollout.expected_updated_at);
+ assert_eq!(resumed_thread.updated_at, expected_updated_at);
- let after_resume_modified = std::fs::metadata(&rollout.rollout_file_path)?.modified()?;
- assert_eq!(after_resume_modified, rollout.before_modified);
+ let after_resume_modified = std::fs::metadata(&rollout_file_path)?.modified()?;
+ assert_eq!(after_resume_modified, before_modified);
let turn_id = mcp
.send_turn_start_request(TurnStartParams {
- thread_id: rollout.conversation_id,
+ thread_id: resumed_thread.id,
input: vec![UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
@@ -258,8 +655,8 @@ async fn thread_resume_with_overrides_defers_updated_at_until_turn_start() -> Re
)
.await??;
- let after_turn_modified = std::fs::metadata(&rollout.rollout_file_path)?.modified()?;
- assert!(after_turn_modified > rollout.before_modified);
+ let after_turn_modified = std::fs::metadata(&rollout_file_path)?.modified()?;
+ assert!(after_turn_modified > before_modified);
Ok(())
}
@@ -374,22 +771,9 @@ async fn thread_resume_supports_history_and_overrides() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
- let mut mcp = McpProcess::new(codex_home.path()).await?;
- timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
-
- // Start a thread.
- let start_id = mcp
- .send_thread_start_request(ThreadStartParams {
- model: Some("gpt-5.1-codex-max".to_string()),
- ..Default::default()
- })
- .await?;
- let start_resp: JSONRPCResponse = timeout(
- DEFAULT_READ_TIMEOUT,
- mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
- )
- .await??;
- let ThreadStartResponse { thread, .. } = to_response::(start_resp)?;
+ let RestartedThreadFixture {
+ mut mcp, thread_id, ..
+ } = start_materialized_thread_and_restart(codex_home.path(), "seed history").await?;
let history_text = "Hello from history";
let history = vec![ResponseItem::Message {
@@ -405,7 +789,7 @@ async fn thread_resume_supports_history_and_overrides() -> Result<()> {
// Resume with explicit history and override the model.
let resume_id = mcp
.send_thread_resume_request(ThreadResumeParams {
- thread_id: thread.id,
+ thread_id,
history: Some(history),
model: Some("mock-model".to_string()),
model_provider: Some("mock_provider".to_string()),
@@ -429,6 +813,70 @@ async fn thread_resume_supports_history_and_overrides() -> Result<()> {
Ok(())
}
+struct RestartedThreadFixture {
+ mcp: McpProcess,
+ thread_id: String,
+ rollout_file_path: PathBuf,
+}
+
+async fn start_materialized_thread_and_restart(
+ codex_home: &Path,
+ seed_text: &str,
+) -> Result {
+ let mut first_mcp = McpProcess::new(codex_home).await?;
+ timeout(DEFAULT_READ_TIMEOUT, first_mcp.initialize()).await??;
+
+ let start_id = first_mcp
+ .send_thread_start_request(ThreadStartParams {
+ model: Some("gpt-5.1-codex-max".to_string()),
+ ..Default::default()
+ })
+ .await?;
+ let start_resp: JSONRPCResponse = timeout(
+ DEFAULT_READ_TIMEOUT,
+ first_mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
+ )
+ .await??;
+ let ThreadStartResponse { thread, .. } = to_response::(start_resp)?;
+
+ let materialize_turn_id = first_mcp
+ .send_turn_start_request(TurnStartParams {
+ thread_id: thread.id.clone(),
+ input: vec![UserInput::Text {
+ text: seed_text.to_string(),
+ text_elements: Vec::new(),
+ }],
+ ..Default::default()
+ })
+ .await?;
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ first_mcp.read_stream_until_response_message(RequestId::Integer(materialize_turn_id)),
+ )
+ .await??;
+ timeout(
+ DEFAULT_READ_TIMEOUT,
+ first_mcp.read_stream_until_notification_message("turn/completed"),
+ )
+ .await??;
+
+ let thread_id = thread.id;
+ let rollout_file_path = thread
+ .path
+ .ok_or_else(|| anyhow::anyhow!("thread path missing from thread/start response"))?;
+
+ drop(first_mcp);
+
+ let mut second_mcp = McpProcess::new(codex_home).await?;
+ timeout(DEFAULT_READ_TIMEOUT, second_mcp.initialize()).await??;
+
+ Ok(RestartedThreadFixture {
+ mcp: second_mcp,
+ thread_id,
+ rollout_file_path,
+ })
+}
+
#[tokio::test]
async fn thread_resume_accepts_personality_override() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -449,10 +897,10 @@ async fn thread_resume_accepts_personality_override() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
- let mut mcp = McpProcess::new(codex_home.path()).await?;
- timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
+ let mut primary = McpProcess::new(codex_home.path()).await?;
+ timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??;
- let start_id = mcp
+ let start_id = primary
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.2-codex".to_string()),
..Default::default()
@@ -460,12 +908,12 @@ async fn thread_resume_accepts_personality_override() -> Result<()> {
.await?;
let start_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
- mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
+ primary.read_stream_until_response_message(RequestId::Integer(start_id)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::(start_resp)?;
- let materialize_id = mcp
+ let materialize_id = primary
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
input: vec![UserInput::Text {
@@ -477,16 +925,19 @@ async fn thread_resume_accepts_personality_override() -> Result<()> {
.await?;
timeout(
DEFAULT_READ_TIMEOUT,
- mcp.read_stream_until_response_message(RequestId::Integer(materialize_id)),
+ primary.read_stream_until_response_message(RequestId::Integer(materialize_id)),
)
.await??;
timeout(
DEFAULT_READ_TIMEOUT,
- mcp.read_stream_until_notification_message("turn/completed"),
+ primary.read_stream_until_notification_message("turn/completed"),
)
.await??;
- let resume_id = mcp
+ let mut secondary = McpProcess::new(codex_home.path()).await?;
+ timeout(DEFAULT_READ_TIMEOUT, secondary.initialize()).await??;
+
+ let resume_id = secondary
.send_thread_resume_request(ThreadResumeParams {
thread_id: thread.id,
model: Some("gpt-5.2-codex".to_string()),
@@ -496,12 +947,12 @@ async fn thread_resume_accepts_personality_override() -> Result<()> {
.await?;
let resume_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
- mcp.read_stream_until_response_message(RequestId::Integer(resume_id)),
+ secondary.read_stream_until_response_message(RequestId::Integer(resume_id)),
)
.await??;
let resume: ThreadResumeResponse = to_response::(resume_resp)?;
- let turn_id = mcp
+ let turn_id = secondary
.send_turn_start_request(TurnStartParams {
thread_id: resume.thread.id,
input: vec![UserInput::Text {
@@ -513,13 +964,13 @@ async fn thread_resume_accepts_personality_override() -> Result<()> {
.await?;
timeout(
DEFAULT_READ_TIMEOUT,
- mcp.read_stream_until_response_message(RequestId::Integer(turn_id)),
+ secondary.read_stream_until_response_message(RequestId::Integer(turn_id)),
)
.await??;
timeout(
DEFAULT_READ_TIMEOUT,
- mcp.read_stream_until_notification_message("turn/completed"),
+ secondary.read_stream_until_notification_message("turn/completed"),
)
.await??;
@@ -557,7 +1008,6 @@ sandbox_mode = "read-only"
model_provider = "mock_provider"
[features]
-remote_models = false
personality = true
[model_providers.mock_provider]
@@ -587,7 +1037,6 @@ sandbox_mode = "read-only"
model_provider = "mock_provider"
[features]
-remote_models = false
personality = true
[model_providers.mock_provider]
diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs
index 97cb14f445..2eecfc5590 100644
--- a/codex-rs/app-server/tests/suite/v2/turn_start.rs
+++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs
@@ -1871,7 +1871,7 @@ fn create_config_toml_with_sandbox(
feature_flags: &BTreeMap,
sandbox_mode: &str,
) -> std::io::Result<()> {
- let mut features = BTreeMap::from([(Feature::RemoteModels, false)]);
+ let mut features = BTreeMap::new();
for (feature, enabled) in feature_flags {
features.insert(*feature, *enabled);
}
diff --git a/codex-rs/chatgpt/src/connectors.rs b/codex-rs/chatgpt/src/connectors.rs
index bafb0273a4..8955fbf84a 100644
--- a/codex-rs/chatgpt/src/connectors.rs
+++ b/codex-rs/chatgpt/src/connectors.rs
@@ -14,7 +14,9 @@ use crate::chatgpt_client::chatgpt_get_request_with_timeout;
use crate::chatgpt_token::get_chatgpt_token_data;
use crate::chatgpt_token::init_chatgpt_token_from_auth;
+use codex_core::connectors::AppBranding;
pub use codex_core::connectors::AppInfo;
+use codex_core::connectors::AppMetadata;
use codex_core::connectors::CONNECTORS_CACHE_TTL;
pub use codex_core::connectors::connector_display_label;
use codex_core::connectors::connector_install_url;
@@ -36,6 +38,10 @@ struct DirectoryApp {
id: String,
name: String,
description: Option,
+ #[serde(alias = "appMetadata")]
+ app_metadata: Option,
+ branding: Option,
+ labels: Option>,
#[serde(alias = "logoUrl")]
logo_url: Option,
#[serde(alias = "logoUrlDark")]
@@ -269,6 +275,9 @@ fn merge_directory_app(existing: &mut DirectoryApp, incoming: DirectoryApp) {
id: _,
name,
description,
+ app_metadata,
+ branding,
+ labels,
logo_url,
logo_url_dark,
distribution_channel,
@@ -302,6 +311,108 @@ fn merge_directory_app(existing: &mut DirectoryApp, incoming: DirectoryApp) {
if existing.distribution_channel.is_none() && distribution_channel.is_some() {
existing.distribution_channel = distribution_channel;
}
+
+ if let Some(incoming_branding) = branding {
+ if let Some(existing_branding) = existing.branding.as_mut() {
+ if existing_branding.category.is_none() && incoming_branding.category.is_some() {
+ existing_branding.category = incoming_branding.category;
+ }
+ if existing_branding.developer.is_none() && incoming_branding.developer.is_some() {
+ existing_branding.developer = incoming_branding.developer;
+ }
+ if existing_branding.website.is_none() && incoming_branding.website.is_some() {
+ existing_branding.website = incoming_branding.website;
+ }
+ if existing_branding.privacy_policy.is_none()
+ && incoming_branding.privacy_policy.is_some()
+ {
+ existing_branding.privacy_policy = incoming_branding.privacy_policy;
+ }
+ if existing_branding.terms_of_service.is_none()
+ && incoming_branding.terms_of_service.is_some()
+ {
+ existing_branding.terms_of_service = incoming_branding.terms_of_service;
+ }
+ if !existing_branding.is_discoverable_app && incoming_branding.is_discoverable_app {
+ existing_branding.is_discoverable_app = true;
+ }
+ } else {
+ existing.branding = Some(incoming_branding);
+ }
+ }
+
+ if let Some(incoming_app_metadata) = app_metadata {
+ if let Some(existing_app_metadata) = existing.app_metadata.as_mut() {
+ if existing_app_metadata.review.is_none() && incoming_app_metadata.review.is_some() {
+ existing_app_metadata.review = incoming_app_metadata.review;
+ }
+ if existing_app_metadata.categories.is_none()
+ && incoming_app_metadata.categories.is_some()
+ {
+ existing_app_metadata.categories = incoming_app_metadata.categories;
+ }
+ if existing_app_metadata.sub_categories.is_none()
+ && incoming_app_metadata.sub_categories.is_some()
+ {
+ existing_app_metadata.sub_categories = incoming_app_metadata.sub_categories;
+ }
+ if existing_app_metadata.seo_description.is_none()
+ && incoming_app_metadata.seo_description.is_some()
+ {
+ existing_app_metadata.seo_description = incoming_app_metadata.seo_description;
+ }
+ if existing_app_metadata.screenshots.is_none()
+ && incoming_app_metadata.screenshots.is_some()
+ {
+ existing_app_metadata.screenshots = incoming_app_metadata.screenshots;
+ }
+ if existing_app_metadata.developer.is_none()
+ && incoming_app_metadata.developer.is_some()
+ {
+ existing_app_metadata.developer = incoming_app_metadata.developer;
+ }
+ if existing_app_metadata.version.is_none() && incoming_app_metadata.version.is_some() {
+ existing_app_metadata.version = incoming_app_metadata.version;
+ }
+ if existing_app_metadata.version_id.is_none()
+ && incoming_app_metadata.version_id.is_some()
+ {
+ existing_app_metadata.version_id = incoming_app_metadata.version_id;
+ }
+ if existing_app_metadata.version_notes.is_none()
+ && incoming_app_metadata.version_notes.is_some()
+ {
+ existing_app_metadata.version_notes = incoming_app_metadata.version_notes;
+ }
+ if existing_app_metadata.first_party_type.is_none()
+ && incoming_app_metadata.first_party_type.is_some()
+ {
+ existing_app_metadata.first_party_type = incoming_app_metadata.first_party_type;
+ }
+ if existing_app_metadata.first_party_requires_install.is_none()
+ && incoming_app_metadata.first_party_requires_install.is_some()
+ {
+ existing_app_metadata.first_party_requires_install =
+ incoming_app_metadata.first_party_requires_install;
+ }
+ if existing_app_metadata
+ .show_in_composer_when_unlinked
+ .is_none()
+ && incoming_app_metadata
+ .show_in_composer_when_unlinked
+ .is_some()
+ {
+ existing_app_metadata.show_in_composer_when_unlinked =
+ incoming_app_metadata.show_in_composer_when_unlinked;
+ }
+ } else {
+ existing.app_metadata = Some(incoming_app_metadata);
+ }
+ }
+
+ if existing.labels.is_none() && labels.is_some() {
+ existing.labels = labels;
+ }
}
fn is_hidden_directory_app(app: &DirectoryApp) -> bool {
@@ -316,6 +427,9 @@ fn directory_app_to_app_info(app: DirectoryApp) -> AppInfo {
logo_url: app.logo_url,
logo_url_dark: app.logo_url_dark,
distribution_channel: app.distribution_channel,
+ branding: app.branding,
+ app_metadata: app.app_metadata,
+ labels: app.labels,
install_url: None,
is_accessible: false,
is_enabled: true,
@@ -342,6 +456,8 @@ const DISALLOWED_CONNECTOR_IDS: &[&str] = &[
"asdk_app_6938a94a61d881918ef32cb999ff937c",
"connector_2b0a9009c9c64bf9933a3dae3f2b1254",
"connector_68de829bf7648191acd70a907364c67c",
+ "connector_68e004f14af881919eb50893d3d9f523",
+ "connector_69272cb413a081919685ec3c88d1744e",
];
const DISALLOWED_CONNECTOR_PREFIX: &str = "connector_openai_";
@@ -375,6 +491,9 @@ mod tests {
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: None,
is_accessible: false,
is_enabled: true,
@@ -429,6 +548,9 @@ mod tests {
logo_url: None,
logo_url_dark: None,
distribution_channel: None,
+ branding: None,
+ app_metadata: None,
+ labels: None,
install_url: Some(connector_install_url(id, id)),
is_accessible,
is_enabled: true,
diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs
index c19d58189f..a65fa486e4 100644
--- a/codex-rs/cli/src/debug_sandbox.rs
+++ b/codex-rs/cli/src/debug_sandbox.rs
@@ -213,12 +213,19 @@ async fn run_command_under_sandbox(
#[cfg(not(target_os = "macos"))]
let _ = log_denials;
+ let managed_network_requirements_enabled = config.managed_network_requirements_enabled();
+
// This proxy should only live for the lifetime of the child process.
let network_proxy = match config.permissions.network.as_ref() {
Some(spec) => Some(
- spec.start_proxy()
- .await
- .map_err(|err| anyhow::anyhow!("failed to start managed network proxy: {err}"))?,
+ spec.start_proxy(
+ config.permissions.sandbox_policy.get(),
+ None,
+ None,
+ managed_network_requirements_enabled,
+ )
+ .await
+ .map_err(|err| anyhow::anyhow!("failed to start managed network proxy: {err}"))?,
),
None => None,
};
diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs
index 494f8d8935..cae322c77e 100644
--- a/codex-rs/cli/src/main.rs
+++ b/codex-rs/cli/src/main.rs
@@ -93,10 +93,10 @@ enum Subcommand {
/// Remove stored authentication credentials.
Logout(LogoutCommand),
- /// [experimental] Run Codex as an MCP server and manage MCP servers.
+ /// Manage external MCP servers for Codex.
Mcp(McpCli),
- /// [experimental] Run the Codex MCP server (stdio transport).
+ /// Start Codex as an MCP server (stdio).
McpServer,
/// [experimental] Run the app server or related tooling.
diff --git a/codex-rs/codex-api/src/common.rs b/codex-rs/codex-api/src/common.rs
index af08b6c977..3ef96c185b 100644
--- a/codex-rs/codex-api/src/common.rs
+++ b/codex-rs/codex-api/src/common.rs
@@ -56,6 +56,9 @@ pub enum ResponseEvent {
Created,
OutputItemDone(ResponseItem),
OutputItemAdded(ResponseItem),
+ /// Emitted when the server includes `OpenAI-Model` on the stream response.
+ /// This can differ from the requested model when backend safety routing applies.
+ ServerModel(String),
/// Emitted when `X-Reasoning-Included: true` is present on the response,
/// meaning the server already accounted for past reasoning tokens and the
/// client should not re-estimate them.
diff --git a/codex-rs/codex-api/src/endpoint/aggregate.rs b/codex-rs/codex-api/src/endpoint/aggregate.rs
index a91eec90a3..6d8e785c82 100644
--- a/codex-rs/codex-api/src/endpoint/aggregate.rs
+++ b/codex-rs/codex-api/src/endpoint/aggregate.rs
@@ -63,6 +63,9 @@ impl Stream for AggregatedStream {
Poll::Ready(Some(Ok(ResponseEvent::ModelsEtag(etag)))) => {
return Poll::Ready(Some(Ok(ResponseEvent::ModelsEtag(etag))));
}
+ Poll::Ready(Some(Ok(ResponseEvent::ServerModel(model)))) => {
+ return Poll::Ready(Some(Ok(ResponseEvent::ServerModel(model))));
+ }
Poll::Ready(Some(Ok(ResponseEvent::Completed {
response_id,
token_usage,
diff --git a/codex-rs/codex-api/src/endpoint/responses_websocket.rs b/codex-rs/codex-api/src/endpoint/responses_websocket.rs
index 6ebf5ab65e..aa559e9836 100644
--- a/codex-rs/codex-api/src/endpoint/responses_websocket.rs
+++ b/codex-rs/codex-api/src/endpoint/responses_websocket.rs
@@ -163,6 +163,7 @@ impl Drop for WsStream {
const X_CODEX_TURN_STATE_HEADER: &str = "x-codex-turn-state";
const X_MODELS_ETAG_HEADER: &str = "x-models-etag";
const X_REASONING_INCLUDED_HEADER: &str = "x-reasoning-included";
+const OPENAI_MODEL_HEADER: &str = "openai-model";
pub struct ResponsesWebsocketConnection {
stream: Arc>>,
@@ -170,6 +171,7 @@ pub struct ResponsesWebsocketConnection {
idle_timeout: Duration,
server_reasoning_included: bool,
models_etag: Option,
+ server_model: Option,
telemetry: Option>,
}
@@ -179,6 +181,7 @@ impl ResponsesWebsocketConnection {
idle_timeout: Duration,
server_reasoning_included: bool,
models_etag: Option,
+ server_model: Option,
telemetry: Option>,
) -> Self {
Self {
@@ -186,6 +189,7 @@ impl ResponsesWebsocketConnection {
idle_timeout,
server_reasoning_included,
models_etag,
+ server_model,
telemetry,
}
}
@@ -204,12 +208,16 @@ impl ResponsesWebsocketConnection {
let idle_timeout = self.idle_timeout;
let server_reasoning_included = self.server_reasoning_included;
let models_etag = self.models_etag.clone();
+ let server_model = self.server_model.clone();
let telemetry = self.telemetry.clone();
let request_body = serde_json::to_value(&request).map_err(|err| {
ApiError::Stream(format!("failed to encode websocket request: {err}"))
})?;
tokio::spawn(async move {
+ if let Some(model) = server_model {
+ let _ = tx_event.send(Ok(ResponseEvent::ServerModel(model))).await;
+ }
if let Some(etag) = models_etag {
let _ = tx_event.send(Ok(ResponseEvent::ModelsEtag(etag))).await;
}
@@ -273,13 +281,14 @@ impl ResponsesWebsocketClient {
merge_request_headers(&self.provider.headers, extra_headers, default_headers);
add_auth_headers_to_header_map(&self.auth, &mut headers);
- let (stream, server_reasoning_included, models_etag) =
+ let (stream, server_reasoning_included, models_etag, server_model) =
connect_websocket(ws_url, headers, turn_state.clone()).await?;
Ok(ResponsesWebsocketConnection::new(
stream,
self.provider.stream_idle_timeout,
server_reasoning_included,
models_etag,
+ server_model,
telemetry,
))
}
@@ -304,7 +313,7 @@ async fn connect_websocket(
url: Url,
headers: HeaderMap,
turn_state: Option>>,
-) -> Result<(WsStream, bool, Option), ApiError> {
+) -> Result<(WsStream, bool, Option, Option), ApiError> {
ensure_rustls_crypto_provider();
info!("connecting to websocket: {url}");
@@ -341,6 +350,11 @@ async fn connect_websocket(
.get(X_MODELS_ETAG_HEADER)
.and_then(|value| value.to_str().ok())
.map(ToString::to_string);
+ let server_model = response
+ .headers()
+ .get(OPENAI_MODEL_HEADER)
+ .and_then(|value| value.to_str().ok())
+ .map(ToString::to_string);
if let Some(turn_state) = turn_state
&& let Some(header_value) = response
.headers()
@@ -349,7 +363,12 @@ async fn connect_websocket(
{
let _ = turn_state.set(header_value.to_string());
}
- Ok((WsStream::new(stream), reasoning_included, models_etag))
+ Ok((
+ WsStream::new(stream),
+ reasoning_included,
+ models_etag,
+ server_model,
+ ))
}
fn websocket_config() -> WebSocketConfig {
@@ -469,6 +488,7 @@ async fn run_websocket_response_stream(
idle_timeout: Duration,
telemetry: Option>,
) -> Result<(), ApiError> {
+ let mut last_server_model: Option = None;
let request_text = match serde_json::to_string(&request_body) {
Ok(text) => text,
Err(err) => {
@@ -536,6 +556,14 @@ async fn run_websocket_response_stream(
}
continue;
}
+ if let Some(model) = event.response_model()
+ && last_server_model.as_deref() != Some(model.as_str())
+ {
+ let _ = tx_event
+ .send(Ok(ResponseEvent::ServerModel(model.clone())))
+ .await;
+ last_server_model = Some(model);
+ }
match process_responses_event(event) {
Ok(Some(event)) => {
let is_completed = matches!(event, ResponseEvent::Completed { .. });
diff --git a/codex-rs/codex-api/src/sse/responses.rs b/codex-rs/codex-api/src/sse/responses.rs
index 11e4f9de34..75c79ba5c1 100644
--- a/codex-rs/codex-api/src/sse/responses.rs
+++ b/codex-rs/codex-api/src/sse/responses.rs
@@ -26,6 +26,7 @@ use tracing::debug;
use tracing::trace;
const X_REASONING_INCLUDED_HEADER: &str = "x-reasoning-included";
+const OPENAI_MODEL_HEADER: &str = "openai-model";
/// Streams SSE events from an on-disk fixture for tests.
pub fn stream_from_fixture(
@@ -60,6 +61,11 @@ pub fn spawn_response_stream(
.get("X-Models-Etag")
.and_then(|v| v.to_str().ok())
.map(ToString::to_string);
+ let server_model = stream_response
+ .headers
+ .get(OPENAI_MODEL_HEADER)
+ .and_then(|v| v.to_str().ok())
+ .map(ToString::to_string);
let reasoning_included = stream_response
.headers
.get(X_REASONING_INCLUDED_HEADER)
@@ -74,6 +80,9 @@ pub fn spawn_response_stream(
}
let (tx_event, rx_event) = mpsc::channel::>(1600);
tokio::spawn(async move {
+ if let Some(model) = server_model {
+ let _ = tx_event.send(Ok(ResponseEvent::ServerModel(model))).await;
+ }
for snapshot in rate_limit_snapshots {
let _ = tx_event.send(Ok(ResponseEvent::RateLimits(snapshot))).await;
}
@@ -169,6 +178,41 @@ impl ResponsesStreamEvent {
pub fn kind(&self) -> &str {
&self.kind
}
+
+ pub fn response_model(&self) -> Option {
+ self.response.as_ref().and_then(extract_server_model)
+ }
+}
+
+fn extract_server_model(value: &Value) -> Option {
+ value
+ .get("model")
+ .and_then(json_value_as_string)
+ .or_else(|| {
+ value
+ .get("headers")
+ .and_then(header_openai_model_value_from_json)
+ })
+}
+
+fn header_openai_model_value_from_json(value: &Value) -> Option {
+ let headers = value.as_object()?;
+ headers.iter().find_map(|(name, value)| {
+ if name.eq_ignore_ascii_case("openai-model") || name.eq_ignore_ascii_case("x-openai-model")
+ {
+ json_value_as_string(value)
+ } else {
+ None
+ }
+ })
+}
+
+fn json_value_as_string(value: &Value) -> Option {
+ match value {
+ Value::String(value) => Some(value.clone()),
+ Value::Array(items) => items.first().and_then(json_value_as_string),
+ _ => None,
+ }
}
#[derive(Debug)]
@@ -339,6 +383,7 @@ pub async fn process_sse(
) {
let mut stream = stream.eventsource();
let mut response_error: Option = None;
+ let mut last_server_model: Option = None;
loop {
let start = Instant::now();
@@ -378,6 +423,19 @@ pub async fn process_sse(
}
};
+ if let Some(model) = event.response_model()
+ && last_server_model.as_deref() != Some(model.as_str())
+ {
+ if tx_event
+ .send(Ok(ResponseEvent::ServerModel(model.clone())))
+ .await
+ .is_err()
+ {
+ return;
+ }
+ last_server_model = Some(model);
+ }
+
match process_responses_event(event) {
Ok(Some(event)) => {
let is_completed = matches!(event, ResponseEvent::Completed { .. });
@@ -456,9 +514,13 @@ mod tests {
use super::*;
use assert_matches::assert_matches;
use bytes::Bytes;
+ use codex_client::StreamResponse;
use codex_protocol::models::MessagePhase;
use codex_protocol::models::ResponseItem;
use futures::stream;
+ use http::HeaderMap;
+ use http::HeaderValue;
+ use http::StatusCode;
use pretty_assertions::assert_eq;
use serde_json::json;
use tokio::sync::mpsc;
@@ -870,6 +932,149 @@ mod tests {
}
}
+ #[tokio::test]
+ async fn spawn_response_stream_emits_server_model_header() {
+ let mut headers = HeaderMap::new();
+ headers.insert(
+ OPENAI_MODEL_HEADER,
+ HeaderValue::from_static(CYBER_RESTRICTED_MODEL_FOR_TESTS),
+ );
+ let bytes = stream::iter(Vec::>::new());
+ let stream_response = StreamResponse {
+ status: StatusCode::OK,
+ headers,
+ bytes: Box::pin(bytes),
+ };
+
+ let mut stream = spawn_response_stream(stream_response, idle_timeout(), None, None);
+ let event = stream
+ .rx_event
+ .recv()
+ .await
+ .expect("expected server model event")
+ .expect("expected ok event");
+
+ match event {
+ ResponseEvent::ServerModel(model) => {
+ assert_eq!(model, CYBER_RESTRICTED_MODEL_FOR_TESTS);
+ }
+ other => panic!("expected server model event, got {other:?}"),
+ }
+ }
+
+ #[tokio::test]
+ async fn process_sse_emits_server_model_from_response_payload() {
+ let events = run_sse(vec![
+ json!({
+ "type": "response.created",
+ "response": {
+ "id": "resp-1",
+ "model": CYBER_RESTRICTED_MODEL_FOR_TESTS
+ }
+ }),
+ json!({
+ "type": "response.completed",
+ "response": {
+ "id": "resp-1",
+ "model": CYBER_RESTRICTED_MODEL_FOR_TESTS
+ }
+ }),
+ ])
+ .await;
+
+ assert_eq!(events.len(), 3);
+ assert_matches!(
+ &events[0],
+ ResponseEvent::ServerModel(model) if model == CYBER_RESTRICTED_MODEL_FOR_TESTS
+ );
+ assert_matches!(&events[1], ResponseEvent::Created);
+ assert_matches!(
+ &events[2],
+ ResponseEvent::Completed {
+ response_id,
+ token_usage: None,
+ can_append: false
+ } if response_id == "resp-1"
+ );
+ }
+
+ #[tokio::test]
+ async fn process_sse_emits_server_model_from_response_headers_payload() {
+ let events = run_sse(vec![
+ json!({
+ "type": "response.created",
+ "response": {
+ "id": "resp-1",
+ "headers": {
+ "OpenAI-Model": CYBER_RESTRICTED_MODEL_FOR_TESTS
+ }
+ }
+ }),
+ json!({
+ "type": "response.completed",
+ "response": {
+ "id": "resp-1"
+ }
+ }),
+ ])
+ .await;
+
+ assert_eq!(events.len(), 3);
+ assert_matches!(
+ &events[0],
+ ResponseEvent::ServerModel(model) if model == CYBER_RESTRICTED_MODEL_FOR_TESTS
+ );
+ assert_matches!(&events[1], ResponseEvent::Created);
+ assert_matches!(
+ &events[2],
+ ResponseEvent::Completed {
+ response_id,
+ token_usage: None,
+ can_append: false
+ } if response_id == "resp-1"
+ );
+ }
+
+ #[tokio::test]
+ async fn process_sse_emits_server_model_again_when_response_model_changes() {
+ let events = run_sse(vec![
+ json!({
+ "type": "response.created",
+ "response": {
+ "id": "resp-1",
+ "model": "gpt-5.2-codex"
+ }
+ }),
+ json!({
+ "type": "response.completed",
+ "response": {
+ "id": "resp-1",
+ "model": "gpt-5.3-codex"
+ }
+ }),
+ ])
+ .await;
+
+ assert_eq!(events.len(), 4);
+ assert_matches!(
+ &events[0],
+ ResponseEvent::ServerModel(model) if model == "gpt-5.2-codex"
+ );
+ assert_matches!(&events[1], ResponseEvent::Created);
+ assert_matches!(
+ &events[2],
+ ResponseEvent::ServerModel(model) if model == "gpt-5.3-codex"
+ );
+ assert_matches!(
+ &events[3],
+ ResponseEvent::Completed {
+ response_id,
+ token_usage: None,
+ can_append: false
+ } if response_id == "resp-1"
+ );
+ }
+
#[test]
fn test_try_parse_retry_after() {
let err = Error {
@@ -909,4 +1114,6 @@ mod tests {
let delay = try_parse_retry_after(&err);
assert_eq!(delay, Some(Duration::from_secs(35)));
}
+
+ const CYBER_RESTRICTED_MODEL_FOR_TESTS: &str = "gpt-5.3-codex";
}
diff --git a/codex-rs/codex-api/tests/models_integration.rs b/codex-rs/codex-api/tests/models_integration.rs
index b33f8b308b..9f7e380665 100644
--- a/codex-rs/codex-api/tests/models_integration.rs
+++ b/codex-rs/codex-api/tests/models_integration.rs
@@ -89,6 +89,7 @@ async fn models_client_hits_models_endpoint() {
experimental_supported_tools: Vec::new(),
input_modalities: default_input_modalities(),
prefer_websockets: false,
+ used_fallback_model_metadata: false,
}],
};
diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml
index 1b3f62c5aa..64b7330c3a 100644
--- a/codex-rs/core/Cargo.toml
+++ b/codex-rs/core/Cargo.toml
@@ -19,7 +19,7 @@ workspace = true
[dependencies]
anyhow = { workspace = true }
-arc-swap = "1.8.0"
+arc-swap = "1.8.2"
async-channel = { workspace = true }
async-trait = { workspace = true }
askama = { workspace = true }
@@ -152,6 +152,7 @@ codex-utils-cargo-bin = { workspace = true }
core_test_support = { workspace = true }
ctor = { workspace = true }
image = { workspace = true, features = ["jpeg", "png"] }
+insta = { workspace = true }
maplit = { workspace = true }
predicates = { workspace = true }
pretty_assertions = { workspace = true }
diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json
index f95958830e..9af40532a8 100644
--- a/codex-rs/core/config.schema.json
+++ b/codex-rs/core/config.schema.json
@@ -6,8 +6,28 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
- "AgentsToml": {
+ "AgentRoleToml": {
"additionalProperties": false,
+ "properties": {
+ "config_file": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/AbsolutePathBuf"
+ }
+ ],
+ "description": "Path to a role-specific config layer."
+ },
+ "description": {
+ "description": "Human-facing role documentation used in spawn tool guidance.",
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "AgentsToml": {
+ "additionalProperties": {
+ "$ref": "#/definitions/AgentRoleToml"
+ },
"properties": {
"max_threads": {
"description": "Maximum number of agent threads that can be open concurrently. When unset, no limit is enforced.",
@@ -299,6 +319,9 @@
"child_agents_md": {
"type": "boolean"
},
+ "codex_git_commit": {
+ "type": "boolean"
+ },
"collab": {
"type": "boolean"
},
@@ -338,6 +361,9 @@
"memory_tool": {
"type": "boolean"
},
+ "multi_agent": {
+ "type": "boolean"
+ },
"personality": {
"type": "boolean"
},
@@ -1390,6 +1416,10 @@
"default": null,
"description": "Preferred backend for storing CLI auth credentials. file (default): Use a file in the Codex home directory. keyring: Use an OS-specific keyring service. auto: Use the keyring if available, otherwise use a file."
},
+ "commit_attribution": {
+ "description": "Optional commit attribution text for commit message co-author trailers.\n\nSet to an empty string to disable automatic commit attribution.",
+ "type": "string"
+ },
"compact_prompt": {
"description": "Compact prompt used for history compaction.",
"type": "string"
@@ -1429,6 +1459,9 @@
"child_agents_md": {
"type": "boolean"
},
+ "codex_git_commit": {
+ "type": "boolean"
+ },
"collab": {
"type": "boolean"
},
@@ -1468,6 +1501,9 @@
"memory_tool": {
"type": "boolean"
},
+ "multi_agent": {
+ "type": "boolean"
+ },
"personality": {
"type": "boolean"
},
diff --git a/codex-rs/core/src/agent/builtins/explorer.toml b/codex-rs/core/src/agent/builtins/explorer.toml
new file mode 100644
index 0000000000..0b6def0323
--- /dev/null
+++ b/codex-rs/core/src/agent/builtins/explorer.toml
@@ -0,0 +1,2 @@
+model = "gpt-5.1-codex-mini"
+model_reasoning_effort = "medium"
diff --git a/codex-rs/core/src/agent/mod.rs b/codex-rs/core/src/agent/mod.rs
index 03652e43e5..d9a8361848 100644
--- a/codex-rs/core/src/agent/mod.rs
+++ b/codex-rs/core/src/agent/mod.rs
@@ -8,5 +8,4 @@ pub(crate) use control::AgentControl;
pub(crate) use guards::MAX_THREAD_SPAWN_DEPTH;
pub(crate) use guards::exceeds_thread_spawn_depth_limit;
pub(crate) use guards::next_thread_spawn_depth;
-pub(crate) use role::AgentRole;
pub(crate) use status::agent_status_from_event;
diff --git a/codex-rs/core/src/agent/role.rs b/codex-rs/core/src/agent/role.rs
index 1dcf8b679e..97cde2c8a3 100644
--- a/codex-rs/core/src/agent/role.rs
+++ b/codex-rs/core/src/agent/role.rs
@@ -1,99 +1,168 @@
+use crate::config::AgentRoleConfig;
use crate::config::Config;
-use crate::protocol::SandboxPolicy;
-use codex_protocol::openai_models::ReasoningEffort;
-use serde::Deserialize;
-use serde::Serialize;
+use crate::config::ConfigOverrides;
+use crate::config::deserialize_config_toml_with_base;
+use crate::config_loader::ConfigLayerEntry;
+use crate::config_loader::ConfigLayerStack;
+use crate::config_loader::ConfigLayerStackOrdering;
+use crate::config_loader::resolve_relative_paths_in_config_toml;
+use codex_app_server_protocol::ConfigLayerSource;
+use std::collections::BTreeMap;
+use std::collections::BTreeSet;
+use std::path::Path;
+use std::sync::LazyLock;
+use toml::Value as TomlValue;
-/// Base instructions for the orchestrator role.
-const ORCHESTRATOR_PROMPT: &str = include_str!("../../templates/agents/orchestrator.md");
-/// Default model override used.
-// TODO(jif) update when we have something smarter.
-const EXPLORER_MODEL: &str = "gpt-5.1-codex-mini";
+const BUILT_IN_EXPLORER_CONFIG: &str = include_str!("builtins/explorer.toml");
+const DEFAULT_ROLE_NAME: &str = "default";
+const AGENT_TYPE_UNAVAILABLE_ERROR: &str = "agent type is currently not available";
-/// Enumerated list of all supported agent roles.
-const ALL_ROLES: [AgentRole; 3] = [
- AgentRole::Default,
- AgentRole::Explorer,
- AgentRole::Worker,
- // TODO(jif) add when we have stable prompts + models
- // AgentRole::Orchestrator,
-];
+/// Applies a role config layer to a mutable config and preserves unspecified keys.
+pub(crate) async fn apply_role_to_config(
+ config: &mut Config,
+ role_name: Option<&str>,
+) -> Result<(), String> {
+ let role_name = role_name.unwrap_or(DEFAULT_ROLE_NAME);
+ let (config_file, is_built_in) = config
+ .agent_roles
+ .get(role_name)
+ .map(|role| (&role.config_file, false))
+ .or_else(|| {
+ built_in::configs()
+ .get(role_name)
+ .map(|role| (&role.config_file, true))
+ })
+ .ok_or_else(|| format!("unknown agent_type '{role_name}'"))?;
+ let Some(config_file) = config_file.as_ref() else {
+ return Ok(());
+ };
-/// Hard-coded agent role selection used when spawning sub-agents.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub enum AgentRole {
- /// Inherit the parent agent's configuration unchanged.
- Default,
- /// Coordination-only agent that delegates to workers.
- Orchestrator,
- /// Task-executing agent with a fixed model override.
- Worker,
- /// Task-executing agent with a fixed model override.
- Explorer,
+ let (role_config_contents, role_config_base) = if is_built_in {
+ (
+ built_in::config_file_contents(config_file)
+ .map(str::to_owned)
+ .ok_or_else(|| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?,
+ config.codex_home.as_path(),
+ )
+ } else {
+ (
+ tokio::fs::read_to_string(config_file)
+ .await
+ .map_err(|_| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?,
+ config_file
+ .parent()
+ .ok_or_else(|| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?,
+ )
+ };
+
+ let role_config_toml: TomlValue = toml::from_str(&role_config_contents)
+ .map_err(|_| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?;
+ deserialize_config_toml_with_base(role_config_toml.clone(), role_config_base)
+ .map_err(|_| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?;
+ let role_layer_toml = resolve_relative_paths_in_config_toml(role_config_toml, role_config_base)
+ .map_err(|_| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?;
+
+ let mut layers: Vec = config
+ .config_layer_stack
+ .get_layers(ConfigLayerStackOrdering::LowestPrecedenceFirst, true)
+ .into_iter()
+ .cloned()
+ .collect();
+ let layer = ConfigLayerEntry::new(ConfigLayerSource::SessionFlags, role_layer_toml);
+ let insertion_index =
+ layers.partition_point(|existing_layer| existing_layer.name <= layer.name);
+ layers.insert(insertion_index, layer);
+
+ let config_layer_stack = ConfigLayerStack::new(
+ layers,
+ config.config_layer_stack.requirements().clone(),
+ config.config_layer_stack.requirements_toml().clone(),
+ )
+ .map_err(|_| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?;
+
+ let merged_toml = config_layer_stack.effective_config();
+ let merged_config = deserialize_config_toml_with_base(merged_toml, &config.codex_home)
+ .map_err(|_| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?;
+ let next_config = Config::load_config_with_layer_stack(
+ merged_config,
+ ConfigOverrides {
+ cwd: Some(config.cwd.clone()),
+ codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
+ js_repl_node_path: config.js_repl_node_path.clone(),
+ ..Default::default()
+ },
+ config.codex_home.clone(),
+ config_layer_stack,
+ )
+ .map_err(|_| AGENT_TYPE_UNAVAILABLE_ERROR.to_string())?;
+ *config = next_config;
+
+ Ok(())
}
-/// Immutable profile data that drives per-agent configuration overrides.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-pub struct AgentProfile {
- /// Optional base instructions override.
- pub base_instructions: Option<&'static str>,
- /// Optional model override.
- pub model: Option<&'static str>,
- /// Optional reasoning effort override.
- pub reasoning_effort: Option,
- /// Whether to force a read-only sandbox policy.
- pub read_only: bool,
- /// Description to include in the tool specs.
- pub description: &'static str,
-}
+pub(crate) mod spawn_tool_spec {
+ use super::*;
-impl AgentRole {
- /// Returns the string values used by JSON schema enums.
- pub fn enum_values() -> Vec {
- ALL_ROLES
- .iter()
- .filter_map(|role| {
- let description = role.profile().description;
- serde_json::to_string(role)
- .map(|role| {
- let description = if !description.is_empty() {
- format!(r#", "description": {description}"#)
- } else {
- String::new()
- };
- format!(r#"{{ "name": {role}{description}}}"#)
- })
- .ok()
- })
- .collect()
+ /// Builds the spawn-agent tool description text from built-in and configured roles.
+ pub(crate) fn build(user_defined_agent_roles: &BTreeMap) -> String {
+ let built_in_roles = built_in::configs();
+ build_from_configs(built_in_roles, user_defined_agent_roles)
}
- /// Returns the hard-coded profile for this role.
- pub fn profile(self) -> AgentProfile {
- match self {
- AgentRole::Default => AgentProfile::default(),
- AgentRole::Orchestrator => AgentProfile {
- base_instructions: Some(ORCHESTRATOR_PROMPT),
- ..Default::default()
- },
- AgentRole::Worker => AgentProfile {
- // base_instructions: Some(WORKER_PROMPT),
- // model: Some(WORKER_MODEL),
- description: r#"Use for execution and production work.
-Typical tasks:
-- Implement part of a feature
-- Fix tests or bugs
-- Split large refactors into independent chunks
-Rules:
-- Explicitly assign **ownership** of the task (files / responsibility).
-- Always tell workers they are **not alone in the codebase**, and they should ignore edits made by others without touching them"#,
- ..Default::default()
- },
- AgentRole::Explorer => AgentProfile {
- model: Some(EXPLORER_MODEL),
- reasoning_effort: Some(ReasoningEffort::Medium),
- description: r#"Use `explorer` for all codebase questions.
+ // This function is not inlined for testing purpose.
+ fn build_from_configs(
+ built_in_roles: &BTreeMap,
+ user_defined_roles: &BTreeMap,
+ ) -> String {
+ let mut seen = BTreeSet::new();
+ let mut formatted_roles = Vec::new();
+ for (name, declaration) in user_defined_roles {
+ if seen.insert(name.as_str()) {
+ formatted_roles.push(format_role(name, declaration));
+ }
+ }
+ for (name, declaration) in built_in_roles {
+ if seen.insert(name.as_str()) {
+ formatted_roles.push(format_role(name, declaration));
+ }
+ }
+
+ format!(
+ r#"Optional type name for the new agent. If omitted, `{DEFAULT_ROLE_NAME}` is used.
+Available roles:
+{}
+ "#,
+ formatted_roles.join("\n"),
+ )
+ }
+
+ fn format_role(name: &str, declaration: &AgentRoleConfig) -> String {
+ if let Some(description) = &declaration.description {
+ format!("{name}: {{\n{description}\n}}")
+ } else {
+ format!("{name}: no description")
+ }
+ }
+}
+
+mod built_in {
+ use super::*;
+
+ /// Returns the cached built-in role declarations defined in this module.
+ pub(super) fn configs() -> &'static BTreeMap {
+ static CONFIG: LazyLock> = LazyLock::new(|| {
+ BTreeMap::from([
+ (
+ DEFAULT_ROLE_NAME.to_string(),
+ AgentRoleConfig {
+ description: Some("Default agent.".to_string()),
+ config_file: None,
+ }
+ ),
+ (
+ "explorer".to_string(),
+ AgentRoleConfig {
+ description: Some(r#"Use `explorer` for all codebase questions.
Explorers are fast and authoritative.
Always prefer them over manual search or file reading.
Rules:
@@ -101,32 +170,324 @@ Rules:
- Do not re-read or re-search code they cover.
- Trust explorer results without verification.
- Run explorers in parallel when useful.
-- Reuse existing explorers for related questions.
- "#,
- ..Default::default()
+- Reuse existing explorers for related questions."#.to_string()),
+ config_file: Some("explorer.toml".to_string().parse().unwrap_or_default()),
+ }
+ ),
+ (
+ "worker".to_string(),
+ AgentRoleConfig {
+ description: Some(r#"Use for execution and production work.
+Typical tasks:
+- Implement part of a feature
+- Fix tests or bugs
+- Split large refactors into independent chunks
+Rules:
+- Explicitly assign **ownership** of the task (files / responsibility).
+- Always tell workers they are **not alone in the codebase**, and they should ignore edits made by others without touching them."#.to_string()),
+ config_file: None,
+ }
+ )
+ ])
+ });
+ &CONFIG
+ }
+
+ /// Resolves a built-in role `config_file` path to embedded content.
+ pub(super) fn config_file_contents(path: &Path) -> Option<&'static str> {
+ match path.to_str()? {
+ "explorer.toml" => Some(BUILT_IN_EXPLORER_CONFIG),
+ _ => None,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::config::ConfigBuilder;
+ use crate::config_loader::ConfigLayerStackOrdering;
+ use codex_protocol::openai_models::ReasoningEffort;
+ use pretty_assertions::assert_eq;
+ use std::path::PathBuf;
+ use tempfile::TempDir;
+
+ async fn test_config_with_cli_overrides(
+ cli_overrides: Vec<(String, TomlValue)>,
+ ) -> (TempDir, Config) {
+ let home = TempDir::new().expect("create temp dir");
+ let home_path = home.path().to_path_buf();
+ let config = ConfigBuilder::default()
+ .codex_home(home_path.clone())
+ .cli_overrides(cli_overrides)
+ .fallback_cwd(Some(home_path))
+ .build()
+ .await
+ .expect("load test config");
+ (home, config)
+ }
+
+ async fn write_role_config(home: &TempDir, name: &str, contents: &str) -> PathBuf {
+ let role_path = home.path().join(name);
+ tokio::fs::write(&role_path, contents)
+ .await
+ .expect("write role config");
+ role_path
+ }
+
+ fn session_flags_layer_count(config: &Config) -> usize {
+ config
+ .config_layer_stack
+ .get_layers(ConfigLayerStackOrdering::LowestPrecedenceFirst, true)
+ .into_iter()
+ .filter(|layer| layer.name == ConfigLayerSource::SessionFlags)
+ .count()
+ }
+
+ #[tokio::test]
+ async fn apply_role_defaults_to_default_and_leaves_config_unchanged() {
+ let (_home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
+ let before = config.clone();
+
+ apply_role_to_config(&mut config, None)
+ .await
+ .expect("default role should apply");
+
+ assert_eq!(before, config);
+ }
+
+ #[tokio::test]
+ async fn apply_role_returns_error_for_unknown_role() {
+ let (_home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
+
+ let err = apply_role_to_config(&mut config, Some("missing-role"))
+ .await
+ .expect_err("unknown role should fail");
+
+ assert_eq!(err, "unknown agent_type 'missing-role'");
+ }
+
+ #[tokio::test]
+ async fn apply_explorer_role_sets_model_and_adds_session_flags_layer() {
+ let (_home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
+ let before_layers = session_flags_layer_count(&config);
+
+ apply_role_to_config(&mut config, Some("explorer"))
+ .await
+ .expect("explorer role should apply");
+
+ assert_eq!(config.model.as_deref(), Some("gpt-5.1-codex-mini"));
+ assert_eq!(config.model_reasoning_effort, Some(ReasoningEffort::Medium));
+ assert_eq!(session_flags_layer_count(&config), before_layers + 1);
+ }
+
+ #[tokio::test]
+ async fn apply_role_returns_unavailable_for_missing_user_role_file() {
+ let (_home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
+ config.agent_roles.insert(
+ "custom".to_string(),
+ AgentRoleConfig {
+ description: None,
+ config_file: Some(PathBuf::from("/path/does/not/exist.toml")),
},
+ );
+
+ let err = apply_role_to_config(&mut config, Some("custom"))
+ .await
+ .expect_err("missing role file should fail");
+
+ assert_eq!(err, AGENT_TYPE_UNAVAILABLE_ERROR);
+ }
+
+ #[tokio::test]
+ async fn apply_role_returns_unavailable_for_invalid_user_role_toml() {
+ let (home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
+ let role_path = write_role_config(&home, "invalid-role.toml", "model = [").await;
+ config.agent_roles.insert(
+ "custom".to_string(),
+ AgentRoleConfig {
+ description: None,
+ config_file: Some(role_path),
+ },
+ );
+
+ let err = apply_role_to_config(&mut config, Some("custom"))
+ .await
+ .expect_err("invalid role file should fail");
+
+ assert_eq!(err, AGENT_TYPE_UNAVAILABLE_ERROR);
+ }
+
+ #[tokio::test]
+ async fn apply_role_preserves_unspecified_keys() {
+ let (home, mut config) = test_config_with_cli_overrides(vec![(
+ "model".to_string(),
+ TomlValue::String("base-model".to_string()),
+ )])
+ .await;
+ let role_path = write_role_config(
+ &home,
+ "effort-only.toml",
+ "model_reasoning_effort = \"high\"",
+ )
+ .await;
+ config.agent_roles.insert(
+ "custom".to_string(),
+ AgentRoleConfig {
+ description: None,
+ config_file: Some(role_path),
+ },
+ );
+
+ apply_role_to_config(&mut config, Some("custom"))
+ .await
+ .expect("custom role should apply");
+
+ assert_eq!(config.model.as_deref(), Some("base-model"));
+ assert_eq!(config.model_reasoning_effort, Some(ReasoningEffort::High));
+ }
+
+ #[tokio::test]
+ #[cfg(not(windows))]
+ async fn apply_role_does_not_materialize_default_sandbox_workspace_write_fields() {
+ use codex_protocol::protocol::SandboxPolicy;
+ let (home, mut config) = test_config_with_cli_overrides(vec![
+ (
+ "sandbox_mode".to_string(),
+ TomlValue::String("workspace-write".to_string()),
+ ),
+ (
+ "sandbox_workspace_write.network_access".to_string(),
+ TomlValue::Boolean(true),
+ ),
+ ])
+ .await;
+ let role_path = write_role_config(
+ &home,
+ "sandbox-role.toml",
+ r#"[sandbox_workspace_write]
+writable_roots = ["./sandbox-root"]
+"#,
+ )
+ .await;
+ config.agent_roles.insert(
+ "custom".to_string(),
+ AgentRoleConfig {
+ description: None,
+ config_file: Some(role_path),
+ },
+ );
+
+ apply_role_to_config(&mut config, Some("custom"))
+ .await
+ .expect("custom role should apply");
+
+ let role_layer = config
+ .config_layer_stack
+ .get_layers(ConfigLayerStackOrdering::LowestPrecedenceFirst, true)
+ .into_iter()
+ .rfind(|layer| layer.name == ConfigLayerSource::SessionFlags)
+ .expect("expected a session flags layer");
+ let sandbox_workspace_write = role_layer
+ .config
+ .get("sandbox_workspace_write")
+ .and_then(TomlValue::as_table)
+ .expect("role layer should include sandbox_workspace_write");
+ assert_eq!(
+ sandbox_workspace_write.contains_key("network_access"),
+ false
+ );
+ assert_eq!(
+ sandbox_workspace_write.contains_key("exclude_tmpdir_env_var"),
+ false
+ );
+ assert_eq!(
+ sandbox_workspace_write.contains_key("exclude_slash_tmp"),
+ false
+ );
+
+ match &*config.permissions.sandbox_policy {
+ SandboxPolicy::WorkspaceWrite { network_access, .. } => {
+ assert_eq!(*network_access, true);
+ }
+ other => panic!("expected workspace-write sandbox policy, got {other:?}"),
}
}
- /// Applies this role's profile onto the provided config.
- pub fn apply_to_config(self, config: &mut Config) -> Result<(), String> {
- let profile = self.profile();
- if let Some(base_instructions) = profile.base_instructions {
- config.base_instructions = Some(base_instructions.to_string());
- }
- if let Some(model) = profile.model {
- config.model = Some(model.to_string());
- }
- if let Some(reasoning_effort) = profile.reasoning_effort {
- config.model_reasoning_effort = Some(reasoning_effort)
- }
- if profile.read_only {
- config
- .permissions
- .sandbox_policy
- .set(SandboxPolicy::new_read_only_policy())
- .map_err(|err| format!("sandbox_policy is invalid: {err}"))?;
- }
- Ok(())
+ #[tokio::test]
+ async fn apply_role_takes_precedence_over_existing_session_flags_for_same_key() {
+ let (home, mut config) = test_config_with_cli_overrides(vec![(
+ "model".to_string(),
+ TomlValue::String("cli-model".to_string()),
+ )])
+ .await;
+ let before_layers = session_flags_layer_count(&config);
+ let role_path = write_role_config(&home, "model-role.toml", "model = \"role-model\"").await;
+ config.agent_roles.insert(
+ "custom".to_string(),
+ AgentRoleConfig {
+ description: None,
+ config_file: Some(role_path),
+ },
+ );
+
+ apply_role_to_config(&mut config, Some("custom"))
+ .await
+ .expect("custom role should apply");
+
+ assert_eq!(config.model.as_deref(), Some("role-model"));
+ assert_eq!(session_flags_layer_count(&config), before_layers + 1);
+ }
+
+ #[test]
+ fn spawn_tool_spec_build_deduplicates_user_defined_built_in_roles() {
+ let user_defined_roles = BTreeMap::from([
+ (
+ "explorer".to_string(),
+ AgentRoleConfig {
+ description: Some("user override".to_string()),
+ config_file: None,
+ },
+ ),
+ ("researcher".to_string(), AgentRoleConfig::default()),
+ ]);
+
+ let spec = spawn_tool_spec::build(&user_defined_roles);
+
+ assert!(spec.contains("researcher: no description"));
+ assert!(spec.contains("explorer: {\nuser override\n}"));
+ assert!(spec.contains("default: {\nDefault agent.\n}"));
+ assert!(!spec.contains("Explorers are fast and authoritative."));
+ }
+
+ #[test]
+ fn spawn_tool_spec_lists_user_defined_roles_before_built_ins() {
+ let user_defined_roles = BTreeMap::from([(
+ "aaa".to_string(),
+ AgentRoleConfig {
+ description: Some("first".to_string()),
+ config_file: None,
+ },
+ )]);
+
+ let spec = spawn_tool_spec::build(&user_defined_roles);
+ let user_index = spec.find("aaa: {\nfirst\n}").expect("find user role");
+ let built_in_index = spec
+ .find("default: {\nDefault agent.\n}")
+ .expect("find built-in role");
+
+ assert!(user_index < built_in_index);
+ }
+
+ #[test]
+ fn built_in_config_file_contents_resolves_explorer_only() {
+ assert_eq!(
+ built_in::config_file_contents(Path::new("explorer.toml")),
+ Some(BUILT_IN_EXPLORER_CONFIG)
+ );
+ assert_eq!(
+ built_in::config_file_contents(Path::new("missing.toml")),
+ None
+ );
}
}
diff --git a/codex-rs/core/src/apps/render.rs b/codex-rs/core/src/apps/render.rs
index f450eb3a3a..7f706f737e 100644
--- a/codex-rs/core/src/apps/render.rs
+++ b/codex-rs/core/src/apps/render.rs
@@ -2,6 +2,6 @@ use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
pub(crate) fn render_apps_section() -> String {
format!(
- "## Apps\nApps are mentioned in the prompt in the format `[$app-name](apps://{{connector_id}})`.\nAn app is equivalent to a set of MCP tools within the `{CODEX_APPS_MCP_SERVER_NAME}` MCP.\nWhen you see an app mention, the app's MCP tools are either already provided in `{CODEX_APPS_MCP_SERVER_NAME}`, or do not exist because the user did not install it.\nDo not additionally call list_mcp_resources for apps that are already mentioned."
+ "## Apps\nApps are mentioned in the prompt in the format `[$app-name](app://{{connector_id}})`.\nAn app is equivalent to a set of MCP tools within the `{CODEX_APPS_MCP_SERVER_NAME}` MCP.\nWhen you see an app mention, the app's MCP tools are either already provided in `{CODEX_APPS_MCP_SERVER_NAME}`, or do not exist because the user did not install it.\nDo not additionally call list_mcp_resources for apps that are already mentioned."
)
}
diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index a09238a3e7..afb9075c35 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -18,6 +18,7 @@ use crate::analytics_client::AppInvocation;
use crate::analytics_client::build_track_events_context;
use crate::app_tool_policy;
use crate::apps::render_apps_section;
+use crate::commit_attribution::commit_message_trailer_instruction;
use crate::compact;
use crate::compact::run_inline_auto_compact_task;
use crate::compact::should_use_remote_compact_task;
@@ -32,7 +33,6 @@ use crate::models_manager::manager::ModelsManager;
use crate::parse_command::parse_command;
use crate::parse_turn_item;
use crate::rollout::session_index;
-use crate::sandbox_tags::sandbox_tag;
use crate::stream_events_utils::HandleOutputCtx;
use crate::stream_events_utils::handle_non_tool_response_item;
use crate::stream_events_utils::handle_output_item_done;
@@ -46,6 +46,7 @@ use async_channel::Sender;
use codex_hooks::HookEvent;
use codex_hooks::HookEventAfterAgent;
use codex_hooks::HookPayload;
+use codex_hooks::HookResult;
use codex_hooks::Hooks;
use codex_hooks::HooksConfig;
use codex_network_proxy::NetworkProxy;
@@ -152,6 +153,7 @@ use crate::mcp::with_codex_apps_mcp;
use crate::mcp_connection_manager::McpConnectionManager;
use crate::mcp_connection_manager::filter_codex_apps_mcp_tools_only;
use crate::mcp_connection_manager::filter_mcp_tools_by_name;
+use crate::mcp_connection_manager::filter_non_codex_apps_mcp_tools_only;
use crate::memories;
use crate::mentions::build_connector_slug_counts;
use crate::mentions::build_skill_name_counts;
@@ -172,6 +174,9 @@ use crate::protocol::Event;
use crate::protocol::EventMsg;
use crate::protocol::ExecApprovalRequestEvent;
use crate::protocol::McpServerRefreshConfig;
+use crate::protocol::ModelRerouteEvent;
+use crate::protocol::ModelRerouteReason;
+use crate::protocol::NetworkApprovalContext;
use crate::protocol::Op;
use crate::protocol::PlanDeltaEvent;
use crate::protocol::RateLimitSnapshot;
@@ -224,7 +229,11 @@ use crate::tasks::SessionTask;
use crate::tasks::SessionTaskContext;
use crate::tools::ToolRouter;
use crate::tools::context::SharedTurnDiffTracker;
+use crate::tools::handlers::SEARCH_TOOL_BM25_TOOL_NAME;
use crate::tools::js_repl::JsReplHandle;
+use crate::tools::network_approval::NetworkApprovalService;
+use crate::tools::network_approval::build_blocked_request_observer;
+use crate::tools::network_approval::build_network_policy_decider;
use crate::tools::parallel::ToolCallRuntime;
use crate::tools::sandboxing::ApprovalStore;
use crate::tools::spec::ToolsConfig;
@@ -274,6 +283,8 @@ pub struct CodexSpawnOk {
pub(crate) const INITIAL_SUBMIT_ID: &str = "";
pub(crate) const SUBMISSION_CHANNEL_CAPACITY: usize = 64;
+const CYBER_VERIFY_URL: &str = "https://chatgpt.com/cyber";
+const CYBER_SAFETY_URL: &str = "https://developers.openai.com/codex/concepts/cyber-safety";
impl Codex {
/// Spawn a new [`Codex`] and initialize the session.
@@ -320,15 +331,11 @@ impl Codex {
let config = Arc::new(config);
let _ = models_manager
- .list_models(
- &config,
- crate::models_manager::manager::RefreshStrategy::OnlineIfUncached,
- )
+ .list_models(crate::models_manager::manager::RefreshStrategy::OnlineIfUncached)
.await;
let model = models_manager
.get_default_model(
&config.model,
- &config,
crate::models_manager::manager::RefreshStrategy::OnlineIfUncached,
)
.await;
@@ -521,7 +528,6 @@ pub(crate) struct Session {
js_repl: Arc,
next_internal_sub_id: AtomicU64,
}
-
/// The context needed for a single turn of the thread.
#[derive(Debug)]
pub(crate) struct TurnContext {
@@ -557,7 +563,7 @@ pub(crate) struct TurnContext {
pub(crate) truncation_policy: TruncationPolicy,
pub(crate) js_repl: Arc,
pub(crate) dynamic_tools: Vec,
- turn_metadata_state: Arc,
+ pub(crate) turn_metadata_state: Arc,
}
impl TurnContext {
pub(crate) fn model_context_window(&self) -> Option {
@@ -602,7 +608,8 @@ impl TurnContext {
model_info: &model_info,
features: &features,
web_search_mode: self.tools_config.web_search_mode,
- });
+ })
+ .with_agent_roles(config.agent_roles.clone());
Self {
sub_id: self.sub_id.clone(),
@@ -687,18 +694,6 @@ impl TurnContext {
denied_domains: network.denied_domains.clone().unwrap_or_default(),
})
}
-
- pub fn current_turn_metadata_header(&self) -> Option {
- self.turn_metadata_state.current_header_value()
- }
-
- pub fn spawn_turn_metadata_enrichment_task(self: &Arc) {
- self.turn_metadata_state.spawn_git_enrichment_task();
- }
-
- pub fn cancel_turn_metadata_enrichment_task(&self) {
- self.turn_metadata_state.cancel_git_enrichment_task();
- }
}
#[derive(Clone)]
@@ -836,6 +831,33 @@ impl Session {
}
}
+ async fn start_managed_network_proxy(
+ spec: &crate::config::NetworkProxySpec,
+ sandbox_policy: &SandboxPolicy,
+ network_policy_decider: Option>,
+ blocked_request_observer: Option>,
+ managed_network_requirements_enabled: bool,
+ ) -> anyhow::Result<(StartedNetworkProxy, SessionNetworkProxyRuntime)> {
+ let network_proxy = spec
+ .start_proxy(
+ sandbox_policy,
+ network_policy_decider,
+ blocked_request_observer,
+ managed_network_requirements_enabled,
+ )
+ .await
+ .map_err(|err| anyhow::anyhow!("failed to start managed network proxy: {err}"))?;
+ let session_network_proxy = {
+ let proxy = network_proxy.proxy();
+ SessionNetworkProxyRuntime {
+ http_addr: proxy.http_addr().to_string(),
+ socks_addr: proxy.socks_addr().to_string(),
+ admin_addr: proxy.admin_addr().to_string(),
+ }
+ };
+ Ok((network_proxy, session_network_proxy))
+ }
+
/// Don't expand the number of mutated arguments on config. We are in the process of getting rid of it.
pub(crate) fn build_per_turn_config(session_configuration: &SessionConfiguration) -> Config {
// todo(aibrahim): store this state somewhere else so we don't need to mut config
@@ -921,19 +943,18 @@ impl Session {
model_info: &model_info,
features: &per_turn_config.features,
web_search_mode: Some(per_turn_config.web_search_mode.value()),
- });
+ })
+ .with_agent_roles(per_turn_config.agent_roles.clone());
let cwd = session_configuration.cwd.clone();
let turn_metadata_state = Arc::new(TurnMetadataState::new(
sub_id.clone(),
cwd.clone(),
- Some(
- sandbox_tag(
- session_configuration.sandbox_policy.get(),
- session_configuration.windows_sandbox_level,
- )
- .to_string(),
- ),
+ session_configuration.sandbox_policy.get(),
+ session_configuration.windows_sandbox_level,
+ per_turn_config
+ .features
+ .enabled(Feature::UseLinuxSandboxBwrap),
));
TurnContext {
sub_id,
@@ -1197,21 +1218,50 @@ impl Session {
};
session_configuration.thread_name = thread_name.clone();
let mut state = SessionState::new(session_configuration.clone());
- let network_proxy =
- match config.permissions.network.as_ref() {
- Some(spec) => Some(spec.start_proxy().await.map_err(|err| {
- anyhow::anyhow!("failed to start managed network proxy: {err}")
- })?),
- None => None,
+ let managed_network_requirements_enabled = config.managed_network_requirements_enabled();
+ let network_approval = Arc::new(NetworkApprovalService::default());
+ // The managed proxy can call back into core for allowlist-miss decisions.
+ let network_policy_decider_session = if managed_network_requirements_enabled {
+ config
+ .permissions
+ .network
+ .as_ref()
+ .map(|_| Arc::new(RwLock::new(std::sync::Weak::::new())))
+ } else {
+ None
+ };
+ let blocked_request_observer = if managed_network_requirements_enabled {
+ config
+ .permissions
+ .network
+ .as_ref()
+ .map(|_| build_blocked_request_observer(Arc::clone(&network_approval)))
+ } else {
+ None
+ };
+ let network_policy_decider =
+ network_policy_decider_session
+ .as_ref()
+ .map(|network_policy_decider_session| {
+ build_network_policy_decider(
+ Arc::clone(&network_approval),
+ Arc::clone(network_policy_decider_session),
+ )
+ });
+ let (network_proxy, session_network_proxy) =
+ if let Some(spec) = config.permissions.network.as_ref() {
+ let (network_proxy, session_network_proxy) = Self::start_managed_network_proxy(
+ spec,
+ config.permissions.sandbox_policy.get(),
+ network_policy_decider.as_ref().map(Arc::clone),
+ blocked_request_observer.as_ref().map(Arc::clone),
+ managed_network_requirements_enabled,
+ )
+ .await?;
+ (Some(network_proxy), Some(session_network_proxy))
+ } else {
+ (None, None)
};
- let session_network_proxy = network_proxy.as_ref().map(|started| {
- let proxy = started.proxy();
- SessionNetworkProxyRuntime {
- http_addr: proxy.http_addr().to_string(),
- socks_addr: proxy.socks_addr().to_string(),
- admin_addr: proxy.admin_addr().to_string(),
- }
- });
let services = SessionServices {
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::default())),
@@ -1237,6 +1287,7 @@ impl Session {
file_watcher,
agent_control,
network_proxy,
+ network_approval: Arc::clone(&network_approval),
state_db: state_db_ctx.clone(),
model_client: ModelClient::new(
Some(Arc::clone(&auth_manager)),
@@ -1279,6 +1330,10 @@ impl Session {
js_repl,
next_internal_sub_id: AtomicU64::new(0),
});
+ if let Some(network_policy_decider_session) = network_policy_decider_session {
+ let mut guard = network_policy_decider_session.write().await;
+ *guard = Arc::downgrade(&sess);
+ }
// Dispatch the SessionConfiguredEvent first and then report any errors.
// If resuming, include converted initial messages in the payload so UIs can render them immediately.
@@ -1441,6 +1496,11 @@ impl Session {
state.merge_mcp_tool_selection(tool_names)
}
+ pub(crate) async fn set_mcp_tool_selection(&self, tool_names: Vec) {
+ let mut state = self.state.lock().await;
+ state.set_mcp_tool_selection(tool_names);
+ }
+
pub(crate) async fn get_mcp_tool_selection(&self) -> Option> {
let state = self.state.lock().await;
state.get_mcp_tool_selection()
@@ -1474,6 +1534,7 @@ impl Session {
async fn record_initial_history(&self, conversation_history: InitialHistory) {
let turn_context = self.new_default_turn().await;
+ self.clear_mcp_tool_selection().await;
match conversation_history {
InitialHistory::New => {
// Build and record initial items (user instructions + environment context)
@@ -1489,6 +1550,8 @@ impl Session {
}
InitialHistory::Resumed(resumed_history) => {
let rollout_items = resumed_history.history;
+ let restored_tool_selection =
+ Self::extract_mcp_tool_selection_from_rollout(&rollout_items);
let previous_model = Self::last_rollout_model_name(&rollout_items)
.map(std::string::ToString::to_string);
{
@@ -1530,12 +1593,17 @@ impl Session {
let mut state = self.state.lock().await;
state.set_token_info(Some(info));
}
+ if let Some(selected_tools) = restored_tool_selection {
+ self.set_mcp_tool_selection(selected_tools).await;
+ }
// Defer seeding the session's initial context until the first turn starts so
// turn/start overrides can be merged before we write to the rollout.
self.flush_rollout().await;
}
InitialHistory::Forked(rollout_items) => {
+ let restored_tool_selection =
+ Self::extract_mcp_tool_selection_from_rollout(&rollout_items);
let previous_model = Self::last_rollout_model_name(&rollout_items)
.map(std::string::ToString::to_string);
self.set_previous_model(previous_model).await;
@@ -1555,6 +1623,9 @@ impl Session {
let mut state = self.state.lock().await;
state.set_token_info(Some(info));
}
+ if let Some(selected_tools) = restored_tool_selection {
+ self.set_mcp_tool_selection(selected_tools).await;
+ }
// If persisting, persist all rollout items as-is (recorder filters)
if !rollout_items.is_empty() {
@@ -1596,6 +1667,54 @@ impl Session {
})
}
+ fn extract_mcp_tool_selection_from_rollout(
+ rollout_items: &[RolloutItem],
+ ) -> Option> {
+ let mut search_call_ids = HashSet::new();
+ let mut active_selected_tools: Option> = None;
+
+ for item in rollout_items {
+ let RolloutItem::ResponseItem(response_item) = item else {
+ continue;
+ };
+ match response_item {
+ ResponseItem::FunctionCall { name, call_id, .. } => {
+ if name == SEARCH_TOOL_BM25_TOOL_NAME {
+ search_call_ids.insert(call_id.clone());
+ }
+ }
+ ResponseItem::FunctionCallOutput { call_id, output } => {
+ if !search_call_ids.contains(call_id) {
+ continue;
+ }
+ let Some(content) = output.body.to_text() else {
+ continue;
+ };
+ let Ok(payload) = serde_json::from_str::(&content) else {
+ continue;
+ };
+ let Some(selected_tools) = payload
+ .get("active_selected_tools")
+ .and_then(Value::as_array)
+ else {
+ continue;
+ };
+ let Some(selected_tools) = selected_tools
+ .iter()
+ .map(|value| value.as_str().map(str::to_string))
+ .collect::