From 3a5a5122bf96c410c11a986cbd2669e351f970d1 Mon Sep 17 00:00:00 2001 From: celia-oai Date: Fri, 5 Dec 2025 17:58:16 -0800 Subject: [PATCH 1/6] changes --- .../app-server-protocol/src/protocol/v2.rs | 89 ++++++++++++++++++- codex-rs/app-server/src/config_api.rs | 19 ++-- .../app-server/tests/suite/v2/config_rpc.rs | 55 +++++------- 3 files changed, 121 insertions(+), 42 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 211f0ba3757..00425410451 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -5,6 +5,7 @@ use crate::protocol::common::AuthMode; use codex_protocol::account::PlanType; use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::config_types::Verbosity; use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent; use codex_protocol::items::TurnItem as CoreTurnItem; use codex_protocol::models::ResponseItem; @@ -12,6 +13,7 @@ use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::parse_command::ParsedCommand as CoreParsedCommand; use codex_protocol::plan_tool::PlanItemArg as CorePlanItemArg; use codex_protocol::plan_tool::StepStatus as CorePlanStepStatus; +use codex_protocol::protocol::AskForApproval as CoreAskForApproval; use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo; use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot; use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot; @@ -159,6 +161,91 @@ pub enum ConfigLayerName { User, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct SandboxWorkspaceWrite { + #[serde(default)] + pub writable_roots: Vec, + #[serde(default)] + pub network_access: bool, + #[serde(default)] + pub exclude_tmpdir_env_var: bool, + #[serde(default)] + pub exclude_slash_tmp: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct Config { + pub model: Option, + pub review_model: Option, + pub model_context_window: Option, + pub model_auto_compact_token_limit: Option, + pub model_provider: Option, + #[serde( + default, + deserialize_with = "deserialize_approval_policy", + serialize_with = "serialize_approval_policy" + )] + pub approval_policy: Option, + #[serde( + default, + deserialize_with = "deserialize_sandbox_mode", + serialize_with = "serialize_sandbox_mode" + )] + pub sandbox_mode: Option, + pub sandbox_workspace_write: Option, + pub instructions: Option, + pub developer_instructions: Option, + pub compact_prompt: Option, + pub model_reasoning_effort: Option, + pub model_reasoning_summary: Option, + pub model_verbosity: Option, + #[serde(default, flatten)] + pub additional: HashMap, +} + +fn deserialize_approval_policy<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let value = Option::::deserialize(deserializer)?; + Ok(value.map(AskForApproval::from)) +} + +fn serialize_approval_policy( + value: &Option, + serializer: S, +) -> Result +where + S: serde::Serializer, +{ + value + .as_ref() + .map(|policy| policy.to_core()) + .serialize(serializer) +} + +fn deserialize_sandbox_mode<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let value = Option::::deserialize(deserializer)?; + Ok(value.map(SandboxMode::from)) +} + +fn serialize_sandbox_mode(value: &Option, serializer: S) -> Result +where + S: serde::Serializer, +{ + value + .as_ref() + .map(|mode| mode.to_core()) + .serialize(serializer) +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -237,7 +324,7 @@ pub struct ConfigReadParams { #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct ConfigReadResponse { - pub config: JsonValue, + pub config: Config, pub origins: HashMap, #[serde(skip_serializing_if = "Option::is_none")] pub layers: Option>, diff --git a/codex-rs/app-server/src/config_api.rs b/codex-rs/app-server/src/config_api.rs index 98fe93fb259..c1eaf62d26f 100644 --- a/codex-rs/app-server/src/config_api.rs +++ b/codex-rs/app-server/src/config_api.rs @@ -1,5 +1,6 @@ use crate::error_code::INTERNAL_ERROR_CODE; use crate::error_code::INVALID_REQUEST_ERROR_CODE; +use codex_app_server_protocol::Config; use codex_app_server_protocol::ConfigBatchWriteParams; use codex_app_server_protocol::ConfigLayer; use codex_app_server_protocol::ConfigLayerMetadata; @@ -75,8 +76,10 @@ impl ConfigApi { let effective = layers.effective_config(); validate_config(&effective).map_err(|err| internal_error("invalid configuration", err))?; + let config: Config = serde_json::from_value(to_json_value(&effective)) + .map_err(|err| internal_error("failed to deserialize configuration", err))?; let response = ConfigReadResponse { - config: to_json_value(&effective), + config, origins: layers.origins(), layers: params.include_layers.then(|| layers.layers_high_to_low()), }; @@ -773,6 +776,7 @@ fn config_write_error(code: ConfigWriteErrorCode, message: impl Into) -> mod tests { use super::*; use anyhow::Result; + use codex_app_server_protocol::AskForApproval; use pretty_assertions::assert_eq; use tempfile::tempdir; @@ -895,10 +899,7 @@ remote_compaction = true .await .expect("response"); - assert_eq!( - response.config.get("approval_policy"), - Some(&json!("never")) - ); + assert_eq!(response.config.approval_policy, Some(AskForApproval::Never)); assert_eq!( response @@ -953,8 +954,10 @@ remote_compaction = true }) .await .expect("read"); - let config_object = read_after.config.as_object().expect("object"); - assert_eq!(config_object.get("approval_policy"), Some(&json!("never"))); + assert_eq!( + read_after.config.approval_policy, + Some(AskForApproval::Never) + ); assert_eq!( read_after .origins @@ -1093,7 +1096,7 @@ remote_compaction = true .await .expect("response"); - assert_eq!(response.config.get("model"), Some(&json!("system"))); + assert_eq!(response.config.model.as_deref(), Some("system")); assert_eq!( response.origins.get("model").expect("origin").name, ConfigLayerName::System diff --git a/codex-rs/app-server/tests/suite/v2/config_rpc.rs b/codex-rs/app-server/tests/suite/v2/config_rpc.rs index eb3ece64b29..844d057bd37 100644 --- a/codex-rs/app-server/tests/suite/v2/config_rpc.rs +++ b/codex-rs/app-server/tests/suite/v2/config_rpc.rs @@ -1,6 +1,7 @@ use anyhow::Result; use app_test_support::McpProcess; use app_test_support::to_response; +use codex_app_server_protocol::AskForApproval; use codex_app_server_protocol::ConfigBatchWriteParams; use codex_app_server_protocol::ConfigEdit; use codex_app_server_protocol::ConfigLayerName; @@ -12,9 +13,11 @@ use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::MergeStrategy; use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::SandboxMode; use codex_app_server_protocol::WriteStatus; use pretty_assertions::assert_eq; use serde_json::json; +use std::path::PathBuf; use tempfile::TempDir; use tokio::time::timeout; @@ -57,7 +60,7 @@ sandbox_mode = "workspace-write" layers, } = to_response(resp)?; - assert_eq!(config.get("model"), Some(&json!("gpt-user"))); + assert_eq!(config.model.as_deref(), Some("gpt-user")); assert_eq!( origins.get("model").expect("origin").name, ConfigLayerName::User @@ -123,30 +126,29 @@ writable_roots = ["/system"] layers, } = to_response(resp)?; - assert_eq!(config.get("model"), Some(&json!("gpt-system"))); + assert_eq!(config.model.as_deref(), Some("gpt-system")); assert_eq!( origins.get("model").expect("origin").name, ConfigLayerName::System ); - assert_eq!(config.get("approval_policy"), Some(&json!("never"))); + assert_eq!(config.approval_policy, Some(AskForApproval::Never)); assert_eq!( origins.get("approval_policy").expect("origin").name, ConfigLayerName::System ); - assert_eq!(config.get("sandbox_mode"), Some(&json!("workspace-write"))); + assert_eq!(config.sandbox_mode, Some(SandboxMode::WorkspaceWrite)); assert_eq!( origins.get("sandbox_mode").expect("origin").name, ConfigLayerName::User ); - assert_eq!( - config - .get("sandbox_workspace_write") - .and_then(|v| v.get("writable_roots")), - Some(&json!(["/system"])) - ); + let sandbox = config + .sandbox_workspace_write + .as_ref() + .expect("sandbox workspace write"); + assert_eq!(sandbox.writable_roots, vec![PathBuf::from("/system")]); assert_eq!( origins .get("sandbox_workspace_write.writable_roots.0") @@ -155,12 +157,7 @@ writable_roots = ["/system"] ConfigLayerName::System ); - assert_eq!( - config - .get("sandbox_workspace_write") - .and_then(|v| v.get("network_access")), - Some(&json!(true)) - ); + assert!(sandbox.network_access); assert_eq!( origins .get("sandbox_workspace_write.network_access") @@ -242,7 +239,7 @@ model = "gpt-old" ) .await??; let verify: ConfigReadResponse = to_response(verify_resp)?; - assert_eq!(verify.config.get("model"), Some(&json!("gpt-new"))); + assert_eq!(verify.config.model.as_deref(), Some("gpt-new")); Ok(()) } @@ -342,22 +339,14 @@ async fn config_batch_write_applies_multiple_edits() -> Result<()> { ) .await??; let read: ConfigReadResponse = to_response(read_resp)?; - assert_eq!( - read.config.get("sandbox_mode"), - Some(&json!("workspace-write")) - ); - assert_eq!( - read.config - .get("sandbox_workspace_write") - .and_then(|v| v.get("writable_roots")), - Some(&json!(["/tmp"])) - ); - assert_eq!( - read.config - .get("sandbox_workspace_write") - .and_then(|v| v.get("network_access")), - Some(&json!(false)) - ); + assert_eq!(read.config.sandbox_mode, Some(SandboxMode::WorkspaceWrite)); + let sandbox = read + .config + .sandbox_workspace_write + .as_ref() + .expect("sandbox workspace write"); + assert_eq!(sandbox.writable_roots, vec![PathBuf::from("/tmp")]); + assert!(!sandbox.network_access); Ok(()) } From 6cd38560990e9268f07e7cfeb9043f9ea7eda564 Mon Sep 17 00:00:00 2001 From: celia-oai Date: Mon, 8 Dec 2025 14:50:36 -0800 Subject: [PATCH 2/6] changes --- .../app-server-protocol/src/protocol/v2.rs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 00425410451..e808293c4ea 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -4,6 +4,7 @@ use std::path::PathBuf; use crate::protocol::common::AuthMode; use codex_protocol::account::PlanType; use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment; +use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::Verbosity; use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent; @@ -175,6 +176,32 @@ pub struct SandboxWorkspaceWrite { pub exclude_slash_tmp: bool, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct Tools { + pub web_search: Option, + pub view_image: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct Profile { + pub model: Option, + pub model_provider: Option, + #[serde( + default, + deserialize_with = "deserialize_approval_policy", + serialize_with = "serialize_approval_policy" + )] + pub approval_policy: Option, + pub model_reasoning_effort: Option, + pub model_reasoning_summary: Option, + pub model_verbosity: Option, + pub chatgpt_base_url: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "snake_case")] #[ts(export_to = "v2/")] @@ -197,6 +224,12 @@ pub struct Config { )] pub sandbox_mode: Option, pub sandbox_workspace_write: Option, + pub forced_chatgpt_workspace_id: Option, + pub forced_login_method: Option, + pub tools: Option, + pub profile: Option, + #[serde(default)] + pub profiles: HashMap, pub instructions: Option, pub developer_instructions: Option, pub compact_prompt: Option, From 2e7dc613b35020c5a68851428020b8d590c8906f Mon Sep 17 00:00:00 2001 From: celia-oai Date: Mon, 8 Dec 2025 15:25:31 -0800 Subject: [PATCH 3/6] hanges --- .../app-server-protocol/src/protocol/v2.rs | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index e808293c4ea..657493bb03a 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -163,7 +163,7 @@ pub enum ConfigLayerName { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] +#[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct SandboxWorkspaceWrite { #[serde(default)] @@ -177,17 +177,17 @@ pub struct SandboxWorkspaceWrite { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] +#[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] -pub struct Tools { +pub struct ToolsV2 { pub web_search: Option, pub view_image: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] +#[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] -pub struct Profile { +pub struct ProfileV2 { pub model: Option, pub model_provider: Option, #[serde( @@ -203,7 +203,7 @@ pub struct Profile { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] +#[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct Config { pub model: Option, @@ -226,10 +226,10 @@ pub struct Config { pub sandbox_workspace_write: Option, pub forced_chatgpt_workspace_id: Option, pub forced_login_method: Option, - pub tools: Option, + pub tools: Option, pub profile: Option, #[serde(default)] - pub profiles: HashMap, + pub profiles: HashMap, pub instructions: Option, pub developer_instructions: Option, pub compact_prompt: Option, @@ -1828,6 +1828,25 @@ mod tests { ); } + #[test] + fn config_tools_deserializes_with_snake_case_keys() { + let config: Config = serde_json::from_value(json!({ + "tools": { + "web_search": true, + "view_image": false + } + })) + .unwrap(); + + assert_eq!( + config.tools, + Some(Tools { + web_search: Some(true), + view_image: Some(false), + }) + ); + } + #[test] fn codex_error_info_serializes_http_status_code_in_camel_case() { let value = CodexErrorInfo::ResponseTooManyFailedAttempts { From 3cc9657ec108a56941be0d18af00ab1a3d62bf97 Mon Sep 17 00:00:00 2001 From: celia-oai Date: Mon, 8 Dec 2025 17:03:19 -0800 Subject: [PATCH 4/6] snake case camel case shenanigans --- .../app-server-protocol/src/protocol/v2.rs | 72 ++++++++++++++++--- .../app-server/tests/suite/v2/config_rpc.rs | 59 +++++++++++++++ 2 files changed, 122 insertions(+), 9 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 657493bb03a..feb928dca64 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -166,13 +166,13 @@ pub enum ConfigLayerName { #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct SandboxWorkspaceWrite { - #[serde(default)] + #[serde(default, alias = "writable_roots")] pub writable_roots: Vec, - #[serde(default)] + #[serde(default, alias = "network_access")] pub network_access: bool, - #[serde(default)] + #[serde(default, alias = "exclude_tmpdir_env_var")] pub exclude_tmpdir_env_var: bool, - #[serde(default)] + #[serde(default, alias = "exclude_slash_tmp")] pub exclude_slash_tmp: bool, } @@ -180,7 +180,9 @@ pub struct SandboxWorkspaceWrite { #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct ToolsV2 { + #[serde(alias = "web_search")] pub web_search: Option, + #[serde(alias = "view_image")] pub view_image: Option, } @@ -189,16 +191,22 @@ pub struct ToolsV2 { #[ts(export_to = "v2/")] pub struct ProfileV2 { pub model: Option, + #[serde(alias = "model_provider")] pub model_provider: Option, #[serde( default, deserialize_with = "deserialize_approval_policy", - serialize_with = "serialize_approval_policy" + serialize_with = "serialize_approval_policy", + alias = "approval_policy" )] pub approval_policy: Option, + #[serde(alias = "model_reasoning_effort")] pub model_reasoning_effort: Option, + #[serde(alias = "model_reasoning_summary")] pub model_reasoning_summary: Option, + #[serde(alias = "model_verbosity")] pub model_verbosity: Option, + #[serde(alias = "chatgpt_base_url")] pub chatgpt_base_url: Option, } @@ -207,36 +215,50 @@ pub struct ProfileV2 { #[ts(export_to = "v2/")] pub struct Config { pub model: Option, + #[serde(alias = "review_model")] pub review_model: Option, + #[serde(alias = "model_context_window")] pub model_context_window: Option, + #[serde(alias = "model_auto_compact_token_limit")] pub model_auto_compact_token_limit: Option, + #[serde(alias = "model_provider")] pub model_provider: Option, #[serde( default, deserialize_with = "deserialize_approval_policy", - serialize_with = "serialize_approval_policy" + serialize_with = "serialize_approval_policy", + alias = "approval_policy" )] pub approval_policy: Option, #[serde( default, deserialize_with = "deserialize_sandbox_mode", - serialize_with = "serialize_sandbox_mode" + serialize_with = "serialize_sandbox_mode", + alias = "sandbox_mode" )] pub sandbox_mode: Option, + #[serde(alias = "sandbox_workspace_write")] pub sandbox_workspace_write: Option, + #[serde(alias = "forced_chatgpt_workspace_id")] pub forced_chatgpt_workspace_id: Option, + #[serde(alias = "forced_login_method")] pub forced_login_method: Option, pub tools: Option, pub profile: Option, #[serde(default)] pub profiles: HashMap, pub instructions: Option, + #[serde(alias = "developer_instructions")] pub developer_instructions: Option, + #[serde(alias = "compact_prompt")] pub compact_prompt: Option, + #[serde(alias = "model_reasoning_effort")] pub model_reasoning_effort: Option, + #[serde(alias = "model_reasoning_summary")] pub model_reasoning_summary: Option, + #[serde(alias = "model_verbosity")] pub model_verbosity: Option, - #[serde(default, flatten)] + #[serde(default, flatten, deserialize_with = "deserialize_additional")] pub additional: HashMap, } @@ -279,6 +301,38 @@ where .serialize(serializer) } +fn deserialize_additional<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let raw = HashMap::::deserialize(deserializer)?; + Ok(raw + .into_iter() + .map(|(key, value)| (snake_to_camel(&key), value)) + .collect()) +} + +fn snake_to_camel(input: &str) -> String { + let mut output = String::with_capacity(input.len()); + let mut upper_next = false; + + for ch in input.chars() { + if ch == '_' { + upper_next = true; + continue; + } + + if upper_next { + output.push(ch.to_ascii_uppercase()); + upper_next = false; + } else { + output.push(ch); + } + } + + output +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -1840,7 +1894,7 @@ mod tests { assert_eq!( config.tools, - Some(Tools { + Some(ToolsV2 { web_search: Some(true), view_image: Some(false), }) diff --git a/codex-rs/app-server/tests/suite/v2/config_rpc.rs b/codex-rs/app-server/tests/suite/v2/config_rpc.rs index 844d057bd37..b6615ef6679 100644 --- a/codex-rs/app-server/tests/suite/v2/config_rpc.rs +++ b/codex-rs/app-server/tests/suite/v2/config_rpc.rs @@ -14,6 +14,7 @@ use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::MergeStrategy; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::SandboxMode; +use codex_app_server_protocol::ToolsV2; use codex_app_server_protocol::WriteStatus; use pretty_assertions::assert_eq; use serde_json::json; @@ -73,6 +74,64 @@ sandbox_mode = "workspace-write" Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn config_read_includes_tools() -> Result<()> { + let codex_home = TempDir::new()?; + write_config( + &codex_home, + r#" +model = "gpt-user" + +[tools] +web_search = true +view_image = false +"#, + )?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_config_read_request(ConfigReadParams { + include_layers: true, + }) + .await?; + let resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + let ConfigReadResponse { + config, + origins, + layers, + } = to_response(resp)?; + + let tools = config.tools.expect("tools present"); + assert_eq!( + tools, + ToolsV2 { + web_search: Some(true), + view_image: Some(false), + } + ); + assert_eq!( + origins.get("tools.web_search").expect("origin").name, + ConfigLayerName::User + ); + assert_eq!( + origins.get("tools.view_image").expect("origin").name, + ConfigLayerName::User + ); + + let layers = layers.expect("layers present"); + assert_eq!(layers.len(), 2); + assert_eq!(layers[0].name, ConfigLayerName::SessionFlags); + assert_eq!(layers[1].name, ConfigLayerName::User); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn config_read_includes_system_layer_and_overrides() -> Result<()> { let codex_home = TempDir::new()?; From 00d039902cfabf19a2408a3186de0a02785b994c Mon Sep 17 00:00:00 2001 From: celia-oai Date: Mon, 8 Dec 2025 17:10:27 -0800 Subject: [PATCH 5/6] back to snake case --- .../app-server-protocol/src/protocol/v2.rs | 97 +++---------------- 1 file changed, 12 insertions(+), 85 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index feb928dca64..79c9ab0942a 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -163,102 +163,80 @@ pub enum ConfigLayerName { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "snake_case")] #[ts(export_to = "v2/")] pub struct SandboxWorkspaceWrite { - #[serde(default, alias = "writable_roots")] + #[serde(default)] pub writable_roots: Vec, - #[serde(default, alias = "network_access")] + #[serde(default)] pub network_access: bool, - #[serde(default, alias = "exclude_tmpdir_env_var")] + #[serde(default)] pub exclude_tmpdir_env_var: bool, - #[serde(default, alias = "exclude_slash_tmp")] + #[serde(default)] pub exclude_slash_tmp: bool, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "snake_case")] #[ts(export_to = "v2/")] pub struct ToolsV2 { - #[serde(alias = "web_search")] pub web_search: Option, - #[serde(alias = "view_image")] pub view_image: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "snake_case")] #[ts(export_to = "v2/")] pub struct ProfileV2 { pub model: Option, - #[serde(alias = "model_provider")] pub model_provider: Option, #[serde( default, deserialize_with = "deserialize_approval_policy", - serialize_with = "serialize_approval_policy", - alias = "approval_policy" + serialize_with = "serialize_approval_policy" )] pub approval_policy: Option, - #[serde(alias = "model_reasoning_effort")] pub model_reasoning_effort: Option, - #[serde(alias = "model_reasoning_summary")] pub model_reasoning_summary: Option, - #[serde(alias = "model_verbosity")] pub model_verbosity: Option, - #[serde(alias = "chatgpt_base_url")] pub chatgpt_base_url: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "snake_case")] #[ts(export_to = "v2/")] pub struct Config { pub model: Option, - #[serde(alias = "review_model")] pub review_model: Option, - #[serde(alias = "model_context_window")] pub model_context_window: Option, - #[serde(alias = "model_auto_compact_token_limit")] pub model_auto_compact_token_limit: Option, - #[serde(alias = "model_provider")] pub model_provider: Option, #[serde( default, deserialize_with = "deserialize_approval_policy", - serialize_with = "serialize_approval_policy", - alias = "approval_policy" + serialize_with = "serialize_approval_policy" )] pub approval_policy: Option, #[serde( default, deserialize_with = "deserialize_sandbox_mode", - serialize_with = "serialize_sandbox_mode", - alias = "sandbox_mode" + serialize_with = "serialize_sandbox_mode" )] pub sandbox_mode: Option, - #[serde(alias = "sandbox_workspace_write")] pub sandbox_workspace_write: Option, - #[serde(alias = "forced_chatgpt_workspace_id")] pub forced_chatgpt_workspace_id: Option, - #[serde(alias = "forced_login_method")] pub forced_login_method: Option, pub tools: Option, pub profile: Option, #[serde(default)] pub profiles: HashMap, pub instructions: Option, - #[serde(alias = "developer_instructions")] pub developer_instructions: Option, - #[serde(alias = "compact_prompt")] pub compact_prompt: Option, - #[serde(alias = "model_reasoning_effort")] pub model_reasoning_effort: Option, - #[serde(alias = "model_reasoning_summary")] pub model_reasoning_summary: Option, - #[serde(alias = "model_verbosity")] pub model_verbosity: Option, - #[serde(default, flatten, deserialize_with = "deserialize_additional")] + #[serde(default, flatten)] pub additional: HashMap, } @@ -301,38 +279,6 @@ where .serialize(serializer) } -fn deserialize_additional<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - let raw = HashMap::::deserialize(deserializer)?; - Ok(raw - .into_iter() - .map(|(key, value)| (snake_to_camel(&key), value)) - .collect()) -} - -fn snake_to_camel(input: &str) -> String { - let mut output = String::with_capacity(input.len()); - let mut upper_next = false; - - for ch in input.chars() { - if ch == '_' { - upper_next = true; - continue; - } - - if upper_next { - output.push(ch.to_ascii_uppercase()); - upper_next = false; - } else { - output.push(ch); - } - } - - output -} - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -1882,25 +1828,6 @@ mod tests { ); } - #[test] - fn config_tools_deserializes_with_snake_case_keys() { - let config: Config = serde_json::from_value(json!({ - "tools": { - "web_search": true, - "view_image": false - } - })) - .unwrap(); - - assert_eq!( - config.tools, - Some(ToolsV2 { - web_search: Some(true), - view_image: Some(false), - }) - ); - } - #[test] fn codex_error_info_serializes_http_status_code_in_camel_case() { let value = CodexErrorInfo::ResponseTooManyFailedAttempts { From deccedc83a324112814db8277fc8d80c43e24de2 Mon Sep 17 00:00:00 2001 From: celia-oai Date: Wed, 10 Dec 2025 12:10:32 -0800 Subject: [PATCH 6/6] comment --- .../app-server-protocol/src/protocol/v2.rs | 125 +++++++++--------- 1 file changed, 63 insertions(+), 62 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 79c9ab0942a..edd3aefa745 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -6,6 +6,7 @@ use codex_protocol::account::PlanType; use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::config_types::SandboxMode as CoreSandboxMode; use codex_protocol::config_types::Verbosity; use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent; use codex_protocol::items::TurnItem as CoreTurnItem; @@ -125,17 +126,68 @@ impl From for CodexErrorInfo { } } -v2_enum_from_core!( - pub enum AskForApproval from codex_protocol::protocol::AskForApproval { - UnlessTrusted, OnFailure, OnRequest, Never +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "kebab-case")] +#[ts(rename_all = "kebab-case", export_to = "v2/")] +pub enum AskForApproval { + #[serde(rename = "untrusted")] + #[ts(rename = "untrusted")] + UnlessTrusted, + OnFailure, + OnRequest, + Never, +} + +impl AskForApproval { + pub fn to_core(self) -> CoreAskForApproval { + match self { + AskForApproval::UnlessTrusted => CoreAskForApproval::UnlessTrusted, + AskForApproval::OnFailure => CoreAskForApproval::OnFailure, + AskForApproval::OnRequest => CoreAskForApproval::OnRequest, + AskForApproval::Never => CoreAskForApproval::Never, + } } -); +} -v2_enum_from_core!( - pub enum SandboxMode from codex_protocol::config_types::SandboxMode { - ReadOnly, WorkspaceWrite, DangerFullAccess +impl From for AskForApproval { + fn from(value: CoreAskForApproval) -> Self { + match value { + CoreAskForApproval::UnlessTrusted => AskForApproval::UnlessTrusted, + CoreAskForApproval::OnFailure => AskForApproval::OnFailure, + CoreAskForApproval::OnRequest => AskForApproval::OnRequest, + CoreAskForApproval::Never => AskForApproval::Never, + } } -); +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "kebab-case")] +#[ts(rename_all = "kebab-case", export_to = "v2/")] +pub enum SandboxMode { + ReadOnly, + WorkspaceWrite, + DangerFullAccess, +} + +impl SandboxMode { + pub fn to_core(self) -> CoreSandboxMode { + match self { + SandboxMode::ReadOnly => CoreSandboxMode::ReadOnly, + SandboxMode::WorkspaceWrite => CoreSandboxMode::WorkspaceWrite, + SandboxMode::DangerFullAccess => CoreSandboxMode::DangerFullAccess, + } + } +} + +impl From for SandboxMode { + fn from(value: CoreSandboxMode) -> Self { + match value { + CoreSandboxMode::ReadOnly => SandboxMode::ReadOnly, + CoreSandboxMode::WorkspaceWrite => SandboxMode::WorkspaceWrite, + CoreSandboxMode::DangerFullAccess => SandboxMode::DangerFullAccess, + } + } +} v2_enum_from_core!( pub enum ReviewDelivery from codex_protocol::protocol::ReviewDelivery { @@ -180,6 +232,7 @@ pub struct SandboxWorkspaceWrite { #[serde(rename_all = "snake_case")] #[ts(export_to = "v2/")] pub struct ToolsV2 { + #[serde(alias = "web_search_request")] pub web_search: Option, pub view_image: Option, } @@ -190,16 +243,13 @@ pub struct ToolsV2 { pub struct ProfileV2 { pub model: Option, pub model_provider: Option, - #[serde( - default, - deserialize_with = "deserialize_approval_policy", - serialize_with = "serialize_approval_policy" - )] pub approval_policy: Option, pub model_reasoning_effort: Option, pub model_reasoning_summary: Option, pub model_verbosity: Option, pub chatgpt_base_url: Option, + #[serde(default, flatten)] + pub additional: HashMap, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] @@ -211,17 +261,7 @@ pub struct Config { pub model_context_window: Option, pub model_auto_compact_token_limit: Option, pub model_provider: Option, - #[serde( - default, - deserialize_with = "deserialize_approval_policy", - serialize_with = "serialize_approval_policy" - )] pub approval_policy: Option, - #[serde( - default, - deserialize_with = "deserialize_sandbox_mode", - serialize_with = "serialize_sandbox_mode" - )] pub sandbox_mode: Option, pub sandbox_workspace_write: Option, pub forced_chatgpt_workspace_id: Option, @@ -240,45 +280,6 @@ pub struct Config { pub additional: HashMap, } -fn deserialize_approval_policy<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - let value = Option::::deserialize(deserializer)?; - Ok(value.map(AskForApproval::from)) -} - -fn serialize_approval_policy( - value: &Option, - serializer: S, -) -> Result -where - S: serde::Serializer, -{ - value - .as_ref() - .map(|policy| policy.to_core()) - .serialize(serializer) -} - -fn deserialize_sandbox_mode<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - let value = Option::::deserialize(deserializer)?; - Ok(value.map(SandboxMode::from)) -} - -fn serialize_sandbox_mode(value: &Option, serializer: S) -> Result -where - S: serde::Serializer, -{ - value - .as_ref() - .map(|mode| mode.to_core()) - .serialize(serializer) -} - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")]