mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Merge branch 'main' into fabian/app-server-streaming-throughput
This commit is contained in:
Generated
+2
-2
@@ -1449,8 +1449,8 @@
|
||||
"resolv-conf_0.7.6": "{\"dependencies\":[],\"features\":{\"system\":[]}}",
|
||||
"rfc6979_0.4.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"reset\"],\"name\":\"hmac\",\"req\":\"^0.12\"},{\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2\"}],\"features\":{}}",
|
||||
"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_1.7.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\":{\"local\":[]}}",
|
||||
"rmcp_1.7.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"async-trait\",\"req\":\"^0.1.89\"},{\"kind\":\"dev\",\"name\":\"async-trait\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"http1\",\"tokio\"],\"kind\":\"dev\",\"name\":\"axum\",\"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\\\"))\"},{\"default_features\":false,\"features\":[\"serde\",\"now\"],\"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\"},{\"features\":[\"client\",\"http1\"],\"name\":\"hyper\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"server\",\"http1\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1\"},{\"features\":[\"tokio\"],\"name\":\"hyper-util\",\"optional\":true,\"req\":\"^0.1\"},{\"features\":[\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"name\":\"jsonwebtoken\",\"optional\":true,\"req\":\"^10\"},{\"default_features\":false,\"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.10\"},{\"default_features\":false,\"features\":[\"json\",\"stream\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.13.2\"},{\"name\":\"rmcp-macros\",\"optional\":true,\"req\":\"^1.7.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\"},{\"kind\":\"dev\",\"name\":\"tower-service\",\"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\"},{\"kind\":\"dev\",\"name\":\"url\",\"req\":\"^2.4\"},{\"features\":[\"v4\"],\"name\":\"uuid\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"which\",\"optional\":true,\"req\":\"^8\"}],\"features\":{\"__reqwest\":[\"dep:reqwest\"],\"auth\":[\"dep:oauth2\",\"__reqwest\",\"dep:url\"],\"auth-client-credentials-jwt\":[\"auth\",\"dep:jsonwebtoken\",\"uuid\"],\"client\":[\"dep:tokio-stream\"],\"client-side-sse\":[\"dep:sse-stream\",\"dep:http\"],\"default\":[\"base64\",\"macros\",\"server\"],\"elicitation\":[\"dep:url\"],\"local\":[\"rmcp-macros?/local\"],\"macros\":[\"dep:rmcp-macros\",\"dep:pastey\"],\"reqwest\":[\"__reqwest\",\"reqwest?/rustls\"],\"reqwest-native-tls\":[\"__reqwest\",\"reqwest?/native-tls\"],\"reqwest-tls-no-provider\":[\"__reqwest\",\"reqwest?/rustls-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\",\"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-client-unix-socket\":[\"transport-streamable-http-client\",\"dep:hyper\",\"dep:hyper-util\",\"dep:http-body-util\",\"dep:http\",\"dep:bytes\",\"tokio/net\"],\"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\"],\"which-command\":[\"transport-child-process\",\"dep:which\"]}}",
|
||||
"rmcp-macros_1.8.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\":{\"local\":[]}}",
|
||||
"rmcp_1.8.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"async-trait\",\"req\":\"^0.1.89\"},{\"kind\":\"dev\",\"name\":\"async-trait\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"http1\",\"tokio\"],\"kind\":\"dev\",\"name\":\"axum\",\"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\\\"))\"},{\"default_features\":false,\"features\":[\"serde\",\"now\"],\"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\"},{\"features\":[\"client\",\"http1\"],\"name\":\"hyper\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"server\",\"http1\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1\"},{\"features\":[\"tokio\"],\"name\":\"hyper-util\",\"optional\":true,\"req\":\"^0.1\"},{\"features\":[\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"name\":\"jsonwebtoken\",\"optional\":true,\"req\":\"^10\"},{\"default_features\":false,\"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.10\"},{\"default_features\":false,\"features\":[\"json\",\"stream\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.13.2\"},{\"name\":\"rmcp-macros\",\"optional\":true,\"req\":\"^1.8.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\"},{\"kind\":\"dev\",\"name\":\"tower-service\",\"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\"},{\"kind\":\"dev\",\"name\":\"url\",\"req\":\"^2.4\"},{\"features\":[\"v4\"],\"name\":\"uuid\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"which\",\"optional\":true,\"req\":\"^8\"}],\"features\":{\"__reqwest\":[\"dep:reqwest\"],\"auth\":[\"dep:oauth2\",\"__reqwest\",\"dep:url\"],\"auth-client-credentials-jwt\":[\"auth\",\"dep:jsonwebtoken\",\"uuid\"],\"client\":[\"dep:tokio-stream\"],\"client-side-sse\":[\"dep:sse-stream\",\"dep:http\"],\"default\":[\"base64\",\"macros\",\"server\"],\"elicitation\":[\"dep:url\"],\"local\":[\"rmcp-macros?/local\"],\"macros\":[\"dep:rmcp-macros\",\"dep:pastey\"],\"reqwest\":[\"__reqwest\",\"reqwest?/rustls\"],\"reqwest-native-tls\":[\"__reqwest\",\"reqwest?/native-tls\"],\"reqwest-tls-no-provider\":[\"__reqwest\",\"reqwest?/rustls-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\",\"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-client-unix-socket\":[\"transport-streamable-http-client\",\"dep:hyper\",\"dep:hyper-util\",\"dep:http-body-util\",\"dep:http\",\"dep:bytes\",\"tokio/net\"],\"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\"],\"which-command\":[\"transport-child-process\",\"dep:which\"]}}",
|
||||
"rtrb_0.3.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"crossbeam-utils\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.10\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}",
|
||||
"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\"]}}",
|
||||
|
||||
Generated
+8
-6
@@ -2888,6 +2888,7 @@ dependencies = [
|
||||
"codex-app-server-protocol",
|
||||
"codex-client",
|
||||
"codex-file-system",
|
||||
"codex-network-proxy",
|
||||
"codex-protocol",
|
||||
"codex-sandboxing",
|
||||
"codex-shell-command",
|
||||
@@ -3981,6 +3982,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"codex-git-utils",
|
||||
"codex-install-context",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-rollout",
|
||||
"codex-state",
|
||||
@@ -9451,7 +9453,7 @@ version = "5.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"getrandom 0.2.17",
|
||||
"http 1.4.0",
|
||||
@@ -10537,7 +10539,7 @@ version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"heck 0.5.0",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"multimap",
|
||||
@@ -11457,9 +11459,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rmcp"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0810a9f717d9828f475fe1f629f4c305c8464b7f496c3a854b58d29e65f4058e"
|
||||
checksum = "1d1f571c72940a19d9532fe52dbea8bc9912bf1d766c2970bb824056b86f3f59"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -11492,9 +11494,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rmcp-macros"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6aefac48c364756e97f04c0401ba3231e8607882c7c1d92da0437dc16307904d"
|
||||
checksum = "1aad0035b69380782d78ea95b508327e6deaa2235909053e596eea8f27b5e1d5"
|
||||
dependencies = [
|
||||
"darling 0.23.0",
|
||||
"proc-macro2",
|
||||
|
||||
+2
-2
@@ -368,7 +368,7 @@ rcgen = { version = "0.14.7", default-features = false, features = [
|
||||
regex = "1.12.3"
|
||||
regex-lite = "0.1.8"
|
||||
reqwest = { version = "0.12", features = ["cookies"] }
|
||||
rmcp = { version = "1.7.0", default-features = false }
|
||||
rmcp = { version = "1.8.0", default-features = false }
|
||||
runfiles = { git = "https://github.com/dzbarsky/rules_rust", rev = "b56cbaa8465e74127f1ea216f813cd377295ad81" }
|
||||
rustls = { version = "0.23", default-features = false, features = [
|
||||
"aws_lc_rs",
|
||||
@@ -380,7 +380,7 @@ schemars = "0.8.22"
|
||||
seccompiler = "0.5.0"
|
||||
semver = "1.0"
|
||||
sentry = "0.46.0"
|
||||
serde = "1"
|
||||
serde = { version = "1", features = ["rc"] }
|
||||
serde_ignored = "0.1.14"
|
||||
serde_json = "1"
|
||||
serde_path_to_error = "0.1.20"
|
||||
|
||||
@@ -177,6 +177,7 @@ fn sample_thread_with_metadata(
|
||||
) -> Thread {
|
||||
Thread {
|
||||
id: thread_id.to_string(),
|
||||
extra: None,
|
||||
session_id: format!("session-{thread_id}"),
|
||||
forked_from_id: None,
|
||||
parent_thread_id,
|
||||
@@ -219,7 +220,7 @@ fn sample_thread_start_response(
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_sources: Vec::new(),
|
||||
approval_policy: AppServerAskForApproval::OnFailure,
|
||||
approval_policy: AppServerAskForApproval::OnRequest,
|
||||
approvals_reviewer: AppServerApprovalsReviewer::User,
|
||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||
active_permission_profile: None,
|
||||
@@ -284,7 +285,7 @@ fn sample_thread_resume_response_with_source(
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_sources: Vec::new(),
|
||||
approval_policy: AppServerAskForApproval::OnFailure,
|
||||
approval_policy: AppServerAskForApproval::OnRequest,
|
||||
approvals_reviewer: AppServerApprovalsReviewer::User,
|
||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||
active_permission_profile: None,
|
||||
@@ -3026,6 +3027,7 @@ fn plugin_used_event_serializes_expected_shape() {
|
||||
"event_type": "codex_plugin_used",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
@@ -3056,6 +3058,7 @@ fn plugin_management_event_serializes_expected_shape() {
|
||||
"event_type": "codex_plugin_installed",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
@@ -3085,6 +3088,7 @@ fn plugin_install_failed_event_serializes_expected_shape() {
|
||||
"event_type": "codex_plugin_install_failed",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
@@ -3098,7 +3102,7 @@ fn plugin_install_failed_event_serializes_expected_shape() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_management_event_can_use_remote_plugin_id_override() {
|
||||
fn plugin_management_event_keeps_plugin_id_local_when_remote_id_exists() {
|
||||
let mut plugin = sample_plugin_metadata();
|
||||
plugin.remote_plugin_id = Some("plugins~Plugin_remote".to_string());
|
||||
let event = TrackEventRequest::PluginInstalled(CodexPluginEventRequest {
|
||||
@@ -3109,11 +3113,21 @@ fn plugin_management_event_can_use_remote_plugin_id_override() {
|
||||
let payload = serde_json::to_value(&event).expect("serialize plugin installed event");
|
||||
|
||||
assert_eq!(
|
||||
payload["event_params"]["plugin_id"],
|
||||
"plugins~Plugin_remote"
|
||||
payload,
|
||||
json!({
|
||||
"event_type": "codex_plugin_installed",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"remote_plugin_id": "plugins~Plugin_remote",
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
"mcp_server_count": 2,
|
||||
"connector_ids": ["calendar", "drive"],
|
||||
"product_client_id": originator().value
|
||||
}
|
||||
})
|
||||
);
|
||||
assert_eq!(payload["event_params"]["plugin_name"], "sample");
|
||||
assert_eq!(payload["event_params"]["marketplace_name"], "test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3453,6 +3467,7 @@ async fn reducer_ingests_plugin_state_changed_fact() {
|
||||
"event_type": "codex_plugin_disabled",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
@@ -3488,6 +3503,7 @@ async fn reducer_ingests_plugin_install_failed_fact() {
|
||||
"event_type": "codex_plugin_install_failed",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
@@ -3505,7 +3521,7 @@ async fn reducer_ingests_plugin_install_failed_fact_without_detail() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut events = Vec::new();
|
||||
let plugin = PluginTelemetryMetadata {
|
||||
plugin_id: PluginId::parse("unknown@openai-curated-remote").expect("valid plugin id"),
|
||||
plugin_id: None,
|
||||
remote_plugin_id: Some("plugins~Plugin_00000000000000000000000000000000".to_string()),
|
||||
capability_summary: None,
|
||||
};
|
||||
@@ -3528,9 +3544,10 @@ async fn reducer_ingests_plugin_install_failed_fact_without_detail() {
|
||||
json!([{
|
||||
"event_type": "codex_plugin_install_failed",
|
||||
"event_params": {
|
||||
"plugin_id": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"plugin_name": "unknown",
|
||||
"marketplace_name": "openai-curated-remote",
|
||||
"plugin_id": null,
|
||||
"remote_plugin_id": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"plugin_name": null,
|
||||
"marketplace_name": null,
|
||||
"has_skills": null,
|
||||
"mcp_server_count": null,
|
||||
"connector_ids": null,
|
||||
@@ -4569,7 +4586,7 @@ async fn turn_completed_without_started_notification_emits_null_started_at() {
|
||||
|
||||
fn sample_plugin_metadata() -> PluginTelemetryMetadata {
|
||||
PluginTelemetryMetadata {
|
||||
plugin_id: PluginId::parse("sample@test").expect("valid plugin id"),
|
||||
plugin_id: Some(PluginId::parse("sample@test").expect("valid plugin id")),
|
||||
remote_plugin_id: None,
|
||||
capability_summary: Some(PluginCapabilitySummary {
|
||||
config_name: "sample@test".to_string(),
|
||||
|
||||
@@ -38,6 +38,7 @@ use codex_app_server_protocol::ServerResponse;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::default_client::create_client;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_plugin::PluginTelemetryMetadata;
|
||||
use codex_protocol::request_permissions::RequestPermissionsResponse;
|
||||
use std::collections::HashSet;
|
||||
@@ -173,7 +174,15 @@ impl AnalyticsEventsQueue {
|
||||
if emitted.len() >= ANALYTICS_EVENT_DEDUPE_MAX_KEYS {
|
||||
emitted.clear();
|
||||
}
|
||||
emitted.insert((tracking.turn_id.clone(), plugin.plugin_id.as_key()))
|
||||
let Some(plugin_id) = plugin
|
||||
.plugin_id
|
||||
.as_ref()
|
||||
.map(PluginId::as_key)
|
||||
.or_else(|| plugin.remote_plugin_id.clone())
|
||||
else {
|
||||
return true;
|
||||
};
|
||||
emitted.insert((tracking.turn_id.clone(), plugin_id))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -277,6 +277,7 @@ fn sample_thread_archive_request() -> ClientRequest {
|
||||
fn sample_thread(thread_id: &str) -> Thread {
|
||||
Thread {
|
||||
id: thread_id.to_string(),
|
||||
extra: None,
|
||||
session_id: format!("session-{thread_id}"),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
@@ -309,7 +310,7 @@ fn sample_thread_start_response() -> ClientResponsePayload {
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_sources: Vec::new(),
|
||||
approval_policy: AppServerAskForApproval::OnFailure,
|
||||
approval_policy: AppServerAskForApproval::OnRequest,
|
||||
approvals_reviewer: AppServerApprovalsReviewer::User,
|
||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||
active_permission_profile: None,
|
||||
@@ -327,7 +328,7 @@ fn sample_thread_resume_response() -> ClientResponsePayload {
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_sources: Vec::new(),
|
||||
approval_policy: AppServerAskForApproval::OnFailure,
|
||||
approval_policy: AppServerAskForApproval::OnRequest,
|
||||
approvals_reviewer: AppServerApprovalsReviewer::User,
|
||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||
active_permission_profile: None,
|
||||
@@ -346,7 +347,7 @@ fn sample_thread_fork_response() -> ClientResponsePayload {
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_sources: Vec::new(),
|
||||
approval_policy: AppServerAskForApproval::OnFailure,
|
||||
approval_policy: AppServerAskForApproval::OnRequest,
|
||||
approvals_reviewer: AppServerApprovalsReviewer::User,
|
||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||
active_permission_profile: None,
|
||||
|
||||
@@ -26,6 +26,7 @@ use crate::now_unix_millis;
|
||||
use codex_app_server_protocol::CodexErrorInfo;
|
||||
use codex_app_server_protocol::CommandExecutionSource;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_plugin::PluginTelemetryMetadata;
|
||||
use codex_protocol::approvals::NetworkApprovalProtocol;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
@@ -933,6 +934,7 @@ pub(crate) struct CodexTurnSteerEventRequest {
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexPluginMetadata {
|
||||
pub(crate) plugin_id: Option<String>,
|
||||
pub(crate) remote_plugin_id: Option<String>,
|
||||
pub(crate) plugin_name: Option<String>,
|
||||
pub(crate) marketplace_name: Option<String>,
|
||||
pub(crate) has_skills: Option<bool>,
|
||||
@@ -1040,11 +1042,13 @@ pub(crate) fn codex_plugin_metadata(plugin: PluginTelemetryMetadata) -> CodexPlu
|
||||
remote_plugin_id,
|
||||
capability_summary,
|
||||
} = plugin;
|
||||
let event_plugin_id = remote_plugin_id.unwrap_or_else(|| plugin_id.as_key());
|
||||
CodexPluginMetadata {
|
||||
plugin_id: Some(event_plugin_id),
|
||||
plugin_name: Some(plugin_id.plugin_name),
|
||||
marketplace_name: Some(plugin_id.marketplace_name),
|
||||
plugin_id: plugin_id.as_ref().map(PluginId::as_key),
|
||||
remote_plugin_id,
|
||||
plugin_name: plugin_id
|
||||
.as_ref()
|
||||
.map(|plugin_id| plugin_id.plugin_name.clone()),
|
||||
marketplace_name: plugin_id.map(|plugin_id| plugin_id.marketplace_name),
|
||||
has_skills: capability_summary
|
||||
.as_ref()
|
||||
.map(|summary| summary.has_skills),
|
||||
|
||||
@@ -125,7 +125,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
|
||||
@@ -450,7 +450,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
@@ -3626,6 +3625,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -3765,6 +3765,10 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoal": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
@@ -5111,6 +5115,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
+6
-1
@@ -6864,7 +6864,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
@@ -16831,6 +16830,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -17052,6 +17052,10 @@
|
||||
"title": "ThreadDeletedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadForkParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using a non-empty path, the thread_id param will be ignored. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.",
|
||||
@@ -19699,6 +19703,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
+6
-1
@@ -990,7 +990,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
@@ -14610,6 +14609,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -14831,6 +14831,10 @@
|
||||
"title": "ThreadDeletedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadForkParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using a non-empty path, the thread_id param will be ignored. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.",
|
||||
@@ -17478,6 +17482,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -173,7 +173,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
|
||||
-1
@@ -15,7 +15,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
|
||||
@@ -1438,6 +1438,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
@@ -1059,6 +1058,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -1165,6 +1165,10 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2017,6 +2021,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -865,6 +865,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -971,6 +972,10 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1823,6 +1828,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
+6
@@ -865,6 +865,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -971,6 +972,10 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1823,6 +1828,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -865,6 +865,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -971,6 +972,10 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1823,6 +1828,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -63,7 +63,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
@@ -1059,6 +1058,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -1165,6 +1165,10 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2017,6 +2021,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -865,6 +865,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -971,6 +972,10 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1823,6 +1828,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
-1
@@ -39,7 +39,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
@@ -1059,6 +1058,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -1165,6 +1165,10 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2017,6 +2021,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -865,6 +865,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -971,6 +972,10 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1823,6 +1828,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -865,6 +865,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -971,6 +972,10 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1823,6 +1828,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -1438,6 +1438,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
|
||||
@@ -1438,6 +1438,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -1438,6 +1438,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -2,4 +2,9 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Identifier for a Codex thread.
|
||||
*
|
||||
* Codex-generated thread IDs are UUIDv7, and some use cases rely on that.
|
||||
*/
|
||||
export type ThreadId = string;
|
||||
|
||||
@@ -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 AskForApproval = "untrusted" | "on-failure" | "on-request" | { "granular": { sandbox_approval: boolean, rules: boolean, skill_approval: boolean, request_permissions: boolean, mcp_elicitations: boolean, } } | "never";
|
||||
export type AskForApproval = "untrusted" | "on-request" | { "granular": { sandbox_approval: boolean, rules: boolean, skill_approval: boolean, request_permissions: boolean, mcp_elicitations: boolean, } } | "never";
|
||||
|
||||
+24
-41
@@ -8,87 +8,70 @@ import type { ThreadSource } from "./ThreadSource";
|
||||
import type { ThreadStatus } from "./ThreadStatus";
|
||||
import type { Turn } from "./Turn";
|
||||
|
||||
export type Thread = { id: string,
|
||||
/**
|
||||
export type Thread = {/**
|
||||
* Identifier for this thread. Codex-generated thread IDs are UUIDv7.
|
||||
*/
|
||||
id: string, /**
|
||||
* Session id shared by threads that belong to the same session tree.
|
||||
*/
|
||||
sessionId: string,
|
||||
/**
|
||||
sessionId: string, /**
|
||||
* Source thread id when this thread was created by forking another thread.
|
||||
*/
|
||||
forkedFromId: string | null,
|
||||
/**
|
||||
forkedFromId: string | null, /**
|
||||
* The ID of the parent thread. This will only be set if this thread is a subagent.
|
||||
*/
|
||||
parentThreadId: string | null,
|
||||
/**
|
||||
parentThreadId: string | null, /**
|
||||
* Usually the first user message in the thread, if available.
|
||||
*/
|
||||
preview: string,
|
||||
/**
|
||||
preview: string, /**
|
||||
* Whether the thread is ephemeral and should not be materialized on disk.
|
||||
*/
|
||||
ephemeral: boolean,
|
||||
/**
|
||||
ephemeral: boolean, /**
|
||||
* Model provider used for this thread (for example, 'openai').
|
||||
*/
|
||||
modelProvider: string,
|
||||
/**
|
||||
modelProvider: string, /**
|
||||
* Unix timestamp (in seconds) when the thread was created.
|
||||
*/
|
||||
createdAt: number,
|
||||
/**
|
||||
createdAt: number, /**
|
||||
* Unix timestamp (in seconds) when the thread was last updated.
|
||||
*/
|
||||
updatedAt: number,
|
||||
/**
|
||||
updatedAt: number, /**
|
||||
* Unix timestamp (in seconds) used for thread recency ordering.
|
||||
*/
|
||||
recencyAt: number | null,
|
||||
/**
|
||||
recencyAt: number | null, /**
|
||||
* Current runtime status for the thread.
|
||||
*/
|
||||
status: ThreadStatus,
|
||||
/**
|
||||
status: ThreadStatus, /**
|
||||
* [UNSTABLE] Path to the thread on disk.
|
||||
*/
|
||||
path: string | null,
|
||||
/**
|
||||
path: string | null, /**
|
||||
* Working directory captured for the thread.
|
||||
*/
|
||||
cwd: AbsolutePathBuf,
|
||||
/**
|
||||
cwd: AbsolutePathBuf, /**
|
||||
* Version of the CLI that created the thread.
|
||||
*/
|
||||
cliVersion: string,
|
||||
/**
|
||||
cliVersion: string, /**
|
||||
* Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.).
|
||||
*/
|
||||
source: SessionSource,
|
||||
/**
|
||||
source: SessionSource, /**
|
||||
* Optional analytics source classification for this thread.
|
||||
*/
|
||||
threadSource: ThreadSource | null,
|
||||
/**
|
||||
threadSource: ThreadSource | null, /**
|
||||
* Optional random unique nickname assigned to an AgentControl-spawned sub-agent.
|
||||
*/
|
||||
agentNickname: string | null,
|
||||
/**
|
||||
agentNickname: string | null, /**
|
||||
* Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.
|
||||
*/
|
||||
agentRole: string | null,
|
||||
/**
|
||||
agentRole: string | null, /**
|
||||
* Optional Git metadata captured when the thread was created.
|
||||
*/
|
||||
gitInfo: GitInfo | null,
|
||||
/**
|
||||
gitInfo: GitInfo | null, /**
|
||||
* Optional user-facing thread title.
|
||||
*/
|
||||
name: string | null,
|
||||
/**
|
||||
name: string | null, /**
|
||||
* Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read`
|
||||
* (when `includeTurns` is true) responses.
|
||||
* For all other responses and notifications returning a Thread,
|
||||
* the turns field will be an empty list.
|
||||
*/
|
||||
turns: Array<Turn>, };
|
||||
turns: Array<Turn>};
|
||||
|
||||
@@ -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.
|
||||
|
||||
/**
|
||||
* Extra app-server data for a thread.
|
||||
*/
|
||||
export type ThreadExtra = Record<string, never>;
|
||||
+5
-1
@@ -6,7 +6,11 @@ import type { TurnError } from "./TurnError";
|
||||
import type { TurnItemsView } from "./TurnItemsView";
|
||||
import type { TurnStatus } from "./TurnStatus";
|
||||
|
||||
export type Turn = { id: string,
|
||||
export type Turn = {
|
||||
/**
|
||||
* Identifier for this turn. Codex-generated turn IDs are UUIDv7.
|
||||
*/
|
||||
id: string,
|
||||
/**
|
||||
* Thread items currently included in this turn payload.
|
||||
*/
|
||||
|
||||
@@ -395,6 +395,7 @@ export type { ThreadCompactStartResponse } from "./ThreadCompactStartResponse";
|
||||
export type { ThreadDeleteParams } from "./ThreadDeleteParams";
|
||||
export type { ThreadDeleteResponse } from "./ThreadDeleteResponse";
|
||||
export type { ThreadDeletedNotification } from "./ThreadDeletedNotification";
|
||||
export type { ThreadExtra } from "./ThreadExtra";
|
||||
export type { ThreadForkParams } from "./ThreadForkParams";
|
||||
export type { ThreadForkResponse } from "./ThreadForkResponse";
|
||||
export type { ThreadGoal } from "./ThreadGoal";
|
||||
|
||||
@@ -2573,6 +2573,7 @@ mod tests {
|
||||
response: v2::ThreadStartResponse {
|
||||
thread: v2::Thread {
|
||||
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
|
||||
extra: None,
|
||||
session_id: "67e55044-10b1-426f-9247-bb680e5fe0c7".to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
@@ -2604,7 +2605,7 @@ mod tests {
|
||||
"/tmp/AGENTS.md",
|
||||
)),
|
||||
],
|
||||
approval_policy: v2::AskForApproval::OnFailure,
|
||||
approval_policy: v2::AskForApproval::OnRequest,
|
||||
approvals_reviewer: v2::ApprovalsReviewer::User,
|
||||
sandbox: v2::SandboxPolicy::DangerFullAccess,
|
||||
active_permission_profile: None,
|
||||
@@ -2622,6 +2623,7 @@ mod tests {
|
||||
"response": {
|
||||
"thread": {
|
||||
"id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
|
||||
"extra": null,
|
||||
"sessionId": "67e55044-10b1-426f-9247-bb680e5fe0c7",
|
||||
"forkedFromId": null,
|
||||
"parentThreadId": null,
|
||||
@@ -2651,7 +2653,7 @@ mod tests {
|
||||
"cwd": absolute_path_string("tmp"),
|
||||
"runtimeWorkspaceRoots": [],
|
||||
"instructionSources": [absolute_path_string("tmp/AGENTS.md")],
|
||||
"approvalPolicy": "on-failure",
|
||||
"approvalPolicy": "on-request",
|
||||
"approvalsReviewer": "user",
|
||||
"sandbox": {
|
||||
"type": "dangerFullAccess"
|
||||
|
||||
@@ -163,7 +163,6 @@ pub enum AskForApproval {
|
||||
#[serde(rename = "untrusted")]
|
||||
#[ts(rename = "untrusted")]
|
||||
UnlessTrusted,
|
||||
OnFailure,
|
||||
OnRequest,
|
||||
#[experimental("askForApproval.granular")]
|
||||
Granular {
|
||||
@@ -182,7 +181,6 @@ impl AskForApproval {
|
||||
pub fn to_core(self) -> CoreAskForApproval {
|
||||
match self {
|
||||
AskForApproval::UnlessTrusted => CoreAskForApproval::UnlessTrusted,
|
||||
AskForApproval::OnFailure => CoreAskForApproval::OnFailure,
|
||||
AskForApproval::OnRequest => CoreAskForApproval::OnRequest,
|
||||
AskForApproval::Granular {
|
||||
sandbox_approval,
|
||||
@@ -206,7 +204,6 @@ impl From<CoreAskForApproval> for AskForApproval {
|
||||
fn from(value: CoreAskForApproval) -> Self {
|
||||
match value {
|
||||
CoreAskForApproval::UnlessTrusted => AskForApproval::UnlessTrusted,
|
||||
CoreAskForApproval::OnFailure => AskForApproval::OnFailure,
|
||||
CoreAskForApproval::OnRequest => AskForApproval::OnRequest,
|
||||
CoreAskForApproval::Granular(granular_config) => AskForApproval::Granular {
|
||||
sandbox_approval: granular_config.sandbox_approval,
|
||||
|
||||
@@ -168,6 +168,7 @@ fn thread_resume_response_round_trips_initial_turns_page() {
|
||||
let response = ThreadResumeResponse {
|
||||
thread: Thread {
|
||||
id: "thr_123".to_string(),
|
||||
extra: None,
|
||||
session_id: "thr_123".to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
@@ -195,7 +196,7 @@ fn thread_resume_response_round_trips_initial_turns_page() {
|
||||
cwd: absolute_path("tmp"),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_sources: Vec::new(),
|
||||
approval_policy: AskForApproval::OnFailure,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
sandbox: SandboxPolicy::DangerFullAccess,
|
||||
active_permission_profile: None,
|
||||
@@ -3689,7 +3690,7 @@ fn thread_lifecycle_responses_default_missing_optional_fields() {
|
||||
"modelProvider": "openai",
|
||||
"serviceTier": null,
|
||||
"cwd": absolute_path_string("tmp"),
|
||||
"approvalPolicy": "on-failure",
|
||||
"approvalPolicy": "on-request",
|
||||
"approvalsReviewer": "user",
|
||||
"sandbox": { "type": "dangerFullAccess" },
|
||||
"reasoningEffort": null
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::CodexErrorInfo;
|
||||
use super::ThreadItem;
|
||||
use super::ThreadStatus;
|
||||
use super::TurnStatus;
|
||||
use codex_experimental_api_macros::ExperimentalApi;
|
||||
use codex_protocol::protocol::SessionSource as CoreSessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource as CoreSubAgentSource;
|
||||
use codex_protocol::protocol::ThreadSource as CoreThreadSource;
|
||||
@@ -120,6 +121,12 @@ impl From<ThreadSource> for CoreThreadSource {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extra app-server data for a thread.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase", export_to = "v2/")]
|
||||
pub struct ThreadExtra {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -129,11 +136,15 @@ pub struct GitInfo {
|
||||
pub origin_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct Thread {
|
||||
/// Identifier for this thread. Codex-generated thread IDs are UUIDv7.
|
||||
pub id: String,
|
||||
/// Optional implementation-specific thread data.
|
||||
#[experimental("thread.extra")]
|
||||
pub extra: Option<ThreadExtra>,
|
||||
/// Session id shared by threads that belong to the same session tree.
|
||||
pub session_id: String,
|
||||
/// Source thread id when this thread was created by forking another thread.
|
||||
@@ -186,6 +197,7 @@ pub struct Thread {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct Turn {
|
||||
/// Identifier for this turn. Codex-generated turn IDs are UUIDv7.
|
||||
pub id: String,
|
||||
/// Thread items currently included in this turn payload.
|
||||
pub items: Vec<ThreadItem>,
|
||||
|
||||
@@ -46,12 +46,13 @@ cargo run -p codex-app-server-test-client -- \
|
||||
|
||||
Use `--capture-file /tmp/plugin-analytics.jsonl` to select the output path.
|
||||
The command validates one `codex_plugin_disabled`, `codex_plugin_enabled`, and
|
||||
`codex_plugin_used` event with the expected local plugin identity and capability
|
||||
metadata. The enabled and disabled events come from successful writes to the
|
||||
temporary config; the command does not mutate the remote enabled state. It
|
||||
prints the events and leaves the JSONL file in place for inspection. It does not
|
||||
install or uninstall plugins and does not modify the profile's persistent
|
||||
config.
|
||||
`codex_plugin_used` event with the expected local and remote plugin identities
|
||||
and capability metadata. Each event includes the local ID in `plugin_id` and the
|
||||
backend ID in `remote_plugin_id`. The enabled and disabled events come from
|
||||
successful writes to the temporary config; the command does not mutate the
|
||||
remote enabled state. It prints the events and leaves the JSONL file in place
|
||||
for inspection. It does not install or uninstall plugins and does not modify
|
||||
the profile's persistent config.
|
||||
|
||||
### Testing remote install and uninstall analytics
|
||||
|
||||
@@ -63,9 +64,11 @@ or CI.
|
||||
Choose a remote plugin that is available to the active account and is not
|
||||
currently installed. The command refuses to run when the plugin is already
|
||||
installed, installs it, validates `codex_plugin_installed`, uninstalls it, and
|
||||
verifies that the original uninstalled state was restored. The current install
|
||||
event uses the backend ID as `plugin_id`. Uninstall is part of cleanup but is
|
||||
not yet an analytics assertion.
|
||||
validates `codex_plugin_uninstalled`, and verifies that the original
|
||||
uninstalled state was restored.
|
||||
|
||||
The mutation events include the local Codex ID in `plugin_id` and the backend ID
|
||||
in `remote_plugin_id`.
|
||||
|
||||
`--remote-plugin-id` takes the backend ID, such as `plugins~Plugin_...`, not the
|
||||
local `<plugin>@<marketplace>` ID.
|
||||
@@ -83,7 +86,7 @@ Analytics use the normal queue, reduction, batching, and serialization path,
|
||||
but the debug capture destination suppresses analytics network delivery. The
|
||||
command prints one of these final states:
|
||||
|
||||
- `PASS`: the install event validated and the plugin is uninstalled.
|
||||
- `PASS`: the install and uninstall events validated and the plugin is uninstalled.
|
||||
- `FAIL-CLEAN`: validation failed, but the original uninstalled state was
|
||||
restored.
|
||||
- `FAIL-LOCAL-CACHE`: the backend is uninstalled, but local cleanup reported
|
||||
|
||||
@@ -35,7 +35,7 @@ pub(super) fn read_events_for_remote_plugin(
|
||||
matching.extend(
|
||||
events
|
||||
.iter()
|
||||
.filter(|event| event["event_params"]["plugin_id"] == remote_plugin_id)
|
||||
.filter(|event| event["event_params"]["remote_plugin_id"] == remote_plugin_id)
|
||||
.cloned(),
|
||||
);
|
||||
}
|
||||
@@ -44,6 +44,7 @@ pub(super) fn read_events_for_remote_plugin(
|
||||
|
||||
pub(super) struct PluginEventIdentity<'a> {
|
||||
pub(super) plugin_id: &'a str,
|
||||
pub(super) remote_plugin_id: &'a str,
|
||||
pub(super) plugin_name: &'a str,
|
||||
pub(super) marketplace_name: &'a str,
|
||||
}
|
||||
@@ -52,25 +53,29 @@ pub(super) fn validate_mutation_events(
|
||||
events: Vec<Value>,
|
||||
expected: PluginEventIdentity<'_>,
|
||||
) -> Result<Vec<Value>> {
|
||||
let event_type = "codex_plugin_installed";
|
||||
let matching = events
|
||||
.iter()
|
||||
.filter(|event| event["event_type"] == event_type)
|
||||
.collect::<Vec<_>>();
|
||||
let [event] = matching.as_slice() else {
|
||||
bail!(
|
||||
"expected exactly one `{event_type}` event for `{}`, found {}",
|
||||
expected.plugin_id,
|
||||
matching.len()
|
||||
);
|
||||
};
|
||||
validate_event(event, &expected)?;
|
||||
Ok(vec![(*event).clone()])
|
||||
let mut validated = Vec::new();
|
||||
for event_type in ["codex_plugin_installed", "codex_plugin_uninstalled"] {
|
||||
let matching = events
|
||||
.iter()
|
||||
.filter(|event| event["event_type"] == event_type)
|
||||
.collect::<Vec<_>>();
|
||||
let [event] = matching.as_slice() else {
|
||||
bail!(
|
||||
"expected exactly one `{event_type}` event for `{}`, found {}",
|
||||
expected.remote_plugin_id,
|
||||
matching.len()
|
||||
);
|
||||
};
|
||||
validate_event(event, &expected)?;
|
||||
validated.push((*event).clone());
|
||||
}
|
||||
Ok(validated)
|
||||
}
|
||||
|
||||
fn validate_event(event: &Value, expected: &PluginEventIdentity<'_>) -> Result<()> {
|
||||
let params = &event["event_params"];
|
||||
require_string(params, "plugin_id", expected.plugin_id)?;
|
||||
require_string(params, "remote_plugin_id", expected.remote_plugin_id)?;
|
||||
require_string(params, "plugin_name", expected.plugin_name)?;
|
||||
require_string(params, "marketplace_name", expected.marketplace_name)?;
|
||||
for field in [
|
||||
|
||||
@@ -14,15 +14,17 @@ const REMOTE_PLUGIN_ID: &str = "plugins~Plugin_test";
|
||||
fn reads_and_validates_remote_plugin_mutation_events() {
|
||||
let path = unique_capture_path("valid");
|
||||
let installed = mutation_event("codex_plugin_installed");
|
||||
let uninstalled = mutation_event("codex_plugin_uninstalled");
|
||||
let unrelated = json!({
|
||||
"event_type": "codex_plugin_installed",
|
||||
"event_params": {
|
||||
"plugin_id": "plugins~Plugin_other"
|
||||
"plugin_id": "other@openai-curated-remote",
|
||||
"remote_plugin_id": "plugins~Plugin_other"
|
||||
}
|
||||
});
|
||||
let contents = [
|
||||
json!({"events": [unrelated]}),
|
||||
json!({"events": [installed]}),
|
||||
json!({"events": [installed, uninstalled]}),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|payload| serde_json::to_string(&payload).expect("serialize capture payload"))
|
||||
@@ -35,7 +37,7 @@ fn reads_and_validates_remote_plugin_mutation_events() {
|
||||
let validated =
|
||||
validate_mutation_events(events, expected_identity()).expect("validate mutation events");
|
||||
|
||||
assert_eq!(validated, vec![installed]);
|
||||
assert_eq!(validated, vec![installed, uninstalled]);
|
||||
fs::remove_file(path).expect("remove capture file");
|
||||
}
|
||||
|
||||
@@ -62,7 +64,8 @@ fn mutation_event(event_type: &str) -> Value {
|
||||
json!({
|
||||
"event_type": event_type,
|
||||
"event_params": {
|
||||
"plugin_id": REMOTE_PLUGIN_ID,
|
||||
"plugin_id": "sample@openai-curated-remote",
|
||||
"remote_plugin_id": REMOTE_PLUGIN_ID,
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "openai-curated-remote",
|
||||
"has_skills": true,
|
||||
@@ -75,7 +78,8 @@ fn mutation_event(event_type: &str) -> Value {
|
||||
|
||||
fn expected_identity() -> PluginEventIdentity<'static> {
|
||||
PluginEventIdentity {
|
||||
plugin_id: REMOTE_PLUGIN_ID,
|
||||
plugin_id: "sample@openai-curated-remote",
|
||||
remote_plugin_id: REMOTE_PLUGIN_ID,
|
||||
plugin_name: "sample",
|
||||
marketplace_name: "openai-curated-remote",
|
||||
}
|
||||
|
||||
@@ -312,13 +312,19 @@ fn run_mutation_sequence(
|
||||
state_err
|
||||
}
|
||||
})?;
|
||||
wait_for_remote_plugin_event(
|
||||
capture_path,
|
||||
&expected.remote_plugin_id,
|
||||
"codex_plugin_uninstalled",
|
||||
)?;
|
||||
|
||||
let captured_events =
|
||||
read_events_for_remote_plugin(capture_path, &expected.remote_plugin_id)?;
|
||||
let events = validate_mutation_events(
|
||||
captured_events,
|
||||
PluginEventIdentity {
|
||||
plugin_id: &expected.remote_plugin_id,
|
||||
plugin_id: &expected.plugin_id,
|
||||
remote_plugin_id: &expected.remote_plugin_id,
|
||||
plugin_name: &expected.plugin_name,
|
||||
marketplace_name: &expected.marketplace_name,
|
||||
},
|
||||
|
||||
@@ -159,6 +159,7 @@ fn wait_for_plugin_usage(
|
||||
#[derive(Debug)]
|
||||
struct ExpectedPlugin {
|
||||
plugin_id: String,
|
||||
remote_plugin_id: String,
|
||||
plugin_name: String,
|
||||
marketplace_name: String,
|
||||
}
|
||||
@@ -208,13 +209,15 @@ fn expected_plugin(response: &PluginInstalledResponse, plugin_id: &str) -> Resul
|
||||
plugin.availability
|
||||
);
|
||||
}
|
||||
plugin
|
||||
let remote_plugin_id = plugin
|
||||
.remote_plugin_id
|
||||
.as_ref()
|
||||
.with_context(|| format!("plugin `{plugin_id}` does not have a remote plugin id"))?;
|
||||
.with_context(|| format!("plugin `{plugin_id}` does not have a remote plugin id"))?
|
||||
.clone();
|
||||
|
||||
Ok(ExpectedPlugin {
|
||||
plugin_id: plugin.id.clone(),
|
||||
remote_plugin_id,
|
||||
plugin_name: plugin.name.clone(),
|
||||
marketplace_name: marketplace.name.clone(),
|
||||
})
|
||||
@@ -444,6 +447,7 @@ fn event_count(events: &[Value], event_type: &str) -> usize {
|
||||
fn validate_identity(event: &Value, expected: &ExpectedPlugin) -> Result<()> {
|
||||
let params = &event["event_params"];
|
||||
require_string(params, "plugin_id", &expected.plugin_id)?;
|
||||
require_string(params, "remote_plugin_id", &expected.remote_plugin_id)?;
|
||||
require_string(params, "plugin_name", &expected.plugin_name)?;
|
||||
require_string(params, "marketplace_name", &expected.marketplace_name)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ use codex_mcp::McpOAuthLoginSupport;
|
||||
use codex_mcp::oauth_login_support;
|
||||
use codex_mcp::should_retry_without_scopes;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_plugin::PluginTelemetryMetadata;
|
||||
use codex_rmcp_client::perform_oauth_login_silent;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -1528,6 +1529,7 @@ impl PluginRequestProcessor {
|
||||
self.track_plugin_install_failed_for_remote_plugin(
|
||||
&remote_plugin_id,
|
||||
&remote_marketplace_name,
|
||||
/*plugin_id*/ None,
|
||||
error_type,
|
||||
err.to_string(),
|
||||
);
|
||||
@@ -1538,6 +1540,12 @@ impl PluginRequestProcessor {
|
||||
})?;
|
||||
let actual_remote_marketplace_name = remote_detail.marketplace_name.clone();
|
||||
let remote_plugin_name = remote_detail.summary.name.clone();
|
||||
let resolved_plugin_id = PluginId::parse(&remote_detail.summary.id).map_err(|err| {
|
||||
internal_error(format!(
|
||||
"invalid resolved plugin id `{}`: {err}",
|
||||
remote_detail.summary.id
|
||||
))
|
||||
})?;
|
||||
if remote_detail.summary.availability == PluginAvailability::DisabledByAdmin {
|
||||
return Err(invalid_request(format!(
|
||||
"remote plugin {remote_plugin_id} is disabled by admin"
|
||||
@@ -1569,6 +1577,7 @@ impl PluginRequestProcessor {
|
||||
self.track_plugin_install_failed_for_remote_plugin(
|
||||
&remote_plugin_id,
|
||||
&actual_remote_marketplace_name,
|
||||
Some(&resolved_plugin_id),
|
||||
error_type,
|
||||
err.to_string(),
|
||||
);
|
||||
@@ -1585,6 +1594,7 @@ impl PluginRequestProcessor {
|
||||
self.track_plugin_install_failed_for_remote_plugin(
|
||||
&remote_plugin_id,
|
||||
&actual_remote_marketplace_name,
|
||||
Some(&resolved_plugin_id),
|
||||
error_type,
|
||||
err.to_string(),
|
||||
);
|
||||
@@ -1606,6 +1616,7 @@ impl PluginRequestProcessor {
|
||||
self.track_plugin_install_failed_for_remote_plugin(
|
||||
&remote_plugin_id,
|
||||
&actual_remote_marketplace_name,
|
||||
Some(&result.plugin_id),
|
||||
error_type,
|
||||
err.to_string(),
|
||||
);
|
||||
@@ -1702,6 +1713,7 @@ impl PluginRequestProcessor {
|
||||
&self,
|
||||
remote_plugin_id: &str,
|
||||
marketplace_name: &str,
|
||||
plugin_id: Option<&PluginId>,
|
||||
error_type: &'static str,
|
||||
error_message: String,
|
||||
) {
|
||||
@@ -1712,16 +1724,17 @@ impl PluginRequestProcessor {
|
||||
error = %error_message,
|
||||
"remote plugin install failed"
|
||||
);
|
||||
// The remote id is reported separately; this local name only satisfies
|
||||
// PluginId validation before remote details are available.
|
||||
let Ok(plugin_id) = PluginId::new("unknown".to_string(), marketplace_name.to_string())
|
||||
else {
|
||||
return;
|
||||
let plugin = if let Some(plugin_id) = plugin_id {
|
||||
self.thread_manager
|
||||
.plugins_manager()
|
||||
.telemetry_metadata_for_plugin_id_with_remote_id(plugin_id, remote_plugin_id)
|
||||
} else {
|
||||
PluginTelemetryMetadata {
|
||||
plugin_id: None,
|
||||
remote_plugin_id: Some(remote_plugin_id.to_string()),
|
||||
capability_summary: None,
|
||||
}
|
||||
};
|
||||
let plugin = self
|
||||
.thread_manager
|
||||
.plugins_manager()
|
||||
.telemetry_metadata_for_plugin_id_with_remote_id(&plugin_id, remote_plugin_id);
|
||||
self.analytics_events_client
|
||||
.track_plugin_install_failed(plugin, error_type.to_string());
|
||||
}
|
||||
@@ -1980,11 +1993,31 @@ impl PluginRequestProcessor {
|
||||
let remote_plugin_service_config = RemotePluginServiceConfig {
|
||||
chatgpt_base_url: config.chatgpt_base_url.clone(),
|
||||
};
|
||||
let uninstall_target = codex_core_plugins::remote::resolve_remote_plugin_uninstall_target(
|
||||
&remote_plugin_service_config,
|
||||
auth.as_ref(),
|
||||
&plugin_id,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
remote_plugin_catalog_error_to_jsonrpc(err, "resolve remote plugin before uninstall")
|
||||
})?;
|
||||
let plugins_manager = self.thread_manager.plugins_manager();
|
||||
let mut plugin_telemetry = plugins_manager
|
||||
.telemetry_metadata_for_installed_plugin_with_remote_id(
|
||||
&uninstall_target.plugin_id,
|
||||
&uninstall_target.remote_plugin_id,
|
||||
)
|
||||
.await;
|
||||
if plugin_telemetry.capability_summary.is_none() {
|
||||
plugin_telemetry.capability_summary =
|
||||
Some(uninstall_target.fallback_capability_summary.clone());
|
||||
}
|
||||
let uninstall_result = codex_core_plugins::remote::uninstall_remote_plugin(
|
||||
&remote_plugin_service_config,
|
||||
auth.as_ref(),
|
||||
config.codex_home.to_path_buf(),
|
||||
&plugin_id,
|
||||
uninstall_target,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1992,7 +2025,8 @@ impl PluginRequestProcessor {
|
||||
&uninstall_result,
|
||||
Ok(()) | Err(RemotePluginCatalogError::CacheRemove(_))
|
||||
) {
|
||||
let plugins_manager = self.thread_manager.plugins_manager();
|
||||
self.analytics_events_client
|
||||
.track_plugin_uninstalled(plugin_telemetry);
|
||||
if plugins_manager.clear_remote_installed_plugins_cache() {
|
||||
self.on_effective_plugins_changed();
|
||||
}
|
||||
|
||||
@@ -2604,8 +2604,8 @@ impl ThreadRequestProcessor {
|
||||
self.resume_thread_from_history(history.as_slice())
|
||||
.await
|
||||
.map(|thread_history| (thread_history, None))
|
||||
} else if let Some(stored_thread) = stored_thread_from_running_probe {
|
||||
self.stored_thread_to_initial_history(&stored_thread)
|
||||
} else if let Some(mut stored_thread) = stored_thread_from_running_probe {
|
||||
self.stored_thread_to_initial_history(&mut stored_thread)
|
||||
.await
|
||||
.map(|thread_history| (thread_history, Some(*stored_thread)))
|
||||
} else {
|
||||
@@ -2758,7 +2758,7 @@ impl ThreadRequestProcessor {
|
||||
let token_usage_thread = include_turns.then(|| thread.clone());
|
||||
let mut initial_turns_page = if let Some(params) = initial_turns_page.as_ref() {
|
||||
match build_thread_resume_initial_turns_page(
|
||||
&response_history.get_rollout_items(),
|
||||
response_history.get_rollout_items(),
|
||||
thread.status.clone(),
|
||||
/*has_live_running_thread*/ false,
|
||||
/*active_turn*/ None,
|
||||
@@ -2803,7 +2803,7 @@ impl ThreadRequestProcessor {
|
||||
// rebuilding history only to attribute a replayed usage update.
|
||||
if let Some(token_usage_thread) = token_usage_thread {
|
||||
let token_usage_turn_id = latest_token_usage_turn_id_from_rollout_items(
|
||||
&response_history.get_rollout_items(),
|
||||
response_history.get_rollout_items(),
|
||||
token_usage_thread.turns.as_slice(),
|
||||
);
|
||||
// The client needs restored usage before it starts another turn.
|
||||
@@ -2901,7 +2901,7 @@ impl ThreadRequestProcessor {
|
||||
}
|
||||
};
|
||||
|
||||
if let Some((existing_thread_id, existing_thread, source_thread)) = running_thread {
|
||||
if let Some((existing_thread_id, existing_thread, mut source_thread)) = running_thread {
|
||||
let existing_thread_rollout_path = existing_thread.rollout_path();
|
||||
let active_path = existing_thread_rollout_path
|
||||
.as_ref()
|
||||
@@ -2963,8 +2963,8 @@ impl ThreadRequestProcessor {
|
||||
should_redact_thread_resume_payloads(app_server_client_name.as_deref());
|
||||
let history_items = source_thread
|
||||
.history
|
||||
.as_ref()
|
||||
.map(|history| history.items.clone())
|
||||
.take()
|
||||
.map(|history| history.items)
|
||||
.ok_or_else(|| {
|
||||
internal_error(format!(
|
||||
"thread {existing_thread_id} did not include persisted history"
|
||||
@@ -2988,10 +2988,8 @@ impl ThreadRequestProcessor {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut summary_source_thread = source_thread;
|
||||
summary_source_thread.history = None;
|
||||
let mut thread_summary = self.stored_thread_to_api_thread(
|
||||
summary_source_thread,
|
||||
source_thread,
|
||||
config_snapshot.model_provider_id.as_str(),
|
||||
/*include_turns*/ false,
|
||||
);
|
||||
@@ -3060,11 +3058,11 @@ impl ThreadRequestProcessor {
|
||||
thread_id: &str,
|
||||
path: Option<&PathBuf>,
|
||||
) -> Result<(InitialHistory, StoredThread), JSONRPCErrorError> {
|
||||
let stored_thread = self
|
||||
let mut stored_thread = self
|
||||
.read_stored_thread_for_resume(thread_id, path, /*include_history*/ true)
|
||||
.await?;
|
||||
let history = self
|
||||
.stored_thread_to_initial_history(&stored_thread)
|
||||
.stored_thread_to_initial_history(&mut stored_thread)
|
||||
.await?;
|
||||
Ok((history, stored_thread))
|
||||
}
|
||||
@@ -3112,13 +3110,13 @@ impl ThreadRequestProcessor {
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn stored_thread_to_initial_history(
|
||||
&self,
|
||||
stored_thread: &StoredThread,
|
||||
stored_thread: &mut StoredThread,
|
||||
) -> Result<InitialHistory, JSONRPCErrorError> {
|
||||
let thread_id = stored_thread.thread_id;
|
||||
let history = stored_thread
|
||||
.history
|
||||
.as_ref()
|
||||
.map(|history| history.items.clone())
|
||||
.take()
|
||||
.map(|history| history.items)
|
||||
.ok_or_else(|| {
|
||||
internal_error(format!(
|
||||
"thread {thread_id} did not include persisted history"
|
||||
@@ -3126,7 +3124,7 @@ impl ThreadRequestProcessor {
|
||||
})?;
|
||||
Ok(InitialHistory::Resumed(ResumedHistory {
|
||||
conversation_id: thread_id,
|
||||
history,
|
||||
history: Arc::new(history),
|
||||
rollout_path: stored_thread.rollout_path.clone(),
|
||||
}))
|
||||
}
|
||||
@@ -3255,7 +3253,7 @@ impl ThreadRequestProcessor {
|
||||
let history_items = thread_history.get_rollout_items();
|
||||
populate_thread_turns_from_history(
|
||||
&mut thread,
|
||||
&history_items,
|
||||
history_items,
|
||||
/*active_turn*/ None,
|
||||
);
|
||||
}
|
||||
@@ -3313,7 +3311,7 @@ impl ThreadRequestProcessor {
|
||||
"`permissions` cannot be combined with `sandbox`",
|
||||
));
|
||||
}
|
||||
let source_thread = self
|
||||
let mut source_thread = self
|
||||
.read_stored_thread_for_resume(&thread_id, path.as_ref(), /*include_history*/ true)
|
||||
.await?;
|
||||
let source_thread_id = source_thread.thread_id;
|
||||
@@ -3323,8 +3321,8 @@ impl ThreadRequestProcessor {
|
||||
.and_then(codex_core::util::normalize_thread_name);
|
||||
let history_items = source_thread
|
||||
.history
|
||||
.as_ref()
|
||||
.map(|history| history.items.clone())
|
||||
.take()
|
||||
.map(|history| Arc::new(history.items))
|
||||
.ok_or_else(|| {
|
||||
internal_error(format!(
|
||||
"thread {source_thread_id} did not include persisted history"
|
||||
@@ -3391,7 +3389,7 @@ impl ThreadRequestProcessor {
|
||||
config,
|
||||
InitialHistory::Resumed(ResumedHistory {
|
||||
conversation_id: source_thread_id,
|
||||
history: history_items.clone(),
|
||||
history: Arc::clone(&history_items),
|
||||
rollout_path: source_thread.rollout_path.clone(),
|
||||
}),
|
||||
thread_source.map(Into::into),
|
||||
@@ -4158,6 +4156,7 @@ pub(crate) fn thread_from_stored_thread(
|
||||
let thread_id = thread.thread_id.to_string();
|
||||
let thread = Thread {
|
||||
id: thread_id.clone(),
|
||||
extra: None,
|
||||
session_id: thread_id,
|
||||
forked_from_id: thread.forked_from_id.map(|id| id.to_string()),
|
||||
parent_thread_id: thread.parent_thread_id.map(|id| id.to_string()),
|
||||
@@ -4368,6 +4367,7 @@ fn build_thread_from_snapshot(
|
||||
let now = time::OffsetDateTime::now_utc().unix_timestamp();
|
||||
Thread {
|
||||
id: thread_id.to_string(),
|
||||
extra: None,
|
||||
session_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: config_snapshot.parent_thread_id.map(|id| id.to_string()),
|
||||
|
||||
@@ -183,6 +183,7 @@ mod tests {
|
||||
fn test_thread(items: Vec<ThreadItem>) -> Thread {
|
||||
Thread {
|
||||
id: "thread-1".to_string(),
|
||||
extra: None,
|
||||
session_id: "session-1".to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
|
||||
@@ -314,6 +314,7 @@ pub(crate) fn summary_to_thread(
|
||||
let thread_id = conversation_id.to_string();
|
||||
Thread {
|
||||
id: thread_id.clone(),
|
||||
extra: None,
|
||||
session_id: thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
|
||||
@@ -58,6 +58,7 @@ fn validate_response_item_image_urls(items: &[ResponseItem]) -> Result<(), JSONR
|
||||
| ResponseItem::Compaction { .. }
|
||||
| ResponseItem::CompactionTrigger { .. }
|
||||
| ResponseItem::ContextCompaction { .. }
|
||||
| ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::Other => false,
|
||||
}) {
|
||||
return Err(invalid_request(REMOTE_IMAGE_URL_ERROR));
|
||||
@@ -1236,7 +1237,7 @@ impl TurnRequestProcessor {
|
||||
config.clone(),
|
||||
InitialHistory::Resumed(ResumedHistory {
|
||||
conversation_id: parent_thread_id,
|
||||
history: parent_history.items,
|
||||
history: Arc::new(parent_history.items),
|
||||
rollout_path: parent_thread.rollout_path(),
|
||||
}),
|
||||
/*thread_source*/ None,
|
||||
|
||||
@@ -889,6 +889,7 @@ mod tests {
|
||||
fn test_thread(thread_id: &str, source: codex_app_server_protocol::SessionSource) -> Thread {
|
||||
Thread {
|
||||
id: thread_id.to_string(),
|
||||
extra: None,
|
||||
session_id: thread_id.to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
|
||||
@@ -115,7 +115,7 @@ model_provider = "mock_provider"
|
||||
|
||||
[features.current_time_reminder]
|
||||
enabled = true
|
||||
reminder_interval_model_requests = 1
|
||||
reminder_interval_seconds = 1
|
||||
clock_source = "external"
|
||||
|
||||
[model_providers.mock_provider]
|
||||
|
||||
@@ -538,12 +538,10 @@ async fn plugin_install_tracks_analytics_when_remote_detail_fetch_fails() -> Res
|
||||
payload["events"][0]["event_type"],
|
||||
"codex_plugin_install_failed"
|
||||
);
|
||||
assert_eq!(event_params["plugin_id"], REMOTE_PLUGIN_ID);
|
||||
assert_eq!(event_params["plugin_name"], "unknown");
|
||||
assert_eq!(
|
||||
event_params["marketplace_name"],
|
||||
"caller-marketplace-is-ignored"
|
||||
);
|
||||
assert_eq!(event_params["plugin_id"], json!(null));
|
||||
assert_eq!(event_params["remote_plugin_id"], REMOTE_PLUGIN_ID);
|
||||
assert_eq!(event_params["plugin_name"], json!(null));
|
||||
assert_eq!(event_params["marketplace_name"], json!(null));
|
||||
assert_eq!(
|
||||
event_params["error_type"],
|
||||
"remote_catalog_unexpected_status"
|
||||
@@ -883,6 +881,7 @@ async fn plugin_install_tracks_analytics_event() -> Result<()> {
|
||||
"event_type": "codex_plugin_installed",
|
||||
"event_params": {
|
||||
"plugin_id": "sample-plugin@debug",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample-plugin",
|
||||
"marketplace_name": "debug",
|
||||
"has_skills": false,
|
||||
@@ -946,6 +945,7 @@ async fn plugin_install_failure_tracks_analytics_event() -> Result<()> {
|
||||
"codex_plugin_install_failed"
|
||||
);
|
||||
assert_eq!(event_params["plugin_id"], "sample-plugin@debug");
|
||||
assert_eq!(event_params["remote_plugin_id"], json!(null));
|
||||
assert_eq!(event_params["plugin_name"], "sample-plugin");
|
||||
assert_eq!(event_params["marketplace_name"], "debug");
|
||||
assert_eq!(event_params["has_skills"], json!(null));
|
||||
@@ -995,7 +995,8 @@ async fn plugin_install_tracks_remote_plugin_analytics_event() -> Result<()> {
|
||||
"events": [{
|
||||
"event_type": "codex_plugin_installed",
|
||||
"event_params": {
|
||||
"plugin_id": REMOTE_PLUGIN_ID,
|
||||
"plugin_id": "linear@openai-curated-remote",
|
||||
"remote_plugin_id": REMOTE_PLUGIN_ID,
|
||||
"plugin_name": "linear",
|
||||
"marketplace_name": "openai-curated-remote",
|
||||
"has_skills": true,
|
||||
@@ -1072,7 +1073,8 @@ async fn plugin_install_preserves_status_when_remote_bundle_error_body_is_too_la
|
||||
payload["events"][0]["event_type"],
|
||||
"codex_plugin_install_failed"
|
||||
);
|
||||
assert_eq!(event_params["plugin_id"], REMOTE_PLUGIN_ID);
|
||||
assert_eq!(event_params["plugin_id"], "linear@openai-curated-remote");
|
||||
assert_eq!(event_params["remote_plugin_id"], REMOTE_PLUGIN_ID);
|
||||
assert_eq!(event_params["marketplace_name"], "openai-curated-remote");
|
||||
assert_eq!(event_params["error_type"], "remote_bundle_download_status");
|
||||
assert!(
|
||||
|
||||
@@ -139,6 +139,7 @@ async fn plugin_uninstall_tracks_analytics_event() -> Result<()> {
|
||||
"event_type": "codex_plugin_uninstalled",
|
||||
"event_params": {
|
||||
"plugin_id": "sample-plugin@debug",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample-plugin",
|
||||
"marketplace_name": "debug",
|
||||
"has_skills": false,
|
||||
@@ -216,6 +217,11 @@ async fn plugin_uninstall_writes_remote_plugin_to_cloud_when_remote_plugin_enabl
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/backend-api/codex/analytics-events/events"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string(r#"{"status":"ok"}"#))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let remote_plugin_cache_root = codex_home
|
||||
.path()
|
||||
@@ -225,6 +231,11 @@ async fn plugin_uninstall_writes_remote_plugin_to_cloud_when_remote_plugin_enabl
|
||||
remote_plugin_cache_root.join("1.0.0/.codex-plugin/plugin.json"),
|
||||
r#"{"name":"linear","version":"1.0.0"}"#,
|
||||
)?;
|
||||
std::fs::create_dir_all(remote_plugin_cache_root.join("1.0.0/skills/plan-work"))?;
|
||||
std::fs::write(
|
||||
remote_plugin_cache_root.join("1.0.0/skills/plan-work/SKILL.md"),
|
||||
"---\nname: plan-work\ndescription: Plan work\n---\n",
|
||||
)?;
|
||||
let legacy_remote_plugin_cache_root = codex_home.path().join(format!(
|
||||
"plugins/cache/openai-curated-remote/{REMOTE_PLUGIN_ID}"
|
||||
));
|
||||
@@ -233,6 +244,10 @@ async fn plugin_uninstall_writes_remote_plugin_to_cloud_when_remote_plugin_enabl
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
// Simulate a background remote-cache refresh removing the local bundle
|
||||
// before the uninstall request captures its telemetry metadata.
|
||||
std::fs::remove_dir_all(remote_plugin_cache_root.join("1.0.0"))?;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_uninstall_request(PluginUninstallParams {
|
||||
plugin_id: REMOTE_PLUGIN_ID.to_string(),
|
||||
@@ -255,6 +270,25 @@ async fn plugin_uninstall_writes_remote_plugin_to_cloud_when_remote_plugin_enabl
|
||||
.await?;
|
||||
assert!(!remote_plugin_cache_root.exists());
|
||||
assert!(!legacy_remote_plugin_cache_root.exists());
|
||||
let payload = wait_for_plugin_analytics_payload(&server).await?;
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({
|
||||
"events": [{
|
||||
"event_type": "codex_plugin_uninstalled",
|
||||
"event_params": {
|
||||
"plugin_id": "linear@openai-curated-remote",
|
||||
"remote_plugin_id": REMOTE_PLUGIN_ID,
|
||||
"plugin_name": "linear",
|
||||
"marketplace_name": "openai-curated-remote",
|
||||
"has_skills": true,
|
||||
"mcp_server_count": 0,
|
||||
"connector_ids": [],
|
||||
"product_client_id": DEFAULT_CLIENT_NAME,
|
||||
}
|
||||
}]
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -638,7 +672,11 @@ async fn mount_remote_plugin_detail_with_name(
|
||||
"interface": {{
|
||||
"short_description": "Plan and track work"
|
||||
}},
|
||||
"skills": []
|
||||
"skills": [{{
|
||||
"name": "plan-work",
|
||||
"description": "Plan work",
|
||||
"interface": null
|
||||
}}]
|
||||
}}
|
||||
}}"#
|
||||
);
|
||||
@@ -652,6 +690,29 @@ async fn mount_remote_plugin_detail_with_name(
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn wait_for_plugin_analytics_payload(server: &MockServer) -> Result<serde_json::Value> {
|
||||
timeout(DEFAULT_TIMEOUT, async {
|
||||
loop {
|
||||
let Some(requests) = server.received_requests().await else {
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
continue;
|
||||
};
|
||||
if let Some(request) = requests.iter().find(|request| {
|
||||
request.method == "POST"
|
||||
&& request
|
||||
.url
|
||||
.path()
|
||||
.ends_with("/codex/analytics-events/events")
|
||||
}) {
|
||||
return serde_json::from_slice(&request.body)
|
||||
.map_err(|err| anyhow::anyhow!("invalid analytics payload: {err}"));
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
}
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn wait_for_remote_plugin_request_count(
|
||||
server: &MockServer,
|
||||
method_name: &str,
|
||||
|
||||
@@ -100,6 +100,7 @@ const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
const TEST_ORIGINATOR: &str = "codex_vscode";
|
||||
const LOCAL_PRAGMATIC_TEMPLATE: &str = "You are a deeply pragmatic, effective software engineer.";
|
||||
const MULTI_AGENT_V2_NAMESPACE: &str = "collaboration";
|
||||
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
|
||||
const TINY_PNG_BYTES: &[u8] = &[
|
||||
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0,
|
||||
@@ -3650,7 +3651,12 @@ async fn direct_input_to_multi_agent_v2_subagent_is_rejected() -> Result<()> {
|
||||
|req: &wiremock::Request| body_contains(req, PARENT_PROMPT),
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-parent-1"),
|
||||
responses::ev_function_call(SPAWN_CALL_ID, "spawn_agent", &spawn_args),
|
||||
responses::ev_function_call_with_namespace(
|
||||
SPAWN_CALL_ID,
|
||||
MULTI_AGENT_V2_NAMESPACE,
|
||||
"spawn_agent",
|
||||
&spawn_args,
|
||||
),
|
||||
responses::ev_completed("resp-parent-1"),
|
||||
]),
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ mod seatbelt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
@@ -16,6 +17,8 @@ use codex_core::exec_env::create_env;
|
||||
use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
|
||||
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::SandboxEnforcement;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_sandboxing::landlock::allow_network_for_proxy;
|
||||
use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_permission_profile;
|
||||
@@ -45,6 +48,7 @@ pub async fn run_command_under_seatbelt(
|
||||
loader_overrides: LoaderOverrides,
|
||||
) -> anyhow::Result<()> {
|
||||
let SeatbeltCommand {
|
||||
sandbox_state,
|
||||
permissions_profile,
|
||||
config_profile: _,
|
||||
cwd,
|
||||
@@ -60,6 +64,7 @@ pub async fn run_command_under_seatbelt(
|
||||
);
|
||||
run_command_under_sandbox(
|
||||
DebugSandboxConfigOptions {
|
||||
sandbox_state,
|
||||
permissions_profile,
|
||||
cwd,
|
||||
managed_requirements_mode,
|
||||
@@ -90,6 +95,7 @@ pub async fn run_command_under_landlock(
|
||||
loader_overrides: LoaderOverrides,
|
||||
) -> anyhow::Result<()> {
|
||||
let LandlockCommand {
|
||||
sandbox_state,
|
||||
permissions_profile,
|
||||
config_profile: _,
|
||||
cwd,
|
||||
@@ -103,6 +109,7 @@ pub async fn run_command_under_landlock(
|
||||
);
|
||||
run_command_under_sandbox(
|
||||
DebugSandboxConfigOptions {
|
||||
sandbox_state,
|
||||
permissions_profile,
|
||||
cwd,
|
||||
managed_requirements_mode,
|
||||
@@ -124,6 +131,7 @@ pub async fn run_command_under_windows_sandbox(
|
||||
loader_overrides: LoaderOverrides,
|
||||
) -> anyhow::Result<()> {
|
||||
let WindowsCommand {
|
||||
sandbox_state,
|
||||
permissions_profile,
|
||||
config_profile: _,
|
||||
cwd,
|
||||
@@ -137,6 +145,7 @@ pub async fn run_command_under_windows_sandbox(
|
||||
);
|
||||
run_command_under_sandbox(
|
||||
DebugSandboxConfigOptions {
|
||||
sandbox_state,
|
||||
permissions_profile,
|
||||
cwd,
|
||||
managed_requirements_mode,
|
||||
@@ -161,6 +170,7 @@ enum SandboxType {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DebugSandboxConfigOptions {
|
||||
sandbox_state: crate::SandboxStateArgs,
|
||||
permissions_profile: Option<String>,
|
||||
cwd: Option<PathBuf>,
|
||||
managed_requirements_mode: ManagedRequirementsMode,
|
||||
@@ -187,7 +197,7 @@ impl ManagedRequirementsMode {
|
||||
}
|
||||
|
||||
async fn run_command_under_sandbox(
|
||||
config_options: DebugSandboxConfigOptions,
|
||||
mut config_options: DebugSandboxConfigOptions,
|
||||
command: Vec<String>,
|
||||
config_overrides: CliConfigOverrides,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
@@ -196,6 +206,34 @@ async fn run_command_under_sandbox(
|
||||
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
|
||||
allow_unix_sockets: &[AbsolutePathBuf],
|
||||
) -> anyhow::Result<()> {
|
||||
let sandbox_state = config_options
|
||||
.sandbox_state
|
||||
.sandbox_state_json
|
||||
.as_deref()
|
||||
.map(serde_json::from_str::<codex_mcp::SandboxState>)
|
||||
.transpose()
|
||||
.map_err(|err| anyhow::anyhow!("invalid --sandbox-state-json value: {err}"))?;
|
||||
let sandbox_state_readable_root = config_options
|
||||
.sandbox_state
|
||||
.sandbox_state_readable_root
|
||||
.clone();
|
||||
let sandbox_state_disable_network = config_options.sandbox_state.sandbox_state_disable_network;
|
||||
let codex_linux_sandbox_exe = match sandbox_state.as_ref() {
|
||||
Some(state) => {
|
||||
config_options.cwd = Some(
|
||||
state
|
||||
.sandbox_cwd
|
||||
.to_abs_path()
|
||||
.context("sandbox state cwd is not native to this host")?
|
||||
.to_path_buf(),
|
||||
);
|
||||
state
|
||||
.codex_linux_sandbox_exe
|
||||
.clone()
|
||||
.or(codex_linux_sandbox_exe)
|
||||
}
|
||||
None => codex_linux_sandbox_exe,
|
||||
};
|
||||
let config = load_debug_sandbox_config(
|
||||
config_overrides
|
||||
.parse_overrides()
|
||||
@@ -220,12 +258,75 @@ async fn run_command_under_sandbox(
|
||||
&config.permissions.shell_environment_policy,
|
||||
/*thread_id*/ None,
|
||||
);
|
||||
let mut permission_profile = match sandbox_state.as_ref() {
|
||||
Some(state) => match &state.permission_profile {
|
||||
PermissionProfile::External { .. } => {
|
||||
// `External` only says that the producer relies on an outer sandbox; it does not
|
||||
// include filesystem permissions we can recreate here. The consumer may not share
|
||||
// that sandbox, so use a locally enforceable read-only profile instead of spawning
|
||||
// without a sandbox.
|
||||
PermissionProfile::read_only()
|
||||
}
|
||||
permission_profile => permission_profile.clone(),
|
||||
},
|
||||
None => config.permissions.effective_permission_profile(),
|
||||
};
|
||||
if matches!(permission_profile, PermissionProfile::Disabled) && sandbox_state_disable_network {
|
||||
anyhow::bail!(
|
||||
"--sandbox-state-disable-network cannot be applied to a disabled permission profile"
|
||||
);
|
||||
}
|
||||
if !matches!(permission_profile, PermissionProfile::Disabled)
|
||||
&& (!sandbox_state_readable_root.is_empty() || sandbox_state_disable_network)
|
||||
{
|
||||
let file_system = permission_profile
|
||||
.file_system_sandbox_policy()
|
||||
.with_additional_readable_roots(&cwd, &sandbox_state_readable_root);
|
||||
let network = if sandbox_state_disable_network {
|
||||
NetworkSandboxPolicy::Restricted
|
||||
} else {
|
||||
permission_profile.network_sandbox_policy()
|
||||
};
|
||||
permission_profile = PermissionProfile::from_runtime_permissions(&file_system, network);
|
||||
}
|
||||
let use_legacy_landlock = sandbox_state.as_ref().map_or_else(
|
||||
|| config.features.use_legacy_landlock(),
|
||||
|state| state.use_legacy_landlock,
|
||||
);
|
||||
|
||||
match permission_profile.enforcement() {
|
||||
SandboxEnforcement::Managed => {}
|
||||
SandboxEnforcement::Disabled | SandboxEnforcement::External => {
|
||||
let (program, args) = command
|
||||
.split_first()
|
||||
.context("sandbox command must not be empty")?;
|
||||
let mut child = spawn_debug_sandbox_child(
|
||||
PathBuf::from(program),
|
||||
args.to_vec(),
|
||||
/*arg0*/ None,
|
||||
cwd.to_path_buf(),
|
||||
permission_profile.network_sandbox_policy(),
|
||||
env,
|
||||
|_| {},
|
||||
)
|
||||
.await?;
|
||||
handle_exit_status(child.wait().await?);
|
||||
}
|
||||
}
|
||||
|
||||
// Special-case Windows sandbox: execute and exit the process to emulate inherited stdio.
|
||||
if let SandboxType::Windows = sandbox_type {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
run_command_under_windows_session(&config, command, cwd, workspace_roots, env).await;
|
||||
run_command_under_windows_session(
|
||||
&config,
|
||||
&permission_profile,
|
||||
command,
|
||||
cwd,
|
||||
workspace_roots,
|
||||
env,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
@@ -244,7 +345,7 @@ async fn run_command_under_sandbox(
|
||||
let network_proxy = match config.permissions.network.as_ref() {
|
||||
Some(spec) => Some(
|
||||
spec.start_proxy(
|
||||
config.permissions.permission_profile(),
|
||||
&permission_profile,
|
||||
/*policy_decider*/ None,
|
||||
/*blocked_request_observer*/ None,
|
||||
managed_network_requirements_enabled,
|
||||
@@ -266,7 +367,7 @@ async fn run_command_under_sandbox(
|
||||
None => None,
|
||||
};
|
||||
let runtime_permission_profile = with_managed_mitm_ca_readable_root(
|
||||
config.permissions.effective_permission_profile(),
|
||||
permission_profile,
|
||||
managed_mitm_ca_trust_bundle_path.as_ref(),
|
||||
sandbox_policy_cwd.as_path(),
|
||||
);
|
||||
@@ -282,6 +383,7 @@ async fn run_command_under_sandbox(
|
||||
network_sandbox_policy,
|
||||
sandbox_policy_cwd: sandbox_policy_cwd.as_path(),
|
||||
enforce_managed_network,
|
||||
managed_network: None,
|
||||
environment_id: None,
|
||||
network: network.as_ref(),
|
||||
extra_allow_unix_sockets: allow_unix_sockets,
|
||||
@@ -308,7 +410,6 @@ async fn run_command_under_sandbox(
|
||||
let codex_linux_sandbox_exe = config
|
||||
.codex_linux_sandbox_exe
|
||||
.expect("codex-linux-sandbox executable not found");
|
||||
let use_legacy_landlock = config.features.use_legacy_landlock();
|
||||
let network_sandbox_policy = runtime_permission_profile.network_sandbox_policy();
|
||||
let args = create_linux_sandbox_command_args_for_permission_profile(
|
||||
command,
|
||||
@@ -364,6 +465,7 @@ async fn run_command_under_sandbox(
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn run_command_under_windows_session(
|
||||
config: &Config,
|
||||
permission_profile: &PermissionProfile,
|
||||
command: Vec<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
workspace_roots: Vec<AbsolutePathBuf>,
|
||||
@@ -371,19 +473,20 @@ async fn run_command_under_windows_session(
|
||||
) -> ! {
|
||||
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_windows_sandbox::WindowsSandboxProxySettingsMode;
|
||||
use codex_windows_sandbox::WindowsSandboxSessionRequest;
|
||||
use codex_windows_sandbox::spawn_windows_sandbox_session_for_level;
|
||||
|
||||
let permission_profile = config.permissions.effective_permission_profile();
|
||||
let empty_paths: &[AbsolutePathBuf] = &[];
|
||||
let spawned = spawn_windows_sandbox_session_for_level(WindowsSandboxSessionRequest {
|
||||
permission_profile: &permission_profile,
|
||||
permission_profile,
|
||||
workspace_roots: workspace_roots.as_slice(),
|
||||
codex_home: config.codex_home.as_path(),
|
||||
command,
|
||||
cwd: cwd.as_path(),
|
||||
env_map: env,
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(config),
|
||||
proxy_settings_mode: WindowsSandboxProxySettingsMode::Reconcile,
|
||||
proxy_enforced: false,
|
||||
timeout_ms: None,
|
||||
read_roots_override: None,
|
||||
@@ -464,6 +567,7 @@ async fn load_debug_sandbox_config_with_codex_home(
|
||||
strict_config: bool,
|
||||
) -> anyhow::Result<Config> {
|
||||
let DebugSandboxConfigOptions {
|
||||
sandbox_state: _,
|
||||
permissions_profile,
|
||||
cwd,
|
||||
managed_requirements_mode,
|
||||
@@ -649,6 +753,7 @@ mod tests {
|
||||
Vec::new(),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
DebugSandboxConfigOptions {
|
||||
sandbox_state: Default::default(),
|
||||
permissions_profile: None,
|
||||
cwd: None,
|
||||
managed_requirements_mode: ManagedRequirementsMode::Include,
|
||||
@@ -717,6 +822,7 @@ mod tests {
|
||||
Vec::new(),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
DebugSandboxConfigOptions {
|
||||
sandbox_state: Default::default(),
|
||||
permissions_profile: None,
|
||||
cwd: None,
|
||||
managed_requirements_mode: ManagedRequirementsMode::Include,
|
||||
@@ -774,6 +880,7 @@ mod tests {
|
||||
cli_overrides,
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
DebugSandboxConfigOptions {
|
||||
sandbox_state: Default::default(),
|
||||
permissions_profile: None,
|
||||
cwd: None,
|
||||
managed_requirements_mode: ManagedRequirementsMode::Include,
|
||||
@@ -832,6 +939,7 @@ mod tests {
|
||||
Vec::new(),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
DebugSandboxConfigOptions {
|
||||
sandbox_state: Default::default(),
|
||||
permissions_profile: None,
|
||||
cwd: None,
|
||||
managed_requirements_mode: ManagedRequirementsMode::Include,
|
||||
@@ -859,6 +967,7 @@ mod tests {
|
||||
Vec::new(),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
DebugSandboxConfigOptions {
|
||||
sandbox_state: Default::default(),
|
||||
permissions_profile: Some(":workspace".to_string()),
|
||||
cwd: None,
|
||||
managed_requirements_mode: ManagedRequirementsMode::Ignore,
|
||||
@@ -898,6 +1007,7 @@ mod tests {
|
||||
Vec::new(),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
DebugSandboxConfigOptions {
|
||||
sandbox_state: Default::default(),
|
||||
permissions_profile: Some("limited-read-test".to_string()),
|
||||
cwd: None,
|
||||
managed_requirements_mode: ManagedRequirementsMode::Ignore,
|
||||
@@ -937,6 +1047,7 @@ mod tests {
|
||||
Vec::new(),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
DebugSandboxConfigOptions {
|
||||
sandbox_state: Default::default(),
|
||||
permissions_profile: Some(":workspace".to_string()),
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
managed_requirements_mode: ManagedRequirementsMode::Ignore,
|
||||
|
||||
+35
-2
@@ -2,6 +2,7 @@ pub(crate) mod debug_sandbox;
|
||||
mod exit_status;
|
||||
pub(crate) mod login;
|
||||
|
||||
use clap::Args;
|
||||
use clap::Parser;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
@@ -21,10 +22,36 @@ pub use login::run_login_with_device_code;
|
||||
pub use login::run_login_with_device_code_fallback_to_browser;
|
||||
pub use login::run_logout;
|
||||
|
||||
#[derive(Debug, Default, Args)]
|
||||
pub struct SandboxStateArgs {
|
||||
/// JSON value from `codex/sandbox-state-meta` to apply directly.
|
||||
#[arg(
|
||||
long = "sandbox-state-json",
|
||||
value_name = "JSON",
|
||||
conflicts_with_all = ["permissions_profile", "cwd", "include_managed_config"]
|
||||
)]
|
||||
pub sandbox_state_json: Option<String>,
|
||||
|
||||
/// Add a readable root to the supplied sandbox state. Repeat for multiple roots.
|
||||
#[arg(
|
||||
long,
|
||||
requires = "sandbox_state_json",
|
||||
value_parser = parse_absolute_path
|
||||
)]
|
||||
pub sandbox_state_readable_root: Vec<AbsolutePathBuf>,
|
||||
|
||||
/// Disable direct network access in the supplied sandbox state.
|
||||
#[arg(long, requires = "sandbox_state_json", default_value_t = false)]
|
||||
pub sandbox_state_disable_network: bool,
|
||||
}
|
||||
|
||||
// These command structs share common sandbox options, but remain separate
|
||||
// because each host backend has a slightly different option surface.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SeatbeltCommand {
|
||||
#[command(flatten)]
|
||||
pub sandbox_state: SandboxStateArgs,
|
||||
|
||||
/// Named permissions profile to apply from the active configuration stack.
|
||||
#[arg(long = "permissions-profile", short = 'P', value_name = "NAME")]
|
||||
pub permissions_profile: Option<String>,
|
||||
@@ -51,7 +78,7 @@ pub struct SeatbeltCommand {
|
||||
pub include_managed_config: bool,
|
||||
|
||||
/// Allow the sandboxed command to bind/connect AF_UNIX sockets rooted at this path. Relative paths are resolved against the current directory. Repeat to allow multiple paths.
|
||||
#[arg(long = "allow-unix-socket", value_parser = parse_allow_unix_socket_path)]
|
||||
#[arg(long = "allow-unix-socket", value_parser = parse_absolute_path)]
|
||||
pub allow_unix_sockets: Vec<AbsolutePathBuf>,
|
||||
|
||||
/// While the command runs, capture macOS sandbox denials via `log stream` and print them after exit
|
||||
@@ -66,13 +93,16 @@ pub struct SeatbeltCommand {
|
||||
pub command: Vec<String>,
|
||||
}
|
||||
|
||||
fn parse_allow_unix_socket_path(raw: &str) -> Result<AbsolutePathBuf, String> {
|
||||
fn parse_absolute_path(raw: &str) -> Result<AbsolutePathBuf, String> {
|
||||
AbsolutePathBuf::relative_to_current_dir(raw)
|
||||
.map_err(|err| format!("invalid path {raw}: {err}"))
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct LandlockCommand {
|
||||
#[command(flatten)]
|
||||
pub sandbox_state: SandboxStateArgs,
|
||||
|
||||
/// Named permissions profile to apply from the active configuration stack.
|
||||
#[arg(long = "permissions-profile", short = 'P', value_name = "NAME")]
|
||||
pub permissions_profile: Option<String>,
|
||||
@@ -108,6 +138,9 @@ pub struct LandlockCommand {
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct WindowsCommand {
|
||||
#[command(flatten)]
|
||||
pub sandbox_state: SandboxStateArgs,
|
||||
|
||||
/// Named permissions profile to apply from the active configuration stack.
|
||||
#[arg(long = "permissions-profile", short = 'P', value_name = "NAME")]
|
||||
pub permissions_profile: Option<String>,
|
||||
|
||||
@@ -28,7 +28,8 @@ pub struct CompactionInput<'a> {
|
||||
pub input: &'a [ResponseItem],
|
||||
#[serde(skip_serializing_if = "str::is_empty")]
|
||||
pub instructions: &'a str,
|
||||
pub tools: Vec<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<Vec<Value>>,
|
||||
pub parallel_tool_calls: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reasoning: Option<Reasoning>,
|
||||
@@ -210,7 +211,8 @@ pub struct ResponsesApiRequest {
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
pub instructions: String,
|
||||
pub input: Vec<ResponseItem>,
|
||||
pub tools: Vec<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<Vec<serde_json::Value>>,
|
||||
pub tool_choice: String,
|
||||
pub parallel_tool_calls: bool,
|
||||
pub reasoning: Option<Reasoning>,
|
||||
@@ -258,7 +260,8 @@ pub struct ResponseCreateWsRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub previous_response_id: Option<String>,
|
||||
pub input: Vec<ResponseItem>,
|
||||
pub tools: Vec<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<Vec<Value>>,
|
||||
pub tool_choice: String,
|
||||
pub parallel_tool_calls: bool,
|
||||
pub reasoning: Option<Reasoning>,
|
||||
|
||||
@@ -838,11 +838,11 @@ mod tests {
|
||||
phase: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}],
|
||||
tools: vec![json!({
|
||||
tools: Some(vec![json!({
|
||||
"type": "function",
|
||||
"name": "lookup",
|
||||
"parameters": {"type": "object"}
|
||||
})],
|
||||
})]),
|
||||
tool_choice: "auto".to_string(),
|
||||
parallel_tool_calls: true,
|
||||
reasoning: None,
|
||||
|
||||
@@ -315,7 +315,7 @@ async fn responses_client_stream_request_preserves_item_ids() -> Result<()> {
|
||||
phase: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}],
|
||||
tools: Vec::new(),
|
||||
tools: Some(Vec::new()),
|
||||
tool_choice: "auto".into(),
|
||||
parallel_tool_calls: false,
|
||||
reasoning: None,
|
||||
@@ -401,7 +401,7 @@ async fn streaming_client_retries_on_transport_error() -> Result<()> {
|
||||
model: "gpt-test".into(),
|
||||
instructions: "Say hi".into(),
|
||||
input: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
tools: Some(Vec::new()),
|
||||
tool_choice: "auto".into(),
|
||||
parallel_tool_calls: false,
|
||||
reasoning: None,
|
||||
@@ -520,7 +520,7 @@ async fn azure_store_sends_ids_and_headers() -> Result<()> {
|
||||
phase: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}],
|
||||
tools: Vec::new(),
|
||||
tools: Some(Vec::new()),
|
||||
tool_choice: "auto".into(),
|
||||
parallel_tool_calls: false,
|
||||
reasoning: None,
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::runtime::emit_duration;
|
||||
use crate::tools::MCP_TOOLS_CACHE_WRITE_DURATION_METRIC;
|
||||
use crate::tools::ToolInfo;
|
||||
@@ -66,15 +65,7 @@ pub(crate) enum CachedCodexAppsToolsLoad {
|
||||
Invalid,
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_codex_apps_tool_title(
|
||||
server_name: &str,
|
||||
connector_name: Option<&str>,
|
||||
value: &str,
|
||||
) -> String {
|
||||
if server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return value.to_string();
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_codex_apps_tool_title(connector_name: Option<&str>, value: &str) -> String {
|
||||
let Some(connector_name) = connector_name
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty())
|
||||
@@ -93,15 +84,10 @@ pub(crate) fn normalize_codex_apps_tool_title(
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_codex_apps_callable_name(
|
||||
server_name: &str,
|
||||
tool_name: &str,
|
||||
connector_id: Option<&str>,
|
||||
connector_name: Option<&str>,
|
||||
) -> String {
|
||||
if server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return tool_name.to_string();
|
||||
}
|
||||
|
||||
let tool_name = sanitize_name(tool_name);
|
||||
|
||||
if let Some(connector_name) = connector_name
|
||||
@@ -131,25 +117,18 @@ pub(crate) fn normalize_codex_apps_callable_namespace(
|
||||
server_name: &str,
|
||||
connector_name: Option<&str>,
|
||||
) -> String {
|
||||
if server_name == CODEX_APPS_MCP_SERVER_NAME
|
||||
&& let Some(connector_name) = connector_name
|
||||
{
|
||||
if let Some(connector_name) = connector_name {
|
||||
format!("{}__{}", server_name, sanitize_name(connector_name))
|
||||
} else {
|
||||
server_name.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn write_cached_codex_apps_tools_if_needed(
|
||||
server_name: &str,
|
||||
pub(crate) fn write_codex_apps_tools_cache(
|
||||
cache_context: Option<&CodexAppsToolsCacheContext>,
|
||||
server_info: &McpServerInfo,
|
||||
tools: &[ToolInfo],
|
||||
) {
|
||||
if server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(cache_context) = cache_context {
|
||||
let cache_write_start = Instant::now();
|
||||
write_cached_codex_apps_tools(cache_context, tools);
|
||||
@@ -165,13 +144,8 @@ pub(crate) fn write_cached_codex_apps_tools_if_needed(
|
||||
}
|
||||
|
||||
pub(crate) fn load_startup_cached_codex_apps_tools_snapshot(
|
||||
server_name: &str,
|
||||
cache_context: Option<&CodexAppsToolsCacheContext>,
|
||||
) -> Option<Vec<ToolInfo>> {
|
||||
if server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cache_context = cache_context?;
|
||||
|
||||
match load_cached_codex_apps_tools(cache_context) {
|
||||
@@ -181,13 +155,8 @@ pub(crate) fn load_startup_cached_codex_apps_tools_snapshot(
|
||||
}
|
||||
|
||||
pub(crate) fn load_startup_cached_codex_apps_server_info(
|
||||
server_name: &str,
|
||||
cache_context: Option<&CodexAppsToolsCacheContext>,
|
||||
) -> Option<McpServerInfo> {
|
||||
if server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return None;
|
||||
}
|
||||
|
||||
load_cached_codex_apps_server_info(cache_context?)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//! `codex-core`.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -16,7 +17,7 @@ use std::time::Instant;
|
||||
use crate::McpAuthStatusEntry;
|
||||
use crate::codex_apps::CodexAppsToolsCacheContext;
|
||||
use crate::codex_apps::CodexAppsToolsCacheKey;
|
||||
use crate::codex_apps::write_cached_codex_apps_tools_if_needed;
|
||||
use crate::codex_apps::write_codex_apps_tools_cache;
|
||||
use crate::elicitation::ElicitationRequestManager;
|
||||
use crate::elicitation::ElicitationReviewerHandle;
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
@@ -40,6 +41,7 @@ use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use async_channel::Sender;
|
||||
use codex_api::SharedAuthProvider;
|
||||
use codex_config::Constrained;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_config::types::AuthKeyringBackendKind;
|
||||
@@ -109,7 +111,6 @@ pub struct McpConnectionManager {
|
||||
server_metadata: HashMap<String, McpServerMetadata>,
|
||||
required_servers: Vec<String>,
|
||||
tool_plugin_provenance: Arc<ToolPluginProvenance>,
|
||||
host_owned_codex_apps_enabled: bool,
|
||||
prefix_mcp_tool_names: bool,
|
||||
elicitation_requests: ElicitationRequestManager,
|
||||
startup_cancellation_token: CancellationToken,
|
||||
@@ -130,7 +131,6 @@ impl McpConnectionManager {
|
||||
runtime_context: McpRuntimeContext,
|
||||
codex_home: PathBuf,
|
||||
codex_apps_tools_cache_key: CodexAppsToolsCacheKey,
|
||||
host_owned_codex_apps_enabled: bool,
|
||||
prefix_mcp_tool_names: bool,
|
||||
client_elicitation_capability: ElicitationCapability,
|
||||
supports_openai_form_elicitation: bool,
|
||||
@@ -173,29 +173,16 @@ impl McpConnectionManager {
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let codex_apps_tools_cache_context = if server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
Some(CodexAppsToolsCacheContext {
|
||||
codex_home: codex_home.clone(),
|
||||
user_key: codex_apps_tools_cache_key.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let uses_env_bearer_token =
|
||||
server
|
||||
.configured_config()
|
||||
.is_some_and(|config| match &config.transport {
|
||||
McpServerTransportConfig::StreamableHttp {
|
||||
bearer_token_env_var,
|
||||
..
|
||||
} => bearer_token_env_var.is_some(),
|
||||
McpServerTransportConfig::Stdio { .. } => false,
|
||||
});
|
||||
let runtime_auth_provider =
|
||||
if server_name == CODEX_APPS_MCP_SERVER_NAME && !uses_env_bearer_token {
|
||||
codex_apps_auth_provider.clone()
|
||||
let (codex_apps_tools_cache_context, runtime_auth_provider) =
|
||||
if server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
codex_apps_cache_context_and_auth_provider(
|
||||
&server,
|
||||
&codex_home,
|
||||
&codex_apps_tools_cache_key,
|
||||
codex_apps_auth_provider.clone(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
regular_mcp_cache_context_and_auth_provider()
|
||||
};
|
||||
let async_managed_client = AsyncManagedClient::new(
|
||||
server_name.clone(),
|
||||
@@ -252,7 +239,6 @@ impl McpConnectionManager {
|
||||
server_metadata,
|
||||
required_servers,
|
||||
tool_plugin_provenance,
|
||||
host_owned_codex_apps_enabled,
|
||||
prefix_mcp_tool_names,
|
||||
elicitation_requests: elicitation_requests.clone(),
|
||||
startup_cancellation_token: startup_cancellation_token.clone(),
|
||||
@@ -338,7 +324,6 @@ impl McpConnectionManager {
|
||||
server_metadata: HashMap::new(),
|
||||
required_servers: Vec::new(),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
host_owned_codex_apps_enabled: false,
|
||||
prefix_mcp_tool_names,
|
||||
elicitation_requests: ElicitationRequestManager::new(
|
||||
approval_policy.value(),
|
||||
@@ -360,8 +345,15 @@ impl McpConnectionManager {
|
||||
/// Stop all MCP clients owned by this manager and terminate stdio server processes.
|
||||
pub async fn shutdown(&self) {
|
||||
self.startup_cancellation_token.cancel();
|
||||
for client in self.clients.values() {
|
||||
client.shutdown().await;
|
||||
let clients = self.clients.values().cloned().collect::<Vec<_>>();
|
||||
// Keep cleanup alive if an interrupt cancels the refresh that requested it.
|
||||
let shutdown_task = tokio::spawn(async move {
|
||||
for client in clients {
|
||||
client.shutdown().await;
|
||||
}
|
||||
});
|
||||
if let Err(error) = shutdown_task.await {
|
||||
warn!("MCP client shutdown task failed: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,7 +398,7 @@ impl McpConnectionManager {
|
||||
}
|
||||
|
||||
pub fn is_host_owned_codex_apps_server(&self, server_name: &str) -> bool {
|
||||
self.host_owned_codex_apps_enabled && server_name == CODEX_APPS_MCP_SERVER_NAME
|
||||
server_name == CODEX_APPS_MCP_SERVER_NAME && self.server_metadata.contains_key(server_name)
|
||||
}
|
||||
|
||||
pub fn set_approval_policy(&self, approval_policy: &Constrained<AskForApproval>) {
|
||||
@@ -510,6 +502,7 @@ impl McpConnectionManager {
|
||||
let fetch_start = Instant::now();
|
||||
let tools = list_tools_for_client_uncached(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
/*is_codex_apps_mcp_server*/ true,
|
||||
&managed_client.client,
|
||||
managed_client.tool_timeout,
|
||||
managed_client.server_instructions.as_deref(),
|
||||
@@ -524,8 +517,7 @@ impl McpConnectionManager {
|
||||
&[],
|
||||
);
|
||||
|
||||
write_cached_codex_apps_tools_if_needed(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
write_codex_apps_tools_cache(
|
||||
managed_client.codex_apps_tools_cache_context.as_ref(),
|
||||
&managed_client.server_info,
|
||||
&tools,
|
||||
@@ -860,6 +852,50 @@ impl Drop for McpConnectionManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the host-owned state used only by the Codex Apps server.
|
||||
///
|
||||
/// The tools cache is scoped to the authenticated user. Runtime authentication is supplied only
|
||||
/// when the server is not already configured to read a bearer token from the environment.
|
||||
fn codex_apps_cache_context_and_auth_provider(
|
||||
server: &EffectiveMcpServer,
|
||||
codex_home: &Path,
|
||||
codex_apps_tools_cache_key: &CodexAppsToolsCacheKey,
|
||||
codex_apps_auth_provider: Option<SharedAuthProvider>,
|
||||
) -> (
|
||||
Option<CodexAppsToolsCacheContext>,
|
||||
Option<SharedAuthProvider>,
|
||||
) {
|
||||
let uses_env_bearer_token =
|
||||
server
|
||||
.configured_config()
|
||||
.is_some_and(|config| match &config.transport {
|
||||
McpServerTransportConfig::StreamableHttp {
|
||||
bearer_token_env_var,
|
||||
..
|
||||
} => bearer_token_env_var.is_some(),
|
||||
McpServerTransportConfig::Stdio { .. } => false,
|
||||
});
|
||||
(
|
||||
Some(CodexAppsToolsCacheContext {
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
user_key: codex_apps_tools_cache_key.clone(),
|
||||
}),
|
||||
if uses_env_bearer_token {
|
||||
None
|
||||
} else {
|
||||
codex_apps_auth_provider
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Keeps regular MCP servers isolated from the host-owned Codex Apps cache and auth provider.
|
||||
fn regular_mcp_cache_context_and_auth_provider() -> (
|
||||
Option<CodexAppsToolsCacheContext>,
|
||||
Option<SharedAuthProvider>,
|
||||
) {
|
||||
(None, None)
|
||||
}
|
||||
|
||||
async fn emit_update(
|
||||
submit_id: &str,
|
||||
tx_event: &Sender<Event>,
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::codex_apps::load_startup_cached_codex_apps_server_info;
|
||||
use crate::codex_apps::load_startup_cached_codex_apps_tools_snapshot;
|
||||
use crate::codex_apps::read_cached_codex_apps_tools;
|
||||
use crate::codex_apps::write_cached_codex_apps_tools;
|
||||
use crate::codex_apps::write_cached_codex_apps_tools_if_needed;
|
||||
use crate::codex_apps::write_codex_apps_tools_cache;
|
||||
use crate::declared_openai_file_input_param_names;
|
||||
use crate::elicitation::ElicitationRequestManager;
|
||||
use crate::elicitation::elicitation_is_rejected_by_policy;
|
||||
@@ -205,9 +205,6 @@ fn tool_with_model_visible_input_schema_leaves_tools_without_file_params_unchang
|
||||
|
||||
#[test]
|
||||
fn elicitation_granular_policy_defaults_to_prompting() {
|
||||
assert!(!elicitation_is_rejected_by_policy(
|
||||
AskForApproval::OnFailure
|
||||
));
|
||||
assert!(!elicitation_is_rejected_by_policy(
|
||||
AskForApproval::OnRequest
|
||||
));
|
||||
@@ -704,22 +701,11 @@ fn startup_cached_codex_apps_tools_loads_from_disk_cache() {
|
||||
"calendar_search",
|
||||
)];
|
||||
let server_info = create_test_server_info("Codex Apps");
|
||||
write_cached_codex_apps_tools_if_needed(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some(&cache_context),
|
||||
&server_info,
|
||||
&cached_tools,
|
||||
);
|
||||
write_codex_apps_tools_cache(Some(&cache_context), &server_info, &cached_tools);
|
||||
|
||||
let startup_tools = load_startup_cached_codex_apps_tools_snapshot(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some(&cache_context),
|
||||
)
|
||||
.expect("expected startup snapshot to load from cache");
|
||||
let cached_server_info = load_startup_cached_codex_apps_server_info(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some(&cache_context),
|
||||
);
|
||||
let startup_tools = load_startup_cached_codex_apps_tools_snapshot(Some(&cache_context))
|
||||
.expect("expected startup snapshot to load from cache");
|
||||
let cached_server_info = load_startup_cached_codex_apps_server_info(Some(&cache_context));
|
||||
|
||||
assert_eq!(startup_tools.len(), 1);
|
||||
assert_eq!(startup_tools[0].server_name, CODEX_APPS_MCP_SERVER_NAME);
|
||||
@@ -746,15 +732,9 @@ fn startup_cached_codex_apps_tools_loads_without_server_info_cache() {
|
||||
.expect("serialize");
|
||||
std::fs::write(cache_path, bytes).expect("write");
|
||||
|
||||
let startup_tools = load_startup_cached_codex_apps_tools_snapshot(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some(&cache_context),
|
||||
)
|
||||
.expect("legacy startup snapshot should remain available");
|
||||
let cached_server_info = load_startup_cached_codex_apps_server_info(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some(&cache_context),
|
||||
);
|
||||
let startup_tools = load_startup_cached_codex_apps_tools_snapshot(Some(&cache_context))
|
||||
.expect("legacy startup snapshot should remain available");
|
||||
let cached_server_info = load_startup_cached_codex_apps_server_info(Some(&cache_context));
|
||||
|
||||
assert_eq!(startup_tools.len(), 1);
|
||||
assert_eq!(startup_tools[0].callable_name, "calendar_search");
|
||||
@@ -770,8 +750,7 @@ fn codex_apps_server_info_cache_survives_legacy_tools_cache_write() {
|
||||
Some("user-one"),
|
||||
);
|
||||
let server_info = create_test_server_info("Codex Apps");
|
||||
write_cached_codex_apps_tools_if_needed(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
write_codex_apps_tools_cache(
|
||||
Some(&cache_context),
|
||||
&server_info,
|
||||
&[create_test_tool(
|
||||
@@ -792,19 +771,10 @@ fn codex_apps_server_info_cache_survives_legacy_tools_cache_write() {
|
||||
std::fs::write(cache_path, bytes).expect("write legacy tools cache");
|
||||
|
||||
assert_eq!(
|
||||
load_startup_cached_codex_apps_server_info(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some(&cache_context),
|
||||
),
|
||||
load_startup_cached_codex_apps_server_info(Some(&cache_context)),
|
||||
Some(server_info)
|
||||
);
|
||||
assert!(
|
||||
load_startup_cached_codex_apps_tools_snapshot(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some(&cache_context),
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert!(load_startup_cached_codex_apps_tools_snapshot(Some(&cache_context)).is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -816,7 +786,7 @@ async fn list_all_tools_uses_cached_tool_info_snapshot_while_client_is_pending()
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
@@ -827,6 +797,7 @@ async fn list_all_tools_uses_cached_tool_info_snapshot_while_client_is_pending()
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
cached_tool_info_snapshot: Some(startup_tools),
|
||||
cached_server_info: None,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
@@ -852,7 +823,7 @@ async fn list_available_server_infos_uses_cache_while_client_is_pending() {
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
@@ -864,6 +835,7 @@ async fn list_available_server_infos_uses_cache_while_client_is_pending() {
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
cached_tool_info_snapshot: Some(Vec::new()),
|
||||
cached_server_info: Some(server_info.clone()),
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
@@ -890,7 +862,7 @@ async fn list_all_tools_accepts_canonical_namespaced_tool_names() {
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
@@ -901,6 +873,7 @@ async fn list_all_tools_accepts_canonical_namespaced_tool_names() {
|
||||
"rmcp".to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: false,
|
||||
cached_tool_info_snapshot: Some(startup_tools),
|
||||
cached_server_info: None,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
@@ -933,7 +906,7 @@ async fn list_all_tools_applies_legacy_mcp_prefix_by_default() {
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
@@ -944,6 +917,7 @@ async fn list_all_tools_applies_legacy_mcp_prefix_by_default() {
|
||||
"rmcp".to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: false,
|
||||
cached_tool_info_snapshot: Some(startup_tools),
|
||||
cached_server_info: None,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
@@ -975,7 +949,7 @@ async fn list_all_tools_blocks_while_client_is_pending_without_cached_tool_info_
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
@@ -986,6 +960,7 @@ async fn list_all_tools_blocks_while_client_is_pending_without_cached_tool_info_
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
cached_tool_info_snapshot: None,
|
||||
cached_server_info: None,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
@@ -1011,7 +986,7 @@ async fn shutdown_cancels_pending_tool_listing() {
|
||||
}
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
@@ -1022,6 +997,7 @@ async fn shutdown_cancels_pending_tool_listing() {
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
cached_tool_info_snapshot: None,
|
||||
cached_server_info: None,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
@@ -1042,10 +1018,19 @@ async fn shutdown_cancels_pending_tool_listing() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_all_tools_does_not_block_when_cached_tool_info_snapshot_is_empty() {
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
async fn shutdown_continues_after_caller_is_aborted() {
|
||||
let (started_tx, started_rx) = tokio::sync::oneshot::channel();
|
||||
let (completed_tx, completed_rx) = tokio::sync::oneshot::channel();
|
||||
let release = Arc::new(tokio::sync::Notify::new());
|
||||
let release_for_client = Arc::clone(&release);
|
||||
let blocking_client = async move {
|
||||
let _ = started_tx.send(());
|
||||
release_for_client.notified().await;
|
||||
let _ = completed_tx.send(());
|
||||
Err(StartupOutcomeError::Cancelled)
|
||||
}
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
@@ -1053,10 +1038,55 @@ async fn list_all_tools_does_not_block_when_cached_tool_info_snapshot_is_empty()
|
||||
&permission_profile,
|
||||
/*prefix_mcp_tool_names*/ true,
|
||||
);
|
||||
manager.clients.insert(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
client: blocking_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
cached_tool_info_snapshot: None,
|
||||
cached_server_info: None,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
},
|
||||
);
|
||||
let manager = Arc::new(manager);
|
||||
let shutdown_task = tokio::spawn({
|
||||
let manager = Arc::clone(&manager);
|
||||
async move { manager.shutdown().await }
|
||||
});
|
||||
|
||||
started_rx.await.expect("client shutdown should start");
|
||||
shutdown_task.abort();
|
||||
let shutdown_error = shutdown_task
|
||||
.await
|
||||
.expect_err("caller shutdown task should be aborted");
|
||||
assert!(shutdown_error.is_cancelled());
|
||||
release.notify_one();
|
||||
|
||||
tokio::time::timeout(Duration::from_secs(1), completed_rx)
|
||||
.await
|
||||
.expect("client shutdown should survive caller cancellation")
|
||||
.expect("client shutdown completion sender should stay alive");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_all_tools_does_not_block_when_cached_tool_info_snapshot_is_empty() {
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
&permission_profile,
|
||||
/*prefix_mcp_tool_names*/ true,
|
||||
);
|
||||
manager.clients.insert(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
cached_tool_info_snapshot: Some(Vec::new()),
|
||||
cached_server_info: None,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
@@ -1085,7 +1115,7 @@ async fn list_all_tools_uses_cached_tool_info_snapshot_when_client_startup_fails
|
||||
))
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
@@ -1097,6 +1127,7 @@ async fn list_all_tools_uses_cached_tool_info_snapshot_when_client_startup_fails
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
client: failed_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
cached_tool_info_snapshot: Some(startup_tools),
|
||||
cached_server_info: Some(server_info.clone()),
|
||||
startup_complete,
|
||||
@@ -1131,7 +1162,7 @@ async fn list_all_tools_adds_server_metadata_to_cached_tools() {
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
@@ -1155,6 +1186,7 @@ async fn list_all_tools_adds_server_metadata_to_cached_tools() {
|
||||
server_name.to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
is_codex_apps_mcp_server: false,
|
||||
cached_tool_info_snapshot: Some(startup_tools),
|
||||
cached_server_info: None,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
@@ -1195,9 +1227,44 @@ fn server_metadata_preserves_tool_approval_policy() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_owned_codex_apps_requires_server_metadata() {
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
&permission_profile,
|
||||
/*prefix_mcp_tool_names*/ true,
|
||||
);
|
||||
|
||||
assert!(!manager.is_host_owned_codex_apps_server(CODEX_APPS_MCP_SERVER_NAME));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_owned_codex_apps_matches_reserved_name_with_server_metadata() {
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
&permission_profile,
|
||||
/*prefix_mcp_tool_names*/ true,
|
||||
);
|
||||
let server = EffectiveMcpServer::configured(crate::codex_apps_mcp_server_config(
|
||||
"https://chatgpt.com",
|
||||
/*apps_mcp_product_sku*/ None,
|
||||
));
|
||||
manager.server_metadata.insert(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
McpServerMetadata::from(&server),
|
||||
);
|
||||
|
||||
assert!(manager.is_host_owned_codex_apps_server(CODEX_APPS_MCP_SERVER_NAME));
|
||||
assert!(!manager.is_host_owned_codex_apps_server("docs"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let (tx_event, rx_event) = async_channel::unbounded();
|
||||
drop(rx_event);
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
@@ -1276,7 +1343,6 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
|
||||
chatgpt_user_id: None,
|
||||
is_workspace_account: false,
|
||||
},
|
||||
/*host_owned_codex_apps_enabled*/ false,
|
||||
/*prefix_mcp_tool_names*/ true,
|
||||
ElicitationCapability::default(),
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
|
||||
@@ -247,7 +247,6 @@ impl ElicitationRequestManager {
|
||||
pub(crate) fn elicitation_is_rejected_by_policy(approval_policy: AskForApproval) -> bool {
|
||||
match approval_policy {
|
||||
AskForApproval::Never => true,
|
||||
AskForApproval::OnFailure => false,
|
||||
AskForApproval::OnRequest => false,
|
||||
AskForApproval::UnlessTrusted => false,
|
||||
AskForApproval::Granular(granular_config) => !granular_config.allows_mcp_elicitations(),
|
||||
|
||||
@@ -10,6 +10,7 @@ use codex_protocol::protocol::McpAuthStatus;
|
||||
use codex_rmcp_client::OAuthProviderError;
|
||||
use codex_rmcp_client::determine_streamable_http_auth_status;
|
||||
use codex_rmcp_client::discover_streamable_http_oauth;
|
||||
use futures::FutureExt;
|
||||
use futures::future::join_all;
|
||||
use tracing::warn;
|
||||
|
||||
@@ -217,6 +218,7 @@ async fn compute_auth_status(
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
)
|
||||
.boxed()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,7 +277,6 @@ pub async fn read_mcp_resource(
|
||||
uri: &str,
|
||||
) -> anyhow::Result<ReadResourceResult> {
|
||||
let mut mcp_servers = effective_mcp_servers(config, auth);
|
||||
let host_owned_codex_apps_enabled = host_owned_codex_apps_enabled(config, auth);
|
||||
mcp_servers.retain(|name, _| name == server);
|
||||
let auth_statuses = compute_auth_statuses(
|
||||
mcp_servers.iter(),
|
||||
@@ -302,7 +301,6 @@ pub async fn read_mcp_resource(
|
||||
runtime_context,
|
||||
config.codex_home.clone(),
|
||||
codex_apps_tools_cache_key(auth),
|
||||
host_owned_codex_apps_enabled,
|
||||
config.prefix_mcp_tool_names,
|
||||
config.client_elicitation_capability.clone(),
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
@@ -337,7 +335,6 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
detail: McpSnapshotDetail,
|
||||
) -> McpServerStatusSnapshot {
|
||||
let mcp_servers = effective_mcp_servers(config, auth);
|
||||
let host_owned_codex_apps_enabled = host_owned_codex_apps_enabled(config, auth);
|
||||
let tool_plugin_provenance = tool_plugin_provenance(config);
|
||||
if mcp_servers.is_empty() {
|
||||
return McpServerStatusSnapshot {
|
||||
@@ -377,7 +374,6 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
runtime_context,
|
||||
config.codex_home.clone(),
|
||||
codex_apps_tools_cache_key(auth),
|
||||
host_owned_codex_apps_enabled,
|
||||
config.prefix_mcp_tool_names,
|
||||
config.client_elicitation_capability.clone(),
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
|
||||
@@ -27,7 +27,7 @@ fn test_mcp_config(codex_home: PathBuf) -> McpConfig {
|
||||
mcp_oauth_callback_port: None,
|
||||
mcp_oauth_callback_url: None,
|
||||
skill_mcp_dependency_install_enabled: true,
|
||||
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
|
||||
approval_policy: Constrained::allow_any(AskForApproval::OnRequest),
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
apps_enabled: false,
|
||||
@@ -83,7 +83,6 @@ fn mcp_prompt_auto_approval_honors_unrestricted_managed_profiles() {
|
||||
fn mcp_prompt_auto_approval_honors_approved_tools_in_all_permission_modes() {
|
||||
for approval_policy in [
|
||||
AskForApproval::UnlessTrusted,
|
||||
AskForApproval::OnFailure,
|
||||
AskForApproval::OnRequest,
|
||||
AskForApproval::Granular(GranularApprovalConfig {
|
||||
sandbox_approval: true,
|
||||
|
||||
@@ -25,7 +25,7 @@ use crate::codex_apps::load_startup_cached_codex_apps_tools_snapshot;
|
||||
use crate::codex_apps::normalize_codex_apps_callable_name;
|
||||
use crate::codex_apps::normalize_codex_apps_callable_namespace;
|
||||
use crate::codex_apps::normalize_codex_apps_tool_title;
|
||||
use crate::codex_apps::write_cached_codex_apps_tools_if_needed;
|
||||
use crate::codex_apps::write_codex_apps_tools_cache;
|
||||
use crate::elicitation::ElicitationRequestManager;
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::mcp::ToolPluginProvenance;
|
||||
@@ -55,6 +55,7 @@ use codex_rmcp_client::ExecutorStdioServerLauncher;
|
||||
use codex_rmcp_client::LocalStdioServerLauncher;
|
||||
use codex_rmcp_client::RmcpClient;
|
||||
use codex_rmcp_client::StdioServerLauncher;
|
||||
use codex_rmcp_client::ToolWithConnectorId;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::future::FutureExt;
|
||||
use futures::future::Shared;
|
||||
@@ -129,6 +130,7 @@ impl ManagedClient {
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AsyncManagedClient {
|
||||
pub(crate) client: Shared<BoxFuture<'static, Result<ManagedClient, StartupOutcomeError>>>,
|
||||
pub(crate) is_codex_apps_mcp_server: bool,
|
||||
pub(crate) cached_tool_info_snapshot: Option<Vec<ToolInfo>>,
|
||||
pub(crate) cached_server_info: Option<McpServerInfo>,
|
||||
pub(crate) startup_complete: Arc<AtomicBool>,
|
||||
@@ -155,20 +157,23 @@ impl AsyncManagedClient {
|
||||
client_elicitation_capability: ElicitationCapability,
|
||||
supports_openai_form_elicitation: bool,
|
||||
) -> Self {
|
||||
let is_codex_apps_mcp_server = server_name == CODEX_APPS_MCP_SERVER_NAME;
|
||||
let tool_filter = server
|
||||
.configured_config()
|
||||
.map(ToolFilter::from_config)
|
||||
.unwrap_or_default();
|
||||
let cached_tool_info_snapshot = load_startup_cached_codex_apps_tools_snapshot(
|
||||
&server_name,
|
||||
codex_apps_tools_cache_context.as_ref(),
|
||||
);
|
||||
let (cached_tool_info_snapshot, cached_server_info) = if is_codex_apps_mcp_server {
|
||||
(
|
||||
load_startup_cached_codex_apps_tools_snapshot(
|
||||
codex_apps_tools_cache_context.as_ref(),
|
||||
),
|
||||
load_startup_cached_codex_apps_server_info(codex_apps_tools_cache_context.as_ref()),
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
let cached_tool_info_snapshot =
|
||||
cached_tool_info_snapshot.map(|tools| filter_tools(tools, &tool_filter));
|
||||
let cached_server_info = load_startup_cached_codex_apps_server_info(
|
||||
&server_name,
|
||||
codex_apps_tools_cache_context.as_ref(),
|
||||
);
|
||||
let startup_tool_filter = tool_filter;
|
||||
let startup_complete = Arc::new(AtomicBool::new(false));
|
||||
let startup_complete_for_fut = Arc::clone(&startup_complete);
|
||||
@@ -194,6 +199,7 @@ impl AsyncManagedClient {
|
||||
server_name,
|
||||
client,
|
||||
StartServerTaskParams {
|
||||
is_codex_apps_mcp_server,
|
||||
startup_timeout: server
|
||||
.configured_config()
|
||||
.and_then(|config| config.startup_timeout_sec)
|
||||
@@ -232,6 +238,7 @@ impl AsyncManagedClient {
|
||||
|
||||
Self {
|
||||
client,
|
||||
is_codex_apps_mcp_server,
|
||||
cached_tool_info_snapshot,
|
||||
cached_server_info,
|
||||
startup_complete,
|
||||
@@ -255,65 +262,7 @@ impl AsyncManagedClient {
|
||||
}
|
||||
}
|
||||
|
||||
fn cached_tool_info_snapshot_while_initializing(&self) -> Option<Vec<ToolInfo>> {
|
||||
if !self.startup_complete.load(Ordering::Acquire) {
|
||||
return self.cached_tool_info_snapshot.clone();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) async fn listed_tools(&self) -> Option<Vec<ToolInfo>> {
|
||||
let annotate_tools = |tools: Vec<ToolInfo>| {
|
||||
let mut tools = tools;
|
||||
for tool in &mut tools {
|
||||
if tool.server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
tool.tool = tool_with_model_visible_input_schema(&tool.tool);
|
||||
}
|
||||
|
||||
let plugin_names = match tool.connector_id.as_deref() {
|
||||
Some(connector_id) => self
|
||||
.tool_plugin_provenance
|
||||
.plugin_display_names_for_connector_id(connector_id),
|
||||
None => self
|
||||
.tool_plugin_provenance
|
||||
.plugin_display_names_for_mcp_server_name(tool.server_name.as_str()),
|
||||
};
|
||||
tool.plugin_display_names = plugin_names.to_vec();
|
||||
|
||||
if plugin_names.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let plugin_source_note = if plugin_names.len() == 1 {
|
||||
format!("This tool is part of plugin `{}`.", plugin_names[0])
|
||||
} else {
|
||||
format!(
|
||||
"This tool is part of plugins {}.",
|
||||
plugin_names
|
||||
.iter()
|
||||
.map(|plugin_name| format!("`{plugin_name}`"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
};
|
||||
let description = tool
|
||||
.tool
|
||||
.description
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.unwrap_or("");
|
||||
let annotated_description = if description.is_empty() {
|
||||
plugin_source_note
|
||||
} else if matches!(description.chars().last(), Some('.' | '!' | '?')) {
|
||||
format!("{description} {plugin_source_note}")
|
||||
} else {
|
||||
format!("{description}. {plugin_source_note}")
|
||||
};
|
||||
tool.tool.description = Some(Cow::Owned(annotated_description));
|
||||
}
|
||||
tools
|
||||
};
|
||||
|
||||
// Keep cache payloads raw; plugin provenance is resolved per-session at read time.
|
||||
let tools = if let Some(startup_tools) = self.cached_tool_info_snapshot_while_initializing()
|
||||
{
|
||||
@@ -323,8 +272,19 @@ impl AsyncManagedClient {
|
||||
Ok(client) => Some(client.listed_tools()),
|
||||
Err(_) => self.cached_tool_info_snapshot.clone(),
|
||||
}
|
||||
};
|
||||
tools.map(annotate_tools)
|
||||
}?;
|
||||
Some(if self.is_codex_apps_mcp_server {
|
||||
prepare_codex_apps_tools_for_model(tools, &self.tool_plugin_provenance)
|
||||
} else {
|
||||
prepare_regular_mcp_tools_for_model(tools, &self.tool_plugin_provenance)
|
||||
})
|
||||
}
|
||||
|
||||
fn cached_tool_info_snapshot_while_initializing(&self) -> Option<Vec<ToolInfo>> {
|
||||
if !self.startup_complete.load(Ordering::Acquire) {
|
||||
return self.cached_tool_info_snapshot.clone();
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,6 +308,7 @@ impl From<anyhow::Error> for StartupOutcomeError {
|
||||
|
||||
pub(crate) async fn list_tools_for_client_uncached(
|
||||
server_name: &str,
|
||||
is_codex_apps_mcp_server: bool,
|
||||
client: &Arc<RmcpClient>,
|
||||
timeout: Option<Duration>,
|
||||
server_instructions: Option<&str>,
|
||||
@@ -359,68 +320,165 @@ pub(crate) async fn list_tools_for_client_uncached(
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| {
|
||||
let mut tool_def = tool.tool;
|
||||
let (connector_id, connector_name, connector_description) =
|
||||
sanitize_tool_connector_metadata(
|
||||
server_name,
|
||||
&mut tool_def,
|
||||
tool.connector_id,
|
||||
tool.connector_name,
|
||||
tool.connector_description,
|
||||
);
|
||||
let callable_name = normalize_codex_apps_callable_name(
|
||||
tool_info_from_listed_tool(
|
||||
server_name,
|
||||
&tool_def.name,
|
||||
connector_id.as_deref(),
|
||||
connector_name.as_deref(),
|
||||
);
|
||||
let callable_namespace =
|
||||
normalize_codex_apps_callable_namespace(server_name, connector_name.as_deref());
|
||||
if let Some(title) = tool_def.title.as_deref() {
|
||||
let normalized_title =
|
||||
normalize_codex_apps_tool_title(server_name, connector_name.as_deref(), title);
|
||||
if tool_def.title.as_deref() != Some(normalized_title.as_str()) {
|
||||
tool_def.title = Some(normalized_title);
|
||||
}
|
||||
}
|
||||
let has_connector_metadata = connector_id.is_some()
|
||||
|| connector_name.is_some()
|
||||
|| connector_description.is_some();
|
||||
let namespace_description = if has_connector_metadata {
|
||||
connector_description
|
||||
} else {
|
||||
server_instructions.map(str::to_string)
|
||||
};
|
||||
ToolInfo {
|
||||
server_name: server_name.to_owned(),
|
||||
supports_parallel_tool_calls: false,
|
||||
server_origin: None,
|
||||
callable_name,
|
||||
callable_namespace,
|
||||
namespace_description,
|
||||
tool: tool_def,
|
||||
connector_id,
|
||||
connector_name,
|
||||
plugin_display_names: Vec::new(),
|
||||
}
|
||||
is_codex_apps_mcp_server,
|
||||
server_instructions,
|
||||
tool,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Ok(tools)
|
||||
}
|
||||
|
||||
fn sanitize_tool_connector_metadata(
|
||||
server_name: &str,
|
||||
tool: &mut RmcpTool,
|
||||
connector_id: Option<String>,
|
||||
connector_name: Option<String>,
|
||||
connector_description: Option<String>,
|
||||
) -> (Option<String>, Option<String>, Option<String>) {
|
||||
if server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
return (connector_id, connector_name, connector_description);
|
||||
/// Presents declared Codex Apps file parameters to the model as local-path inputs and adds plugin
|
||||
/// names to each tool. Plugin membership is resolved by connector ID, falling back to the MCP
|
||||
/// server when absent.
|
||||
fn prepare_codex_apps_tools_for_model(
|
||||
mut tools: Vec<ToolInfo>,
|
||||
tool_plugin_provenance: &ToolPluginProvenance,
|
||||
) -> Vec<ToolInfo> {
|
||||
for tool in &mut tools {
|
||||
tool.tool = tool_with_model_visible_input_schema(&tool.tool);
|
||||
let plugin_names = match tool.connector_id.as_deref() {
|
||||
Some(connector_id) => {
|
||||
tool_plugin_provenance.plugin_display_names_for_connector_id(connector_id)
|
||||
}
|
||||
None => tool_plugin_provenance
|
||||
.plugin_display_names_for_mcp_server_name(tool.server_name.as_str()),
|
||||
};
|
||||
add_plugin_provenance_to_tool(tool, plugin_names);
|
||||
}
|
||||
tools
|
||||
}
|
||||
|
||||
/// Stores plugin names on the tool and appends a model-visible plugin membership note.
|
||||
fn add_plugin_provenance_to_tool(tool: &mut ToolInfo, plugin_names: &[String]) {
|
||||
tool.plugin_display_names = plugin_names.to_vec();
|
||||
if plugin_names.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
strip_untrusted_connector_meta(tool);
|
||||
(None, None, None)
|
||||
let plugin_source_note = if plugin_names.len() == 1 {
|
||||
format!("This tool is part of plugin `{}`.", plugin_names[0])
|
||||
} else {
|
||||
format!(
|
||||
"This tool is part of plugins {}.",
|
||||
plugin_names
|
||||
.iter()
|
||||
.map(|plugin_name| format!("`{plugin_name}`"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
};
|
||||
let description = tool
|
||||
.tool
|
||||
.description
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.unwrap_or("");
|
||||
let annotated_description = if description.is_empty() {
|
||||
plugin_source_note
|
||||
} else if matches!(description.chars().last(), Some('.' | '!' | '?')) {
|
||||
format!("{description} {plugin_source_note}")
|
||||
} else {
|
||||
format!("{description}. {plugin_source_note}")
|
||||
};
|
||||
tool.tool.description = Some(Cow::Owned(annotated_description));
|
||||
}
|
||||
|
||||
/// Adds server-scoped plugin names to regular MCP tools without changing their input schemas.
|
||||
fn prepare_regular_mcp_tools_for_model(
|
||||
mut tools: Vec<ToolInfo>,
|
||||
tool_plugin_provenance: &ToolPluginProvenance,
|
||||
) -> Vec<ToolInfo> {
|
||||
for tool in &mut tools {
|
||||
let plugin_names = tool_plugin_provenance
|
||||
.plugin_display_names_for_mcp_server_name(tool.server_name.as_str());
|
||||
add_plugin_provenance_to_tool(tool, plugin_names);
|
||||
}
|
||||
tools
|
||||
}
|
||||
|
||||
fn tool_info_from_listed_tool(
|
||||
server_name: &str,
|
||||
is_codex_apps_mcp_server: bool,
|
||||
server_instructions: Option<&str>,
|
||||
tool: ToolWithConnectorId,
|
||||
) -> ToolInfo {
|
||||
if is_codex_apps_mcp_server {
|
||||
codex_apps_tool_info_from_listed_tool(server_name, server_instructions, tool)
|
||||
} else {
|
||||
regular_mcp_tool_info_from_listed_tool(server_name, server_instructions, tool)
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a Codex Apps tool by preserving connector fields, removing connector prefixes from
|
||||
/// model-visible names and titles, and using the connector description for its tool namespace.
|
||||
fn codex_apps_tool_info_from_listed_tool(
|
||||
server_name: &str,
|
||||
server_instructions: Option<&str>,
|
||||
tool: ToolWithConnectorId,
|
||||
) -> ToolInfo {
|
||||
let mut tool_def = tool.tool;
|
||||
let connector_id = tool.connector_id;
|
||||
let connector_name = tool.connector_name;
|
||||
let connector_description = tool.connector_description;
|
||||
let callable_name = normalize_codex_apps_callable_name(
|
||||
&tool_def.name,
|
||||
connector_id.as_deref(),
|
||||
connector_name.as_deref(),
|
||||
);
|
||||
let callable_namespace =
|
||||
normalize_codex_apps_callable_namespace(server_name, connector_name.as_deref());
|
||||
if let Some(title) = tool_def.title.as_deref() {
|
||||
let normalized_title = normalize_codex_apps_tool_title(connector_name.as_deref(), title);
|
||||
if tool_def.title.as_deref() != Some(normalized_title.as_str()) {
|
||||
tool_def.title = Some(normalized_title);
|
||||
}
|
||||
}
|
||||
let has_connector_metadata =
|
||||
connector_id.is_some() || connector_name.is_some() || connector_description.is_some();
|
||||
let namespace_description = if has_connector_metadata {
|
||||
connector_description
|
||||
} else {
|
||||
server_instructions.map(str::to_string)
|
||||
};
|
||||
ToolInfo {
|
||||
server_name: server_name.to_owned(),
|
||||
supports_parallel_tool_calls: false,
|
||||
server_origin: None,
|
||||
callable_name,
|
||||
callable_namespace,
|
||||
namespace_description,
|
||||
tool: tool_def,
|
||||
connector_id,
|
||||
connector_name,
|
||||
plugin_display_names: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a regular MCP tool by removing reserved connector metadata, keeping its raw tool name,
|
||||
/// and using the MCP server name and instructions for the model-visible namespace.
|
||||
fn regular_mcp_tool_info_from_listed_tool(
|
||||
server_name: &str,
|
||||
server_instructions: Option<&str>,
|
||||
tool: ToolWithConnectorId,
|
||||
) -> ToolInfo {
|
||||
let mut tool_def = tool.tool;
|
||||
strip_untrusted_connector_meta(&mut tool_def);
|
||||
ToolInfo {
|
||||
server_name: server_name.to_owned(),
|
||||
supports_parallel_tool_calls: false,
|
||||
server_origin: None,
|
||||
callable_name: tool_def.name.to_string(),
|
||||
callable_namespace: server_name.to_string(),
|
||||
namespace_description: server_instructions.map(str::to_string),
|
||||
tool: tool_def,
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
plugin_display_names: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_untrusted_connector_meta(tool: &mut RmcpTool) {
|
||||
@@ -477,6 +535,7 @@ async fn start_server_task(
|
||||
params: StartServerTaskParams,
|
||||
) -> Result<ManagedClient, StartupOutcomeError> {
|
||||
let StartServerTaskParams {
|
||||
is_codex_apps_mcp_server,
|
||||
startup_timeout,
|
||||
tool_timeout,
|
||||
tool_filter,
|
||||
@@ -508,6 +567,7 @@ async fn start_server_task(
|
||||
let fetch_start = Instant::now();
|
||||
let tools = list_tools_for_client_uncached(
|
||||
&server_name,
|
||||
is_codex_apps_mcp_server,
|
||||
&client,
|
||||
startup_timeout,
|
||||
initialize_result.instructions.as_deref(),
|
||||
@@ -520,19 +580,21 @@ async fn start_server_task(
|
||||
&[],
|
||||
);
|
||||
let server_info = mcp_server_info_from_implementation(initialize_result.server_info);
|
||||
write_cached_codex_apps_tools_if_needed(
|
||||
&server_name,
|
||||
codex_apps_tools_cache_context.as_ref(),
|
||||
&server_info,
|
||||
&tools,
|
||||
);
|
||||
if server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
let codex_apps_tools_cache_context = if is_codex_apps_mcp_server {
|
||||
write_codex_apps_tools_cache(
|
||||
codex_apps_tools_cache_context.as_ref(),
|
||||
&server_info,
|
||||
&tools,
|
||||
);
|
||||
emit_duration(
|
||||
MCP_TOOLS_LIST_DURATION_METRIC,
|
||||
list_start.elapsed(),
|
||||
&[("cache", "miss")],
|
||||
);
|
||||
}
|
||||
codex_apps_tools_cache_context
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let tools = filter_tools(tools, &tool_filter);
|
||||
|
||||
let managed = ManagedClient {
|
||||
@@ -585,6 +647,7 @@ fn mcp_server_info_from_implementation(server_info: Implementation) -> McpServer
|
||||
}
|
||||
|
||||
struct StartServerTaskParams {
|
||||
is_codex_apps_mcp_server: bool,
|
||||
startup_timeout: Option<Duration>, // TODO: cancel_token should handle this.
|
||||
tool_timeout: Duration,
|
||||
tool_filter: ToolFilter,
|
||||
@@ -685,6 +748,7 @@ async fn make_rmcp_client(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::model::JsonObject;
|
||||
use rmcp::model::Meta;
|
||||
|
||||
@@ -737,18 +801,7 @@ mod tests {
|
||||
fn custom_mcp_connector_metadata_is_stripped() {
|
||||
let mut tool = tool_with_connector_meta();
|
||||
|
||||
let (connector_id, connector_name, connector_description) =
|
||||
sanitize_tool_connector_metadata(
|
||||
"minimaltest",
|
||||
&mut tool,
|
||||
Some("connector_gmail".to_string()),
|
||||
Some("Gmail".to_string()),
|
||||
Some("Mail connector".to_string()),
|
||||
);
|
||||
|
||||
assert_eq!(connector_id, None);
|
||||
assert_eq!(connector_name, None);
|
||||
assert_eq!(connector_description, None);
|
||||
strip_untrusted_connector_meta(&mut tool);
|
||||
|
||||
let meta = tool.meta.as_ref().expect("meta");
|
||||
for key in [
|
||||
@@ -771,32 +824,36 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn codex_apps_connector_metadata_is_preserved() {
|
||||
let mut tool = tool_with_connector_meta();
|
||||
let tool = tool_with_connector_meta();
|
||||
let expected_tool = tool.clone();
|
||||
|
||||
let (connector_id, connector_name, connector_description) =
|
||||
sanitize_tool_connector_metadata(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
&mut tool,
|
||||
Some("connector_gmail".to_string()),
|
||||
Some("Gmail".to_string()),
|
||||
Some("Mail connector".to_string()),
|
||||
);
|
||||
let tool_info = tool_info_from_listed_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
/*is_codex_apps_mcp_server*/ true,
|
||||
/*server_instructions*/ None,
|
||||
ToolWithConnectorId {
|
||||
tool,
|
||||
connector_id: Some("connector_gmail".to_string()),
|
||||
connector_name: Some("Gmail".to_string()),
|
||||
connector_description: Some("Mail connector".to_string()),
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(connector_id.as_deref(), Some("connector_gmail"));
|
||||
assert_eq!(connector_name.as_deref(), Some("Gmail"));
|
||||
assert_eq!(connector_description.as_deref(), Some("Mail connector"));
|
||||
|
||||
let meta = tool.meta.as_ref().expect("meta");
|
||||
for key in [
|
||||
"connector_id",
|
||||
"connector_name",
|
||||
"connector_display_name",
|
||||
"connector_description",
|
||||
"connectorDescription",
|
||||
"connectorFutureField",
|
||||
"CONNECTOR_UPPERCASE",
|
||||
] {
|
||||
assert!(meta.0.contains_key(key), "{key} should be preserved");
|
||||
}
|
||||
let expected = ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
supports_parallel_tool_calls: false,
|
||||
server_origin: None,
|
||||
callable_name: "capture_file_upload".to_string(),
|
||||
callable_namespace: "codex_apps__gmail".to_string(),
|
||||
namespace_description: Some("Mail connector".to_string()),
|
||||
tool: expected_tool,
|
||||
connector_id: Some("connector_gmail".to_string()),
|
||||
connector_name: Some("Gmail".to_string()),
|
||||
plugin_display_names: Vec::new(),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(tool_info).expect("serialize actual tool info"),
|
||||
serde_json::to_value(expected).expect("serialize expected tool info")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,10 @@ use codex_utils_path_uri::PathUri;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SandboxState {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
pub permission_profile: PermissionProfile,
|
||||
pub codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
pub sandbox_cwd: PathUri,
|
||||
#[serde(default)]
|
||||
|
||||
@@ -113,9 +113,9 @@ impl ToolFilter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the model-visible view of a tool while preserving the raw metadata
|
||||
/// used by execution. Keep cache entries raw and call this at manager return
|
||||
/// boundaries.
|
||||
/// Returns the model-visible view of a tool while preserving the raw metadata used by execution.
|
||||
/// Declared file parameters are presented as local file paths; execution later uploads those files
|
||||
/// and replaces the paths with the uploaded-file objects expected by the app.
|
||||
pub(crate) fn tool_with_model_visible_input_schema(tool: &Tool) -> Tool {
|
||||
let file_params = declared_openai_file_input_param_names(tool.meta.as_deref());
|
||||
if file_params.is_empty() {
|
||||
@@ -124,7 +124,7 @@ pub(crate) fn tool_with_model_visible_input_schema(tool: &Tool) -> Tool {
|
||||
|
||||
let mut tool = tool.clone();
|
||||
let mut input_schema = JsonValue::Object(tool.input_schema.as_ref().clone());
|
||||
mask_input_schema_for_file_path_params(&mut input_schema, &file_params);
|
||||
rewrite_input_schema_for_local_file_paths(&mut input_schema, &file_params);
|
||||
if let JsonValue::Object(input_schema) = input_schema {
|
||||
tool.input_schema = Arc::new(input_schema);
|
||||
}
|
||||
@@ -262,15 +262,7 @@ const MAX_TOOL_NAME_LENGTH: usize = 64;
|
||||
const CALLABLE_NAME_HASH_LEN: usize = 12;
|
||||
const META_OPENAI_FILE_PARAMS: &str = "openai/fileParams";
|
||||
|
||||
fn callable_namespace_with_prefix(namespace: &str, prefix_mcp_tool_names: bool) -> String {
|
||||
if !prefix_mcp_tool_names || namespace.starts_with(LEGACY_MCP_TOOL_NAME_PREFIX) {
|
||||
namespace.to_string()
|
||||
} else {
|
||||
format!("{LEGACY_MCP_TOOL_NAME_PREFIX}{namespace}")
|
||||
}
|
||||
}
|
||||
|
||||
fn mask_input_schema_for_file_path_params(input_schema: &mut JsonValue, file_params: &[String]) {
|
||||
fn rewrite_input_schema_for_local_file_paths(input_schema: &mut JsonValue, file_params: &[String]) {
|
||||
let Some(properties) = input_schema
|
||||
.as_object_mut()
|
||||
.and_then(|schema| schema.get_mut("properties"))
|
||||
@@ -283,11 +275,11 @@ fn mask_input_schema_for_file_path_params(input_schema: &mut JsonValue, file_par
|
||||
let Some(property_schema) = properties.get_mut(field_name) else {
|
||||
continue;
|
||||
};
|
||||
mask_input_property_schema(property_schema);
|
||||
rewrite_input_property_schema_as_local_file_path(property_schema);
|
||||
}
|
||||
}
|
||||
|
||||
fn mask_input_property_schema(schema: &mut JsonValue) {
|
||||
fn rewrite_input_property_schema_as_local_file_path(schema: &mut JsonValue) {
|
||||
let Some(object) = schema.as_object_mut() else {
|
||||
return;
|
||||
};
|
||||
@@ -316,6 +308,14 @@ fn mask_input_property_schema(schema: &mut JsonValue) {
|
||||
}
|
||||
}
|
||||
|
||||
fn callable_namespace_with_prefix(namespace: &str, prefix_mcp_tool_names: bool) -> String {
|
||||
if !prefix_mcp_tool_names || namespace.starts_with(LEGACY_MCP_TOOL_NAME_PREFIX) {
|
||||
namespace.to_string()
|
||||
} else {
|
||||
format!("{LEGACY_MCP_TOOL_NAME_PREFIX}{namespace}")
|
||||
}
|
||||
}
|
||||
|
||||
fn sha1_hex(s: &str) -> String {
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(s.as_bytes());
|
||||
|
||||
@@ -2513,17 +2513,6 @@ allowed_approvals_reviewers = ["user"]
|
||||
.can_set(&AskForApproval::UnlessTrusted)
|
||||
.is_ok()
|
||||
);
|
||||
assert_eq!(
|
||||
requirements
|
||||
.approval_policy
|
||||
.can_set(&AskForApproval::OnFailure),
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "approval_policy",
|
||||
candidate: "OnFailure".into(),
|
||||
allowed: "[UnlessTrusted, OnRequest]".into(),
|
||||
requirement_source: RequirementSource::Unknown,
|
||||
})
|
||||
);
|
||||
assert!(
|
||||
requirements
|
||||
.approval_policy
|
||||
|
||||
@@ -52,7 +52,7 @@ fn active_user_layer_is_highest_precedence_user_layer() {
|
||||
toml::from_str(
|
||||
r#"
|
||||
model = "base"
|
||||
approval_policy = "on-failure"
|
||||
approval_policy = "on-request"
|
||||
"#,
|
||||
)
|
||||
.expect("base config"),
|
||||
@@ -86,7 +86,7 @@ approval_policy = "on-failure"
|
||||
.expect("merged user config")
|
||||
.get("approval_policy")
|
||||
.and_then(toml::Value::as_str),
|
||||
Some("on-failure")
|
||||
Some("on-request")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ fn with_user_config_updates_matching_user_layer_without_replacing_active_profile
|
||||
file: profile_file.clone(),
|
||||
profile: Some("work".to_string()),
|
||||
},
|
||||
toml::from_str(r#"approval_policy = "on-failure""#).expect("profile config"),
|
||||
toml::from_str(r#"approval_policy = "on-request""#).expect("profile config"),
|
||||
);
|
||||
let stack = ConfigLayerStack::new(
|
||||
vec![base_layer, profile_layer],
|
||||
@@ -136,6 +136,6 @@ fn with_user_config_updates_matching_user_layer_without_replacing_active_profile
|
||||
.expect("merged user config")
|
||||
.get("approval_policy")
|
||||
.and_then(toml::Value::as_str),
|
||||
Some("on-failure")
|
||||
Some("on-request")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -708,6 +708,37 @@ impl PluginsManager {
|
||||
remote_installed_plugins_to_config(plugins, &self.store)
|
||||
}
|
||||
|
||||
fn remote_plugin_id_for(&self, plugin_id: &PluginId) -> Option<String> {
|
||||
let cached_remote_plugin_id = {
|
||||
let cache = match self.remote_installed_plugins_cache.read() {
|
||||
Ok(cache) => cache,
|
||||
Err(err) => err.into_inner(),
|
||||
};
|
||||
cache.as_ref().and_then(|plugins| {
|
||||
plugins.iter().find_map(|plugin| {
|
||||
(plugin.name == plugin_id.plugin_name
|
||||
&& plugin.marketplace_name == plugin_id.marketplace_name)
|
||||
.then(|| plugin.id.clone())
|
||||
})
|
||||
})
|
||||
};
|
||||
if cached_remote_plugin_id.is_some() {
|
||||
return cached_remote_plugin_id;
|
||||
}
|
||||
|
||||
match self.store.remote_plugin_id(plugin_id) {
|
||||
Ok(remote_plugin_id) => remote_plugin_id,
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
plugin_id = %plugin_id.as_key(),
|
||||
error = %err,
|
||||
"failed to read persisted remote plugin identity"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn telemetry_metadata_for_installed_plugin(
|
||||
&self,
|
||||
plugin_id: &PluginId,
|
||||
@@ -739,8 +770,8 @@ impl PluginsManager {
|
||||
plugin_id: &PluginId,
|
||||
) -> PluginTelemetryMetadata {
|
||||
PluginTelemetryMetadata {
|
||||
plugin_id: plugin_id.clone(),
|
||||
remote_plugin_id: None,
|
||||
plugin_id: Some(plugin_id.clone()),
|
||||
remote_plugin_id: self.remote_plugin_id_for(plugin_id),
|
||||
capability_summary: None,
|
||||
}
|
||||
}
|
||||
@@ -762,8 +793,8 @@ impl PluginsManager {
|
||||
) -> Option<PluginTelemetryMetadata> {
|
||||
let plugin_id = PluginId::parse(&summary.config_name).ok()?;
|
||||
Some(PluginTelemetryMetadata {
|
||||
plugin_id,
|
||||
remote_plugin_id: None,
|
||||
remote_plugin_id: self.remote_plugin_id_for(&plugin_id),
|
||||
plugin_id: Some(plugin_id),
|
||||
capability_summary: Some(summary.clone()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -859,7 +859,7 @@ async fn installed_plugin_telemetry_metadata_collects_capabilities() {
|
||||
assert_eq!(
|
||||
metadata,
|
||||
PluginTelemetryMetadata {
|
||||
plugin_id,
|
||||
plugin_id: Some(plugin_id),
|
||||
remote_plugin_id: None,
|
||||
capability_summary: Some(PluginCapabilitySummary {
|
||||
config_name: "sample@test".to_string(),
|
||||
@@ -873,6 +873,71 @@ async fn installed_plugin_telemetry_metadata_collects_capabilities() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn installed_plugin_telemetry_metadata_resolves_persisted_remote_identity() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
write_cached_plugin(codex_home.path(), "openai-curated-remote", "linear");
|
||||
let plugin_id =
|
||||
PluginId::parse("linear@openai-curated-remote").expect("plugin id should parse");
|
||||
PluginStore::new(codex_home.path().to_path_buf())
|
||||
.write_remote_plugin_id(&plugin_id, "plugins~Plugin_linear")
|
||||
.expect("persist remote plugin id");
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
|
||||
let metadata = manager
|
||||
.telemetry_metadata_for_installed_plugin(&plugin_id)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
metadata,
|
||||
PluginTelemetryMetadata {
|
||||
plugin_id: Some(plugin_id),
|
||||
remote_plugin_id: Some("plugins~Plugin_linear".to_string()),
|
||||
capability_summary: Some(PluginCapabilitySummary {
|
||||
config_name: "linear@openai-curated-remote".to_string(),
|
||||
display_name: "linear".to_string(),
|
||||
description: None,
|
||||
has_skills: true,
|
||||
mcp_server_names: Vec::new(),
|
||||
app_connector_ids: Vec::new(),
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn installed_plugin_telemetry_metadata_prefers_remote_snapshot_identity() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
write_cached_plugin(codex_home.path(), "openai-curated-remote", "linear");
|
||||
let plugin_id =
|
||||
PluginId::parse("linear@openai-curated-remote").expect("plugin id should parse");
|
||||
PluginStore::new(codex_home.path().to_path_buf())
|
||||
.write_remote_plugin_id(&plugin_id, "plugins~Plugin_stale")
|
||||
.expect("persist remote plugin id");
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
manager.write_remote_installed_plugins_cache(vec![remote_installed_linear_plugin()]);
|
||||
|
||||
let metadata = manager
|
||||
.telemetry_metadata_for_installed_plugin(&plugin_id)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
metadata,
|
||||
PluginTelemetryMetadata {
|
||||
plugin_id: Some(plugin_id),
|
||||
remote_plugin_id: Some("plugins~Plugin_linear".to_string()),
|
||||
capability_summary: Some(PluginCapabilitySummary {
|
||||
config_name: "linear@openai-curated-remote".to_string(),
|
||||
display_name: "linear".to_string(),
|
||||
description: None,
|
||||
has_skills: true,
|
||||
mcp_server_names: Vec::new(),
|
||||
app_connector_ids: Vec::new(),
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn installed_plugin_telemetry_metadata_accepts_authoritative_remote_identity() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
@@ -887,7 +952,7 @@ async fn installed_plugin_telemetry_metadata_accepts_authoritative_remote_identi
|
||||
assert_eq!(
|
||||
metadata,
|
||||
PluginTelemetryMetadata {
|
||||
plugin_id,
|
||||
plugin_id: Some(plugin_id),
|
||||
remote_plugin_id: Some("plugins~Plugin_linear".to_string()),
|
||||
capability_summary: None,
|
||||
}
|
||||
@@ -912,14 +977,46 @@ fn capability_summary_telemetry_metadata_uses_local_identity() {
|
||||
assert_eq!(
|
||||
metadata,
|
||||
Some(PluginTelemetryMetadata {
|
||||
plugin_id: PluginId::parse("linear@openai-curated-remote")
|
||||
.expect("plugin id should parse"),
|
||||
plugin_id: Some(
|
||||
PluginId::parse("linear@openai-curated-remote").expect("plugin id should parse"),
|
||||
),
|
||||
remote_plugin_id: None,
|
||||
capability_summary: Some(summary),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_summary_telemetry_metadata_resolves_persisted_remote_identity() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
write_cached_plugin(codex_home.path(), "openai-curated-remote", "linear");
|
||||
let plugin_id =
|
||||
PluginId::parse("linear@openai-curated-remote").expect("plugin id should parse");
|
||||
PluginStore::new(codex_home.path().to_path_buf())
|
||||
.write_remote_plugin_id(&plugin_id, "plugins~Plugin_linear")
|
||||
.expect("persist remote plugin id");
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let summary = PluginCapabilitySummary {
|
||||
config_name: "linear@openai-curated-remote".to_string(),
|
||||
display_name: "Linear".to_string(),
|
||||
description: Some("Track work".to_string()),
|
||||
has_skills: true,
|
||||
mcp_server_names: vec!["linear".to_string()],
|
||||
app_connector_ids: vec![AppConnectorId("linear-app".to_string())],
|
||||
};
|
||||
|
||||
let metadata = manager.telemetry_metadata_for_capability_summary(&summary);
|
||||
|
||||
assert_eq!(
|
||||
metadata,
|
||||
Some(PluginTelemetryMetadata {
|
||||
plugin_id: Some(plugin_id),
|
||||
remote_plugin_id: Some("plugins~Plugin_linear".to_string()),
|
||||
capability_summary: Some(summary),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_installed_cache_prefers_local_curated_conflicts_when_remote_plugin_disabled() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
|
||||
@@ -12,8 +12,10 @@ use codex_login::CodexAuth;
|
||||
use codex_login::default_client::build_reqwest_client;
|
||||
use codex_plugin::AppConnectorId;
|
||||
use codex_plugin::AppDeclaration;
|
||||
use codex_plugin::PluginCapabilitySummary;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_plugin::app_connector_ids_from_declarations;
|
||||
use codex_plugin::prompt_safe_plugin_description;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use reqwest::RequestBuilder;
|
||||
use serde::Deserialize;
|
||||
@@ -120,6 +122,13 @@ pub struct RemotePluginServiceConfig {
|
||||
pub chatgpt_base_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RemotePluginUninstallTarget {
|
||||
pub plugin_id: PluginId,
|
||||
pub remote_plugin_id: String,
|
||||
pub fallback_capability_summary: PluginCapabilitySummary,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RemoteMarketplace {
|
||||
pub name: String,
|
||||
@@ -1304,40 +1313,90 @@ pub async fn install_remote_plugin(
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn resolve_remote_plugin_uninstall_target(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
remote_plugin_id: &str,
|
||||
) -> Result<RemotePluginUninstallTarget, RemotePluginCatalogError> {
|
||||
let auth = ensure_chatgpt_auth(auth)?;
|
||||
let plugin = fetch_plugin_detail(
|
||||
config,
|
||||
auth,
|
||||
remote_plugin_id,
|
||||
/*include_download_urls*/ false,
|
||||
)
|
||||
.await?;
|
||||
let marketplace_name = remote_plugin_canonical_marketplace_name(&plugin)?.to_string();
|
||||
let plugin_id = PluginId::new(plugin.name.clone(), marketplace_name).map_err(|err| {
|
||||
RemotePluginCatalogError::UnexpectedResponse(format!(
|
||||
"invalid local plugin id for remote plugin `{}`: {err}",
|
||||
plugin.id
|
||||
))
|
||||
})?;
|
||||
let app_declarations = plugin
|
||||
.release
|
||||
.app_manifest
|
||||
.as_ref()
|
||||
.map(plugin_app_declarations_from_value)
|
||||
.unwrap_or_else(|| app_declarations_from_remote_app_ids(&plugin.release.app_ids));
|
||||
let mut mcp_server_names = plugin
|
||||
.release
|
||||
.mcp_servers
|
||||
.iter()
|
||||
.map(|server| server.key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
mcp_server_names.sort_unstable();
|
||||
mcp_server_names.dedup();
|
||||
let fallback_capability_summary = PluginCapabilitySummary {
|
||||
config_name: plugin_id.as_key(),
|
||||
display_name: plugin.release.display_name,
|
||||
description: prompt_safe_plugin_description(Some(&plugin.release.description)),
|
||||
has_skills: !plugin.release.skills.is_empty(),
|
||||
mcp_server_names,
|
||||
app_connector_ids: app_connector_ids_from_declarations(&app_declarations),
|
||||
};
|
||||
Ok(RemotePluginUninstallTarget {
|
||||
plugin_id,
|
||||
remote_plugin_id: plugin.id,
|
||||
fallback_capability_summary,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn uninstall_remote_plugin(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
codex_home: PathBuf,
|
||||
plugin_id: &str,
|
||||
target: RemotePluginUninstallTarget,
|
||||
) -> Result<(), RemotePluginCatalogError> {
|
||||
let auth = ensure_chatgpt_auth(auth)?;
|
||||
let plugin = fetch_plugin_detail(
|
||||
config, auth, plugin_id, /*include_download_urls*/ false,
|
||||
)
|
||||
.await?;
|
||||
let marketplace_name = remote_plugin_canonical_marketplace_name(&plugin)?.to_string();
|
||||
let plugin_name = plugin.name;
|
||||
let RemotePluginUninstallTarget {
|
||||
plugin_id,
|
||||
remote_plugin_id,
|
||||
fallback_capability_summary: _,
|
||||
} = target;
|
||||
let marketplace_name = plugin_id.marketplace_name.clone();
|
||||
let plugin_name = plugin_id.plugin_name.clone();
|
||||
|
||||
let base_url = config.chatgpt_base_url.trim_end_matches('/');
|
||||
let url = format!("{base_url}/ps/plugins/{plugin_id}/uninstall");
|
||||
let url = format!("{base_url}/ps/plugins/{remote_plugin_id}/uninstall");
|
||||
let client = build_reqwest_client();
|
||||
let request = authenticated_request(client.post(&url), auth)?;
|
||||
let response: RemotePluginMutationResponse = send_and_decode(request, &url).await?;
|
||||
if response.id != plugin_id {
|
||||
if response.id != remote_plugin_id {
|
||||
return Err(RemotePluginCatalogError::UnexpectedPluginId {
|
||||
expected: plugin_id.to_string(),
|
||||
expected: remote_plugin_id,
|
||||
actual: response.id,
|
||||
});
|
||||
}
|
||||
if response.enabled {
|
||||
return Err(RemotePluginCatalogError::UnexpectedEnabledState {
|
||||
plugin_id: plugin_id.to_string(),
|
||||
plugin_id: response.id,
|
||||
expected_enabled: false,
|
||||
actual_enabled: response.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
let legacy_plugin_id = plugin_id.to_string();
|
||||
let legacy_plugin_id = response.id;
|
||||
tokio::task::spawn_blocking(move || {
|
||||
remove_remote_plugin_cache(codex_home, marketplace_name, plugin_name, legacy_plugin_id)
|
||||
})
|
||||
|
||||
@@ -252,13 +252,6 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.",
|
||||
"enum": [
|
||||
"on-failure"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "The model decides when to ask the user for approval.",
|
||||
"enum": [
|
||||
@@ -826,7 +819,7 @@
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"reminder_interval_model_requests": {
|
||||
"reminder_interval_seconds": {
|
||||
"format": "uint64",
|
||||
"minimum": 1.0,
|
||||
"type": "integer"
|
||||
|
||||
@@ -171,7 +171,7 @@ For all of testing, running, building, and formatting, do not attempt to fix unr
|
||||
|
||||
Be mindful of whether to run validation commands proactively. In the absence of behavioral guidance:
|
||||
|
||||
- When running in non-interactive approval modes like **never** or **on-failure**, you can proactively run tests, lint and do whatever you need to ensure you've completed the task. If you are unable to run tests, you must still do your utmost best to complete the task.
|
||||
- When running in the non-interactive approval mode **never**, you can proactively run tests, lint and do whatever you need to ensure you've completed the task. If you are unable to run tests, you must still do your utmost best to complete the task.
|
||||
- When working in interactive approval modes like **untrusted**, or **on-request**, hold off on running tests or lint commands until the user is ready for you to finalize your output, because these commands take time to run and slow down iteration. Instead suggest what you want to do next, and let the user confirm first.
|
||||
- When working on test-related tasks, such as adding tests, fixing tests, or reproducing a bug to verify behavior, you may proactively run tests regardless of approval mode. Use your judgement to decide whether this is a test-related task.
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ For all of testing, running, building, and formatting, do not attempt to fix unr
|
||||
|
||||
Be mindful of whether to run validation commands proactively. In the absence of behavioral guidance:
|
||||
|
||||
- When running in non-interactive approval modes like **never** or **on-failure**, you can proactively run tests, lint and do whatever you need to ensure you've completed the task. If you are unable to run tests, you must still do your utmost best to complete the task.
|
||||
- When running in the non-interactive approval mode **never**, you can proactively run tests, lint and do whatever you need to ensure you've completed the task. If you are unable to run tests, you must still do your utmost best to complete the task.
|
||||
- When working in interactive approval modes like **untrusted**, or **on-request**, hold off on running tests or lint commands until the user is ready for you to finalize your output, because these commands take time to run and slow down iteration. Instead suggest what you want to do next, and let the user confirm first.
|
||||
- When working on test-related tasks, such as adding tests, fixing tests, or reproducing a bug to verify behavior, you may proactively run tests regardless of approval mode. Use your judgement to decide whether this is a test-related task.
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ For all of testing, running, building, and formatting, do not attempt to fix unr
|
||||
|
||||
Be mindful of whether to run validation commands proactively. In the absence of behavioral guidance:
|
||||
|
||||
- When running in non-interactive approval modes like **never** or **on-failure**, proactively run tests, lint and do whatever you need to ensure you've completed the task.
|
||||
- When running in the non-interactive approval mode **never**, proactively run tests, lint and do whatever you need to ensure you've completed the task.
|
||||
- When working in interactive approval modes like **untrusted**, or **on-request**, hold off on running tests or lint commands until the user is ready for you to finalize your output, because these commands take time to run and slow down iteration. Instead suggest what you want to do next, and let the user confirm first.
|
||||
- When working on test-related tasks, such as adding tests, fixing tests, or reproducing a bug to verify behavior, you may proactively run tests regardless of approval mode. Use your judgement to decide whether this is a test-related task.
|
||||
|
||||
|
||||
@@ -41,7 +41,8 @@ fn keep_forked_rollout_item(item: &RolloutItem, preserve_reference_context_item:
|
||||
_ => false,
|
||||
},
|
||||
RolloutItem::ResponseItem(
|
||||
ResponseItem::AgentMessage { .. }
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
| ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
@@ -142,7 +143,7 @@ impl AgentControl {
|
||||
.items;
|
||||
let initial_history = InitialHistory::Resumed(ResumedHistory {
|
||||
conversation_id: thread_id,
|
||||
history,
|
||||
history: Arc::new(history),
|
||||
rollout_path: stored_thread.rollout_path,
|
||||
});
|
||||
if initial_history.get_multi_agent_version() != Some(MultiAgentVersion::V2) {
|
||||
@@ -625,7 +626,7 @@ impl AgentControl {
|
||||
.items;
|
||||
let initial_history = InitialHistory::Resumed(ResumedHistory {
|
||||
conversation_id: thread_id,
|
||||
history,
|
||||
history: Arc::new(history),
|
||||
rollout_path: stored_thread.rollout_path,
|
||||
});
|
||||
let parent_thread_id = stored_thread.parent_thread_id;
|
||||
|
||||
@@ -73,6 +73,7 @@ use codex_otel::current_span_w3c_trace_context;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
||||
use codex_protocol::config_types::Verbosity as VerbosityConfig;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
@@ -162,6 +163,19 @@ pub(crate) struct CompactConversationRequestSettings {
|
||||
pub(crate) service_tier: Option<String>,
|
||||
}
|
||||
|
||||
fn session_telemetry_for_request(
|
||||
session_telemetry: &SessionTelemetry,
|
||||
request: &ResponsesApiRequest,
|
||||
) -> SessionTelemetry {
|
||||
session_telemetry.clone().with_inference_request(
|
||||
request.service_tier.as_deref(),
|
||||
request
|
||||
.reasoning
|
||||
.as_ref()
|
||||
.and_then(|reasoning| reasoning.effort.as_ref()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Session-scoped state shared by all [`ModelClient`] clones.
|
||||
///
|
||||
/// This is intentionally kept minimal so `ModelClient` does not need to hold a full `Config`. Most
|
||||
@@ -777,7 +791,6 @@ impl ModelClient {
|
||||
service_tier: Option<String>,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
) -> Result<ResponsesApiRequest> {
|
||||
let instructions = &prompt.base_instructions.text;
|
||||
let mut input = prompt.get_formatted_input_for_request(model_info.use_responses_lite);
|
||||
if !self.state.provider.info().is_openai() {
|
||||
input
|
||||
@@ -785,6 +798,28 @@ impl ModelClient {
|
||||
.for_each(ResponseItem::clear_internal_chat_message_metadata_passthrough);
|
||||
}
|
||||
let tools = create_tools_json_for_responses_api(&prompt.tools)?;
|
||||
let (instructions, tools) = if model_info.use_responses_lite {
|
||||
let mut prefix = vec![ResponseItem::AdditionalTools {
|
||||
id: None,
|
||||
role: "developer".to_string(),
|
||||
tools,
|
||||
}];
|
||||
if !prompt.base_instructions.text.is_empty() {
|
||||
prefix.push(ResponseItem::Message {
|
||||
id: None,
|
||||
role: "developer".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: prompt.base_instructions.text.clone(),
|
||||
}],
|
||||
phase: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
});
|
||||
}
|
||||
input.splice(0..0, prefix);
|
||||
(String::new(), None)
|
||||
} else {
|
||||
(prompt.base_instructions.text.clone(), Some(tools))
|
||||
};
|
||||
let reasoning = Self::build_reasoning(model_info, effort, summary);
|
||||
let include = if reasoning.is_some() {
|
||||
vec!["reasoning.encrypted_content".to_string()]
|
||||
@@ -811,7 +846,7 @@ impl ModelClient {
|
||||
let service_tier = model_info.service_tier_for_request(service_tier);
|
||||
let request = ResponsesApiRequest {
|
||||
model: model_info.slug.clone(),
|
||||
instructions: instructions.clone(),
|
||||
instructions,
|
||||
input,
|
||||
tools,
|
||||
tool_choice: "auto".to_string(),
|
||||
@@ -1319,6 +1354,8 @@ impl ModelClientSession {
|
||||
let store = request.store;
|
||||
self.client
|
||||
.prepare_response_items_for_request(&mut request.input, store);
|
||||
let request_session_telemetry =
|
||||
session_telemetry_for_request(session_telemetry, &request);
|
||||
let inference_trace_attempt = inference_trace.start_attempt();
|
||||
inference_trace_attempt.add_request_headers(&mut options.extra_headers);
|
||||
inference_trace_attempt.record_started(&request);
|
||||
@@ -1334,7 +1371,7 @@ impl ModelClientSession {
|
||||
Ok(stream) => {
|
||||
let (stream, _) = map_response_stream(
|
||||
stream,
|
||||
session_telemetry.clone(),
|
||||
request_session_telemetry,
|
||||
inference_trace_attempt,
|
||||
Arc::clone(&self.client.state.provider),
|
||||
);
|
||||
@@ -1426,6 +1463,12 @@ impl ModelClientSession {
|
||||
service_tier.clone(),
|
||||
responses_metadata,
|
||||
)?;
|
||||
let request_session_telemetry = if warmup {
|
||||
// `generate=false` prewarm is connection setup, not an inference request.
|
||||
session_telemetry.clone()
|
||||
} else {
|
||||
session_telemetry_for_request(session_telemetry, &request)
|
||||
};
|
||||
let mut client_metadata = self
|
||||
.client
|
||||
.build_ws_client_metadata(responses_metadata, model_info.use_responses_lite);
|
||||
@@ -1529,7 +1572,7 @@ impl ModelClientSession {
|
||||
})?;
|
||||
let (stream, last_request_rx) = map_response_stream(
|
||||
stream_result,
|
||||
session_telemetry.clone(),
|
||||
request_session_telemetry,
|
||||
inference_trace_attempt,
|
||||
Arc::clone(&self.client.state.provider),
|
||||
);
|
||||
|
||||
@@ -83,7 +83,8 @@ fn strip_image_details(items: &mut [ResponseItem]) {
|
||||
}
|
||||
}
|
||||
}
|
||||
ResponseItem::Reasoning { .. }
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
|
||||
@@ -110,7 +110,7 @@ fn serializes_text_verbosity_when_set() {
|
||||
model: "gpt-5.4".to_string(),
|
||||
instructions: "i".to_string(),
|
||||
input,
|
||||
tools,
|
||||
tools: Some(tools),
|
||||
tool_choice: "auto".to_string(),
|
||||
parallel_tool_calls: true,
|
||||
reasoning: None,
|
||||
@@ -157,7 +157,7 @@ fn serializes_text_schema_with_strict_format() {
|
||||
model: "gpt-5.4".to_string(),
|
||||
instructions: "i".to_string(),
|
||||
input,
|
||||
tools,
|
||||
tools: Some(tools),
|
||||
tool_choice: "auto".to_string(),
|
||||
parallel_tool_calls: true,
|
||||
reasoning: None,
|
||||
@@ -218,7 +218,7 @@ fn omits_text_when_not_set() {
|
||||
model: "gpt-5.4".to_string(),
|
||||
instructions: "i".to_string(),
|
||||
input,
|
||||
tools,
|
||||
tools: Some(tools),
|
||||
tool_choice: "auto".to_string(),
|
||||
parallel_tool_calls: true,
|
||||
reasoning: None,
|
||||
@@ -241,7 +241,7 @@ fn serializes_flex_service_tier_when_set() {
|
||||
model: "gpt-5.4".to_string(),
|
||||
instructions: "i".to_string(),
|
||||
input: vec![],
|
||||
tools: vec![],
|
||||
tools: Some(vec![]),
|
||||
tool_choice: "auto".to_string(),
|
||||
parallel_tool_calls: true,
|
||||
reasoning: None,
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::time::Instant;
|
||||
use crate::Prompt;
|
||||
use crate::client::ModelClientSession;
|
||||
use crate::client_common::ResponseEvent;
|
||||
use crate::context::world_state::WorldState;
|
||||
use crate::hook_runtime::PostCompactHookOutcome;
|
||||
use crate::hook_runtime::PreCompactHookOutcome;
|
||||
use crate::hook_runtime::run_post_compact_hooks;
|
||||
@@ -60,12 +61,29 @@ const COMPACT_USER_MESSAGE_MAX_TOKENS: usize = 20_000;
|
||||
/// Mid-turn compaction must use `BeforeLastUserMessage` because the model is trained to see the
|
||||
/// compaction summary as the last item in history after mid-turn compaction; we therefore inject
|
||||
/// initial context into the replacement history just above the last real user message.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum InitialContextInjection {
|
||||
BeforeLastUserMessage,
|
||||
BeforeLastUserMessage(Arc<WorldState>),
|
||||
DoNotInject,
|
||||
}
|
||||
|
||||
pub(crate) async fn build_compaction_initial_context(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
initial_context_injection: &InitialContextInjection,
|
||||
) -> (Vec<ResponseItem>, Option<Arc<WorldState>>) {
|
||||
// Return the rendered state with its items so history and its baseline stay identical.
|
||||
match initial_context_injection {
|
||||
InitialContextInjection::BeforeLastUserMessage(world_state) => {
|
||||
let items = sess
|
||||
.build_initial_context_with_world_state(turn_context, world_state.as_ref())
|
||||
.await;
|
||||
(items, Some(Arc::clone(world_state)))
|
||||
}
|
||||
InitialContextInjection::DoNotInject => (Vec::new(), None),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn should_use_remote_compact_task(provider: &ModelProviderInfo) -> bool {
|
||||
provider.supports_remote_compaction()
|
||||
}
|
||||
@@ -309,17 +327,21 @@ async fn run_compact_task_inner_impl(
|
||||
}
|
||||
let (window_number, window_ids) = sess.advance_auto_compact_window().await;
|
||||
|
||||
if matches!(
|
||||
initial_context_injection,
|
||||
InitialContextInjection::BeforeLastUserMessage
|
||||
) {
|
||||
let initial_context = sess.build_initial_context(turn_context.as_ref()).await;
|
||||
let (initial_context, world_state_baseline) = build_compaction_initial_context(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
&initial_context_injection,
|
||||
)
|
||||
.await;
|
||||
if !initial_context.is_empty() {
|
||||
new_history =
|
||||
insert_initial_context_before_last_real_user_or_summary(new_history, initial_context);
|
||||
}
|
||||
let reference_context_item = match initial_context_injection {
|
||||
InitialContextInjection::DoNotInject => None,
|
||||
InitialContextInjection::BeforeLastUserMessage => Some(turn_context.to_turn_context_item()),
|
||||
InitialContextInjection::BeforeLastUserMessage(_) => {
|
||||
Some(turn_context.to_turn_context_item())
|
||||
}
|
||||
};
|
||||
let compacted_item = CompactedItem {
|
||||
message: summary_text.clone(),
|
||||
@@ -333,6 +355,7 @@ async fn run_compact_task_inner_impl(
|
||||
turn_context.as_ref(),
|
||||
new_history,
|
||||
reference_context_item,
|
||||
world_state_baseline,
|
||||
compacted_item,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -6,8 +6,10 @@ use crate::client::CompactConversationRequestSettings;
|
||||
use crate::compact::CompactionAnalyticsAttempt;
|
||||
use crate::compact::CompactionAnalyticsDetails;
|
||||
use crate::compact::InitialContextInjection;
|
||||
use crate::compact::build_compaction_initial_context;
|
||||
use crate::compact::compaction_status_from_result;
|
||||
use crate::compact::insert_initial_context_before_last_real_user_or_summary;
|
||||
use crate::context::world_state::WorldState;
|
||||
use crate::context_manager::ContextManager;
|
||||
use crate::hook_runtime::PostCompactHookOutcome;
|
||||
use crate::hook_runtime::PreCompactHookOutcome;
|
||||
@@ -237,7 +239,7 @@ async fn run_remote_compact_task_inner_impl(
|
||||
window_id,
|
||||
CodexResponsesRequestKind::Compaction(compaction_metadata),
|
||||
);
|
||||
let mut new_history = sess
|
||||
let new_history = sess
|
||||
.services
|
||||
.model_client
|
||||
.compact_conversation_history(
|
||||
@@ -259,17 +261,19 @@ async fn run_remote_compact_task_inner_impl(
|
||||
)
|
||||
.await?;
|
||||
let (new_window_number, new_window_ids) = sess.advance_auto_compact_window().await;
|
||||
new_history = process_compacted_history(
|
||||
let (new_history, world_state_baseline) = process_compacted_history(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
new_history,
|
||||
initial_context_injection,
|
||||
&initial_context_injection,
|
||||
)
|
||||
.await;
|
||||
|
||||
let reference_context_item = match initial_context_injection {
|
||||
InitialContextInjection::DoNotInject => None,
|
||||
InitialContextInjection::BeforeLastUserMessage => Some(turn_context.to_turn_context_item()),
|
||||
InitialContextInjection::BeforeLastUserMessage(_) => {
|
||||
Some(turn_context.to_turn_context_item())
|
||||
}
|
||||
};
|
||||
let compacted_item = CompactedItem {
|
||||
message: String::new(),
|
||||
@@ -290,6 +294,7 @@ async fn run_remote_compact_task_inner_impl(
|
||||
turn_context.as_ref(),
|
||||
new_history,
|
||||
reference_context_item,
|
||||
world_state_baseline,
|
||||
compacted_item,
|
||||
)
|
||||
.await;
|
||||
@@ -304,22 +309,19 @@ pub(crate) async fn process_compacted_history(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
mut compacted_history: Vec<ResponseItem>,
|
||||
initial_context_injection: InitialContextInjection,
|
||||
) -> Vec<ResponseItem> {
|
||||
initial_context_injection: &InitialContextInjection,
|
||||
) -> (Vec<ResponseItem>, Option<Arc<WorldState>>) {
|
||||
// Mid-turn compaction is the only path that must inject initial context above the last user
|
||||
// message in the replacement history. Pre-turn compaction instead injects context after the
|
||||
// compaction item, but mid-turn compaction keeps the compaction item last for model training.
|
||||
let initial_context = if matches!(
|
||||
initial_context_injection,
|
||||
InitialContextInjection::BeforeLastUserMessage
|
||||
) {
|
||||
sess.build_initial_context(turn_context).await
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let (initial_context, world_state_baseline) =
|
||||
build_compaction_initial_context(sess, turn_context, initial_context_injection).await;
|
||||
|
||||
compacted_history.retain(should_keep_compacted_history_item);
|
||||
insert_initial_context_before_last_real_user_or_summary(compacted_history, initial_context)
|
||||
(
|
||||
insert_initial_context_before_last_real_user_or_summary(compacted_history, initial_context),
|
||||
world_state_baseline,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns whether an item from remote compaction output should be preserved.
|
||||
@@ -352,7 +354,8 @@ pub(crate) fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
|
||||
ResponseItem::AgentMessage { .. } => true,
|
||||
ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true,
|
||||
ResponseItem::CompactionTrigger { .. } => false,
|
||||
ResponseItem::Reasoning { .. }
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
| ResponseItem::ToolSearchCall { .. }
|
||||
|
||||
@@ -165,18 +165,18 @@ async fn run_remote_compact_task_inner(
|
||||
attempt
|
||||
.track(sess.as_ref(), status, codex_error, analytics_details)
|
||||
.await;
|
||||
if matches!(&result, Err(CodexErr::TurnAborted)) {
|
||||
return result;
|
||||
match result {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err @ CodexErr::TurnAborted) => Err(err),
|
||||
Err(err) => {
|
||||
sess.track_turn_codex_error(turn_context, &err);
|
||||
let event = EventMsg::Error(
|
||||
err.to_error_event(Some("Error running remote compact task".to_string())),
|
||||
);
|
||||
sess.send_event(turn_context, event).await;
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
if let Err(err) = result {
|
||||
sess.track_turn_codex_error(turn_context, &err);
|
||||
let event = EventMsg::Error(
|
||||
err.to_error_event(Some("Error running remote compact task".to_string())),
|
||||
);
|
||||
sess.send_event(turn_context, event).await;
|
||||
return Err(err);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_remote_compact_task_inner_impl(
|
||||
@@ -293,17 +293,19 @@ async fn run_remote_compact_task_inner_impl(
|
||||
build_v2_compacted_history(&prompt_input, compaction_output);
|
||||
analytics_details.retained_image_count = Some(retained_images);
|
||||
let (new_window_number, new_window_ids) = sess.advance_auto_compact_window().await;
|
||||
let new_history = process_compacted_history(
|
||||
let (new_history, world_state_baseline) = process_compacted_history(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
compacted_history,
|
||||
initial_context_injection,
|
||||
&initial_context_injection,
|
||||
)
|
||||
.await;
|
||||
|
||||
let reference_context_item = match initial_context_injection {
|
||||
InitialContextInjection::DoNotInject => None,
|
||||
InitialContextInjection::BeforeLastUserMessage => Some(turn_context.to_turn_context_item()),
|
||||
InitialContextInjection::BeforeLastUserMessage(_) => {
|
||||
Some(turn_context.to_turn_context_item())
|
||||
}
|
||||
};
|
||||
let compacted_item = CompactedItem {
|
||||
message: String::new(),
|
||||
@@ -321,6 +323,7 @@ async fn run_remote_compact_task_inner_impl(
|
||||
turn_context.as_ref(),
|
||||
new_history,
|
||||
reference_context_item,
|
||||
world_state_baseline,
|
||||
compacted_item,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use super::*;
|
||||
use crate::session::tests::build_world_state_from_turn_context;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
|
||||
use codex_protocol::models::InternalChatMessageMetadataPassthrough;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::sync::Arc;
|
||||
|
||||
async fn process_compacted_history_with_test_session(
|
||||
compacted_history: Vec<ResponseItem>,
|
||||
@@ -13,12 +15,16 @@ async fn process_compacted_history_with_test_session(
|
||||
session
|
||||
.set_previous_turn_settings(previous_turn_settings.cloned())
|
||||
.await;
|
||||
let initial_context = session.build_initial_context(&turn_context).await;
|
||||
let refreshed = crate::compact_remote::process_compacted_history(
|
||||
let world_state = Arc::new(build_world_state_from_turn_context(&session, &turn_context).await);
|
||||
let initial_context = session
|
||||
.build_initial_context_with_world_state(&turn_context, world_state.as_ref())
|
||||
.await;
|
||||
let initial_context_injection = InitialContextInjection::BeforeLastUserMessage(world_state);
|
||||
let (refreshed, _) = crate::compact_remote::process_compacted_history(
|
||||
&session,
|
||||
&turn_context,
|
||||
compacted_history,
|
||||
InitialContextInjection::BeforeLastUserMessage,
|
||||
&initial_context_injection,
|
||||
)
|
||||
.await;
|
||||
(refreshed, initial_context)
|
||||
|
||||
@@ -640,7 +640,7 @@ async fn selected_user_config_file_layers_over_base_user_config() {
|
||||
tmp.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
model = "gpt-main"
|
||||
approval_policy = "on-failure"
|
||||
approval_policy = "on-request"
|
||||
"#,
|
||||
)
|
||||
.expect("write default user config");
|
||||
@@ -697,7 +697,7 @@ approval_policy = "on-failure"
|
||||
.effective_config()
|
||||
.get("approval_policy")
|
||||
.and_then(TomlValue::as_str),
|
||||
Some("on-failure")
|
||||
Some("on-request")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1033,12 +1033,6 @@ personality = true
|
||||
config_requirements
|
||||
.approval_policy
|
||||
.can_set(&AskForApproval::Never)?;
|
||||
assert!(
|
||||
config_requirements
|
||||
.approval_policy
|
||||
.can_set(&AskForApproval::OnFailure)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(
|
||||
config_requirements.web_search_mode.value(),
|
||||
WebSearchMode::Cached
|
||||
|
||||
@@ -620,11 +620,11 @@ current_time_reminder = true
|
||||
r#"
|
||||
[features.current_time_reminder]
|
||||
enabled = true
|
||||
reminder_interval_model_requests = 4
|
||||
reminder_interval_seconds = 4
|
||||
clock_source = "external"
|
||||
"#,
|
||||
CurrentTimeReminderConfig {
|
||||
reminder_interval_model_requests: 4,
|
||||
reminder_interval_seconds: 4,
|
||||
clock_source: CurrentTimeSource::External,
|
||||
},
|
||||
),
|
||||
@@ -642,7 +642,7 @@ async fn load_config_rejects_zero_current_time_reminder_interval() -> std::io::R
|
||||
r#"
|
||||
[features.current_time_reminder]
|
||||
enabled = true
|
||||
reminder_interval_model_requests = 0
|
||||
reminder_interval_seconds = 0
|
||||
"#,
|
||||
)
|
||||
.await
|
||||
@@ -651,7 +651,7 @@ reminder_interval_model_requests = 0
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"features.current_time_reminder.reminder_interval_model_requests must be positive"
|
||||
"features.current_time_reminder.reminder_interval_seconds must be positive"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -8272,7 +8272,7 @@ model_provider = "openai-custom"
|
||||
[profiles.zdr]
|
||||
model = "o3"
|
||||
model_provider = "openai"
|
||||
approval_policy = "on-failure"
|
||||
approval_policy = "on-request"
|
||||
|
||||
[profiles.zdr.analytics]
|
||||
enabled = false
|
||||
@@ -8280,7 +8280,7 @@ enabled = false
|
||||
[profiles.gpt5]
|
||||
model = "gpt-5.4"
|
||||
model_provider = "openai"
|
||||
approval_policy = "on-failure"
|
||||
approval_policy = "on-request"
|
||||
model_reasoning_effort = "high"
|
||||
model_reasoning_summary = "detailed"
|
||||
model_verbosity = "high"
|
||||
|
||||
@@ -246,7 +246,8 @@ Payload:
|
||||
```
|
||||
You may also see them addressed as to=/root/..., which indicates your identity is /root/...
|
||||
"#;
|
||||
const DEFAULT_MULTI_AGENT_V2_SHARED_USAGE_HINT_TEXT: &str = r#"Note that collaboration tools cannot be called from inside `functions.exec`. Call `spawn_agent`, `send_message`, `followup_task`, `wait_agent`, `interrupt_agent`, and `list_agents` only as direct tool calls using the recipient shown in their tool definitions, such as `to=functions.spawn_agent` without a configured namespace or `to=functions.agents.spawn_agent` with `tool_namespace = "agents"`, since they are intentionally absent from the `functions.exec` `tools.*` namespace. Available tools in `functions.exec` are explicitly described with a `tools` namespace in the developer message.
|
||||
const DEFAULT_MULTI_AGENT_V2_TOOL_NAMESPACE: &str = "collaboration";
|
||||
const DEFAULT_MULTI_AGENT_V2_SHARED_USAGE_HINT_TEXT: &str = r#"Note that collaboration tools cannot be called from inside `functions.exec`. Call `spawn_agent`, `send_message`, `followup_task`, `wait_agent`, `interrupt_agent`, and `list_agents` only as direct tool calls using the recipient shown in their tool definitions, such as `to=functions.collaboration.spawn_agent`, since they are intentionally absent from the `functions.exec` `tools.*` namespace. Available tools in `functions.exec` are explicitly described with a `tools` namespace in the developer message.
|
||||
|
||||
All agents share the same directory. In detail:
|
||||
- All agents have access to the same container and filesystem as you.
|
||||
@@ -1114,14 +1115,14 @@ pub struct RolloutBudgetConfig {
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
pub struct CurrentTimeReminderConfig {
|
||||
pub reminder_interval_model_requests: u64,
|
||||
pub reminder_interval_seconds: u64,
|
||||
pub clock_source: CurrentTimeSource,
|
||||
}
|
||||
|
||||
impl Default for CurrentTimeReminderConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
reminder_interval_model_requests: 1,
|
||||
reminder_interval_seconds: 1,
|
||||
clock_source: CurrentTimeSource::System,
|
||||
}
|
||||
}
|
||||
@@ -1157,7 +1158,7 @@ impl MultiAgentV2Config {
|
||||
DEFAULT_MULTI_AGENT_V2_SUBAGENT_USAGE_HINT_TEXT,
|
||||
max_concurrent_threads_per_session,
|
||||
)),
|
||||
tool_namespace: None,
|
||||
tool_namespace: Some(DEFAULT_MULTI_AGENT_V2_TOOL_NAMESPACE.to_string()),
|
||||
hide_spawn_agent_metadata: true,
|
||||
non_code_mode_only: true,
|
||||
}
|
||||
@@ -2656,18 +2657,18 @@ fn resolve_current_time_reminder_config(
|
||||
|
||||
let base = current_time_reminder_toml_config(config_toml.features.as_ref());
|
||||
let default = CurrentTimeReminderConfig::default();
|
||||
let reminder_interval_model_requests = base
|
||||
.and_then(|config| config.reminder_interval_model_requests)
|
||||
.unwrap_or(default.reminder_interval_model_requests);
|
||||
if reminder_interval_model_requests == 0 {
|
||||
let reminder_interval_seconds = base
|
||||
.and_then(|config| config.reminder_interval_seconds)
|
||||
.unwrap_or(default.reminder_interval_seconds);
|
||||
if reminder_interval_seconds == 0 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"features.current_time_reminder.reminder_interval_model_requests must be positive",
|
||||
"features.current_time_reminder.reminder_interval_seconds must be positive",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Some(CurrentTimeReminderConfig {
|
||||
reminder_interval_model_requests,
|
||||
reminder_interval_seconds,
|
||||
clock_source: base
|
||||
.and_then(|config| config.clock_source)
|
||||
.unwrap_or(default.clock_source),
|
||||
|
||||
@@ -40,7 +40,6 @@ use codex_mcp::ToolPluginProvenance;
|
||||
use codex_mcp::codex_apps_tools_cache_key;
|
||||
use codex_mcp::compute_auth_statuses;
|
||||
use codex_mcp::effective_mcp_servers;
|
||||
use codex_mcp::host_owned_codex_apps_enabled;
|
||||
use codex_mcp::tool_plugin_provenance;
|
||||
|
||||
const CONNECTORS_READY_TIMEOUT_ON_EMPTY_TOOLS: Duration = Duration::from_secs(30);
|
||||
@@ -240,7 +239,6 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
|
||||
let mut mcp_servers = effective_mcp_servers(&mcp_config, auth.as_ref());
|
||||
mcp_servers.retain(|name, _| name == CODEX_APPS_MCP_SERVER_NAME);
|
||||
let host_owned_codex_apps_enabled = host_owned_codex_apps_enabled(&mcp_config, auth.as_ref());
|
||||
if mcp_servers.is_empty() {
|
||||
return Ok(AccessibleConnectorsStatus {
|
||||
connectors: Vec::new(),
|
||||
@@ -275,7 +273,6 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
McpRuntimeContext::new(environment_manager, config.cwd.to_path_buf()),
|
||||
config.codex_home.to_path_buf(),
|
||||
codex_apps_tools_cache_key(auth.as_ref()),
|
||||
host_owned_codex_apps_enabled,
|
||||
mcp_config.prefix_mcp_tool_names,
|
||||
mcp_config.client_elicitation_capability,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
|
||||
@@ -24,10 +24,6 @@ pub(crate) struct EnvironmentsState {
|
||||
}
|
||||
|
||||
impl EnvironmentsState {
|
||||
pub(crate) fn from_turn_context(turn_context: &TurnContext) -> Self {
|
||||
Self::from_turn_context_with_environments(turn_context, &turn_context.environments)
|
||||
}
|
||||
|
||||
pub(crate) fn from_turn_context_with_environments(
|
||||
turn_context: &TurnContext,
|
||||
environments: &TurnEnvironmentSnapshot,
|
||||
|
||||
@@ -86,18 +86,18 @@ impl ContextManager {
|
||||
|
||||
pub(crate) fn update_world_state(
|
||||
&mut self,
|
||||
world_state: WorldState,
|
||||
world_state: Arc<WorldState>,
|
||||
) -> Vec<Box<dyn ContextualUserFragment>> {
|
||||
let fragments = self.world_state_baseline.as_deref().map_or_else(
|
||||
|| world_state.render_full(),
|
||||
|previous| world_state.render_diff(previous),
|
||||
);
|
||||
self.world_state_baseline = Some(Arc::new(world_state));
|
||||
self.world_state_baseline = Some(world_state);
|
||||
fragments
|
||||
}
|
||||
|
||||
pub(crate) fn set_world_state_baseline(&mut self, world_state: WorldState) {
|
||||
self.world_state_baseline = Some(Arc::new(world_state));
|
||||
pub(crate) fn set_world_state_baseline(&mut self, world_state: Arc<WorldState>) {
|
||||
self.world_state_baseline = Some(world_state);
|
||||
}
|
||||
|
||||
pub(crate) fn set_token_usage_full(&mut self, context_window: i64) {
|
||||
@@ -386,7 +386,8 @@ impl ContextManager {
|
||||
output: truncate_function_output_payload(output, policy_with_serialization_budget),
|
||||
internal_chat_message_metadata_passthrough: metadata.clone(),
|
||||
},
|
||||
ResponseItem::Message { .. }
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::Message { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
| ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
@@ -476,7 +477,8 @@ pub(crate) fn truncate_function_output_payload(
|
||||
fn is_api_message(message: &ResponseItem) -> bool {
|
||||
match message {
|
||||
ResponseItem::Message { role, .. } => role.as_str() != "system",
|
||||
ResponseItem::AgentMessage { .. }
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
| ResponseItem::FunctionCallOutput { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
| ResponseItem::ToolSearchCall { .. }
|
||||
@@ -722,7 +724,8 @@ fn is_model_generated_item(item: &ResponseItem) -> bool {
|
||||
| ResponseItem::Compaction { .. }
|
||||
| ResponseItem::ContextCompaction { .. } => true,
|
||||
ResponseItem::CompactionTrigger { .. } => false,
|
||||
ResponseItem::FunctionCallOutput { .. }
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::FunctionCallOutput { .. }
|
||||
| ResponseItem::ToolSearchOutput { .. }
|
||||
| ResponseItem::CustomToolCallOutput { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
|
||||
@@ -32,6 +32,7 @@ use image::Luma;
|
||||
use image::Rgba;
|
||||
use pretty_assertions::assert_eq;
|
||||
use regex_lite::Regex;
|
||||
use std::sync::Arc;
|
||||
|
||||
const EXEC_FORMAT_MAX_BYTES: usize = 10_000;
|
||||
const EXEC_FORMAT_MAX_TOKENS: usize = 2_500;
|
||||
@@ -82,7 +83,7 @@ fn world_state_baseline_deduplicates_until_history_is_replaced() {
|
||||
state.add_section(EnvironmentsState::from_turn_context_item(
|
||||
&reference_context_item(),
|
||||
));
|
||||
state
|
||||
Arc::new(state)
|
||||
};
|
||||
let mut history = ContextManager::new();
|
||||
|
||||
|
||||
@@ -377,6 +377,7 @@ pub fn build_exec_request(
|
||||
args: args.to_vec(),
|
||||
cwd,
|
||||
env,
|
||||
managed_network: None,
|
||||
additional_permissions: None,
|
||||
};
|
||||
let options = ExecOptions {
|
||||
@@ -459,6 +460,7 @@ pub(crate) async fn execute_exec_request(
|
||||
arg0,
|
||||
exec_server_sandbox: _,
|
||||
exec_server_enforce_managed_network: _,
|
||||
exec_server_managed_network: _,
|
||||
} = exec_request;
|
||||
|
||||
// TODO(anp): Keep PathUri through the local process launch boundary.
|
||||
|
||||
@@ -177,7 +177,6 @@ pub(crate) fn prompt_is_rejected_by_policy(
|
||||
) -> Option<&'static str> {
|
||||
match approval_policy {
|
||||
AskForApproval::Never => Some(PROMPT_CONFLICT_REASON),
|
||||
AskForApproval::OnFailure => None,
|
||||
AskForApproval::OnRequest => None,
|
||||
AskForApproval::UnlessTrusted => None,
|
||||
AskForApproval::Granular(granular_config) => {
|
||||
@@ -689,15 +688,14 @@ pub(crate) fn render_decision_for_unmatched_command(
|
||||
Decision::Forbidden
|
||||
}
|
||||
}
|
||||
AskForApproval::OnFailure
|
||||
| AskForApproval::OnRequest
|
||||
AskForApproval::OnRequest
|
||||
| AskForApproval::UnlessTrusted
|
||||
| AskForApproval::Granular(_) => Decision::Prompt,
|
||||
};
|
||||
}
|
||||
|
||||
match approval_policy {
|
||||
AskForApproval::Never | AskForApproval::OnFailure => {
|
||||
AskForApproval::Never => {
|
||||
// We allow the command to run, relying on the sandbox for
|
||||
// protection.
|
||||
Decision::Allow
|
||||
|
||||
@@ -738,10 +738,9 @@ fn hook_run_metric_tags(run: &HookRunSummary) -> [(&'static str, &'static str);
|
||||
fn hook_permission_mode(turn_context: &TurnContext) -> String {
|
||||
match turn_context.approval_policy.value() {
|
||||
AskForApproval::Never => "bypassPermissions",
|
||||
AskForApproval::UnlessTrusted
|
||||
| AskForApproval::OnFailure
|
||||
| AskForApproval::OnRequest
|
||||
| AskForApproval::Granular(_) => "default",
|
||||
AskForApproval::UnlessTrusted | AskForApproval::OnRequest | AskForApproval::Granular(_) => {
|
||||
"default"
|
||||
}
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@ pub(crate) fn prepare_response_items(items: &mut [ResponseItem]) {
|
||||
prepare_tool_output_content(content);
|
||||
}
|
||||
}
|
||||
ResponseItem::Reasoning { .. }
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
//! and rewrite only the declared arguments into the provided-file payload
|
||||
//! shape expected by the downstream Apps tool.
|
||||
//!
|
||||
//! Model-visible schema masking is owned by `codex-mcp` alongside MCP tool
|
||||
//! inventory, so this module only handles the execution-time argument rewrite.
|
||||
//! The model-facing local-path schema is owned by `codex-mcp` alongside MCP tool inventory, so this
|
||||
//! module only handles uploading the files and rewriting the execution-time arguments.
|
||||
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user