Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 121 additions & 1 deletion codex-rs/app-server-protocol/src/protocol/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ use crate::protocol::common::AuthMode;
use codex_protocol::account::PlanType;
use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment;
use codex_protocol::approvals::SandboxCommandAssessment as CoreSandboxCommandAssessment;
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;
use codex_protocol::items::TurnItem as CoreTurnItem;
use codex_protocol::models::ResponseItem;
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;
Expand Down Expand Up @@ -160,6 +163,123 @@ 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<PathBuf>,
#[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 ToolsV2 {
pub web_search: Option<bool>,
pub view_image: Option<bool>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export_to = "v2/")]
pub struct ProfileV2 {
pub model: Option<String>,
pub model_provider: Option<String>,
#[serde(
default,
deserialize_with = "deserialize_approval_policy",
serialize_with = "serialize_approval_policy"
)]
pub approval_policy: Option<AskForApproval>,
pub model_reasoning_effort: Option<ReasoningEffort>,
pub model_reasoning_summary: Option<ReasoningSummary>,
pub model_verbosity: Option<Verbosity>,
pub chatgpt_base_url: Option<String>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export_to = "v2/")]
pub struct Config {
pub model: Option<String>,
pub review_model: Option<String>,
pub model_context_window: Option<i64>,
pub model_auto_compact_token_limit: Option<i64>,
pub model_provider: Option<String>,
#[serde(
default,
deserialize_with = "deserialize_approval_policy",
serialize_with = "serialize_approval_policy"
)]
pub approval_policy: Option<AskForApproval>,
#[serde(
default,
deserialize_with = "deserialize_sandbox_mode",
serialize_with = "serialize_sandbox_mode"
)]
pub sandbox_mode: Option<SandboxMode>,
pub sandbox_workspace_write: Option<SandboxWorkspaceWrite>,
pub forced_chatgpt_workspace_id: Option<String>,
pub forced_login_method: Option<ForcedLoginMethod>,
pub tools: Option<ToolsV2>,
pub profile: Option<String>,
#[serde(default)]
pub profiles: HashMap<String, ProfileV2>,
pub instructions: Option<String>,
pub developer_instructions: Option<String>,
pub compact_prompt: Option<String>,
pub model_reasoning_effort: Option<ReasoningEffort>,
pub model_reasoning_summary: Option<ReasoningSummary>,
pub model_verbosity: Option<Verbosity>,
#[serde(default, flatten)]
pub additional: HashMap<String, JsonValue>,
}

fn deserialize_approval_policy<'de, D>(deserializer: D) -> Result<Option<AskForApproval>, D::Error>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need all those custom serializers? (make sure to just explain in comment)

where
D: serde::Deserializer<'de>,
{
let value = Option::<CoreAskForApproval>::deserialize(deserializer)?;
Ok(value.map(AskForApproval::from))
}

fn serialize_approval_policy<S>(
value: &Option<AskForApproval>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
value
.as_ref()
.map(|policy| policy.to_core())
.serialize(serializer)
Comment on lines +258 to +262
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Config enum values serialized in kebab-case, schema still camelCase

Config now serializes approval/sandbox enums via the core helpers (serialize_approval_policy/serialize_sandbox_mode), which emit the kebab-case strings used in TOML (e.g., "on-request", "workspace-write"). The exported AskForApproval/SandboxMode enums in this file remain camelCase (via v2_enum_from_core!), so the generated V2 TypeScript schema will describe camelCase literals while config.read returns kebab-case. Typed clients using the generated bindings will reject the actual payload unless the serialization or schema is made consistent.

Useful? React with 👍 / 👎.

}

fn deserialize_sandbox_mode<'de, D>(deserializer: D) -> Result<Option<SandboxMode>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Option::<codex_protocol::config_types::SandboxMode>::deserialize(deserializer)?;
Ok(value.map(SandboxMode::from))
}

fn serialize_sandbox_mode<S>(value: &Option<SandboxMode>, serializer: S) -> Result<S::Ok, S::Error>
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/")]
Expand Down Expand Up @@ -238,7 +358,7 @@ pub struct ConfigReadParams {
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ConfigReadResponse {
pub config: JsonValue,
pub config: Config,
pub origins: HashMap<String, ConfigLayerMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub layers: Option<Vec<ConfigLayer>>,
Expand Down
19 changes: 11 additions & 8 deletions codex-rs/app-server/src/config_api.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::error_code::INTERNAL_ERROR_CODE;
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
use anyhow::anyhow;
use codex_app_server_protocol::Config;
use codex_app_server_protocol::ConfigBatchWriteParams;
use codex_app_server_protocol::ConfigLayer;
use codex_app_server_protocol::ConfigLayerMetadata;
Expand Down Expand Up @@ -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()),
};
Expand Down Expand Up @@ -735,6 +738,7 @@ fn config_write_error(code: ConfigWriteErrorCode, message: impl Into<String>) ->
#[cfg(test)]
mod tests {
use super::*;
use codex_app_server_protocol::AskForApproval;
use pretty_assertions::assert_eq;
use tempfile::tempdir;

Expand Down Expand Up @@ -763,10 +767,7 @@ mod tests {
.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
Expand Down Expand Up @@ -821,8 +822,10 @@ mod tests {
})
.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
Expand Down Expand Up @@ -961,7 +964,7 @@ mod tests {
.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
Expand Down
Loading
Loading