Skip to content

Commit bfb4d57

Browse files
authored
[app-server-protocol] Add types for config (#7658)
Currently the config returned by `config/read` in untyped. Add types so it's easier for client to parse the config. Since currently configs are all defined in snake case we'll keep that instead of using camel case like the rest of V2. Sample output by testing using the app server test client: ``` { < "id": "f28449f4-b015-459b-b07b-eef06980165d", < "result": { < "config": { < "approvalPolicy": null, < "compactPrompt": null, < "developerInstructions": null, < "features": { < "experimental_use_rmcp_client": true < }, < "forcedChatgptWorkspaceId": null, < "forcedLoginMethod": null, < "instructions": null, < "model": "gpt-5.1-codex-max", < "modelAutoCompactTokenLimit": null, < "modelContextWindow": null, < "modelProvider": null, < "modelReasoningEffort": null, < "modelReasoningSummary": null, < "modelVerbosity": null, < "model_providers": { < "local": { < "base_url": "http://localhost:8061/api/codex", < "env_http_headers": { < "ChatGPT-Account-ID": "OPENAI_ACCOUNT_ID" < }, < "env_key": "CHATGPT_TOKEN_STAGING", < "name": "local", < "wire_api": "responses" < } < }, < "model_reasoning_effort": "medium", < "notice": { < "hide_gpt-5.1-codex-max_migration_prompt": true, < "hide_gpt5_1_migration_prompt": true < }, < "profile": null, < "profiles": {}, < "projects": { < "/Users/celia/code": { < "trust_level": "trusted" < }, < "/Users/celia/code/codex": { < "trust_level": "trusted" < }, < "/Users/celia/code/openai": { < "trust_level": "trusted" < } < }, < "reviewModel": null, < "sandboxMode": null, < "sandboxWorkspaceWrite": null, < "tools": { < "viewImage": null, < "webSearch": null < } < }, < "origins": { < "features.experimental_use_rmcp_client": { < "name": "user", < "source": "/Users/celia/.codex/config.toml", < "version": "sha256:a1d8eaedb5d9db5dfdfa69f30fa9df2efec66bb4dd46aa67f149fcc67cd0711c" < }, < "model": { < "name": "user", < "source": "/Users/celia/.codex/config.toml", < "version": "sha256:a1d8eaedb5d9db5dfdfa69f30fa9df2efec66bb4dd46aa67f149fcc67cd0711c" < }, < "model_providers.local.base_url": { < "name": "user", < "source": "/Users/celia/.codex/config.toml", < "version": "sha256:a1d8eaedb5d9db5dfdfa69f30fa9df2efec66bb4dd46aa67f149fcc67cd0711c" < }, < "model_providers.local.env_http_headers.ChatGPT-Account-ID": { < "name": "user", < "source": "/Users/celia/.codex/config.toml", < "version": "sha256:a1d8eaedb5d9db5dfdfa69f30fa9df2efec66bb4dd46aa67f149fcc67cd0711c" < }, < "model_providers.local.env_key": { < "name": "user", < "source": "/Users/celia/.codex/config.toml", < "version": "sha256:a1d8eaedb5d9db5dfdfa69f30fa9df2efec66bb4dd46aa67f149fcc67cd0711c" < }, < "model_providers.local.name": { < "name": "user", < "source": "/Users/celia/.codex/config.toml", < "version": "sha256:a1d8eaedb5d9db5dfdfa69f30fa9df2efec66bb4dd46aa67f149fcc67cd0711c" < }, < "model_providers.local.wire_api": { < "name": "user", < "source": "/Users/celia/.codex/config.toml", < "version": "sha256:a1d8eaedb5d9db5dfdfa69f30fa9df2efec66bb4dd46aa67f149fcc67cd0711c" < }, < "model_reasoning_effort": { < "name": "user", < "source": "/Users/celia/.codex/config.toml", < "version": "sha256:a1d8eaedb5d9db5dfdfa69f30fa9df2efec66bb4dd46aa67f149fcc67cd0711c" < }, < "notice.hide_gpt-5.1-codex-max_migration_prompt": { < "name": "user", < "source": "/Users/celia/.codex/config.toml", < "version": "sha256:a1d8eaedb5d9db5dfdfa69f30fa9df2efec66bb4dd46aa67f149fcc67cd0711c" < }, < "notice.hide_gpt5_1_migration_prompt": { < "name": "user", < "source": "/Users/celia/.codex/config.toml", < "version": "sha256:a1d8eaedb5d9db5dfdfa69f30fa9df2efec66bb4dd46aa67f149fcc67cd0711c" < }, < "projects./Users/celia/code.trust_level": { < "name": "user", < "source": "/Users/celia/.codex/config.toml", < "version": "sha256:a1d8eaedb5d9db5dfdfa69f30fa9df2efec66bb4dd46aa67f149fcc67cd0711c" < }, < "projects./Users/celia/code/codex.trust_level": { < "name": "user", < "source": "/Users/celia/.codex/config.toml", < "version": "sha256:a1d8eaedb5d9db5dfdfa69f30fa9df2efec66bb4dd46aa67f149fcc67cd0711c" < }, < "projects./Users/celia/code/openai.trust_level": { < "name": "user", < "source": "/Users/celia/.codex/config.toml", < "version": "sha256:a1d8eaedb5d9db5dfdfa69f30fa9df2efec66bb4dd46aa67f149fcc67cd0711c" < }, < "tools.web_search": { < "name": "user", < "source": "/Users/celia/.codex/config.toml", < "version": "sha256:a1d8eaedb5d9db5dfdfa69f30fa9df2efec66bb4dd46aa67f149fcc67cd0711c" < } < } < } < } ```
1 parent 4953b2a commit bfb4d57

File tree

3 files changed

+222
-50
lines changed

3 files changed

+222
-50
lines changed

codex-rs/app-server-protocol/src/protocol/v2.rs

Lines changed: 130 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@ use std::path::PathBuf;
44
use crate::protocol::common::AuthMode;
55
use codex_protocol::account::PlanType;
66
use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment;
7+
use codex_protocol::config_types::ForcedLoginMethod;
78
use codex_protocol::config_types::ReasoningSummary;
9+
use codex_protocol::config_types::SandboxMode as CoreSandboxMode;
10+
use codex_protocol::config_types::Verbosity;
811
use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent;
912
use codex_protocol::items::TurnItem as CoreTurnItem;
1013
use codex_protocol::models::ResponseItem;
1114
use codex_protocol::openai_models::ReasoningEffort;
1215
use codex_protocol::parse_command::ParsedCommand as CoreParsedCommand;
1316
use codex_protocol::plan_tool::PlanItemArg as CorePlanItemArg;
1417
use codex_protocol::plan_tool::StepStatus as CorePlanStepStatus;
18+
use codex_protocol::protocol::AskForApproval as CoreAskForApproval;
1519
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
1620
use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot;
1721
use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot;
@@ -122,17 +126,68 @@ impl From<CoreCodexErrorInfo> for CodexErrorInfo {
122126
}
123127
}
124128

125-
v2_enum_from_core!(
126-
pub enum AskForApproval from codex_protocol::protocol::AskForApproval {
127-
UnlessTrusted, OnFailure, OnRequest, Never
129+
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
130+
#[serde(rename_all = "kebab-case")]
131+
#[ts(rename_all = "kebab-case", export_to = "v2/")]
132+
pub enum AskForApproval {
133+
#[serde(rename = "untrusted")]
134+
#[ts(rename = "untrusted")]
135+
UnlessTrusted,
136+
OnFailure,
137+
OnRequest,
138+
Never,
139+
}
140+
141+
impl AskForApproval {
142+
pub fn to_core(self) -> CoreAskForApproval {
143+
match self {
144+
AskForApproval::UnlessTrusted => CoreAskForApproval::UnlessTrusted,
145+
AskForApproval::OnFailure => CoreAskForApproval::OnFailure,
146+
AskForApproval::OnRequest => CoreAskForApproval::OnRequest,
147+
AskForApproval::Never => CoreAskForApproval::Never,
148+
}
128149
}
129-
);
150+
}
130151

131-
v2_enum_from_core!(
132-
pub enum SandboxMode from codex_protocol::config_types::SandboxMode {
133-
ReadOnly, WorkspaceWrite, DangerFullAccess
152+
impl From<CoreAskForApproval> for AskForApproval {
153+
fn from(value: CoreAskForApproval) -> Self {
154+
match value {
155+
CoreAskForApproval::UnlessTrusted => AskForApproval::UnlessTrusted,
156+
CoreAskForApproval::OnFailure => AskForApproval::OnFailure,
157+
CoreAskForApproval::OnRequest => AskForApproval::OnRequest,
158+
CoreAskForApproval::Never => AskForApproval::Never,
159+
}
134160
}
135-
);
161+
}
162+
163+
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
164+
#[serde(rename_all = "kebab-case")]
165+
#[ts(rename_all = "kebab-case", export_to = "v2/")]
166+
pub enum SandboxMode {
167+
ReadOnly,
168+
WorkspaceWrite,
169+
DangerFullAccess,
170+
}
171+
172+
impl SandboxMode {
173+
pub fn to_core(self) -> CoreSandboxMode {
174+
match self {
175+
SandboxMode::ReadOnly => CoreSandboxMode::ReadOnly,
176+
SandboxMode::WorkspaceWrite => CoreSandboxMode::WorkspaceWrite,
177+
SandboxMode::DangerFullAccess => CoreSandboxMode::DangerFullAccess,
178+
}
179+
}
180+
}
181+
182+
impl From<CoreSandboxMode> for SandboxMode {
183+
fn from(value: CoreSandboxMode) -> Self {
184+
match value {
185+
CoreSandboxMode::ReadOnly => SandboxMode::ReadOnly,
186+
CoreSandboxMode::WorkspaceWrite => SandboxMode::WorkspaceWrite,
187+
CoreSandboxMode::DangerFullAccess => SandboxMode::DangerFullAccess,
188+
}
189+
}
190+
}
136191

137192
v2_enum_from_core!(
138193
pub enum ReviewDelivery from codex_protocol::protocol::ReviewDelivery {
@@ -159,6 +214,72 @@ pub enum ConfigLayerName {
159214
User,
160215
}
161216

217+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
218+
#[serde(rename_all = "snake_case")]
219+
#[ts(export_to = "v2/")]
220+
pub struct SandboxWorkspaceWrite {
221+
#[serde(default)]
222+
pub writable_roots: Vec<PathBuf>,
223+
#[serde(default)]
224+
pub network_access: bool,
225+
#[serde(default)]
226+
pub exclude_tmpdir_env_var: bool,
227+
#[serde(default)]
228+
pub exclude_slash_tmp: bool,
229+
}
230+
231+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
232+
#[serde(rename_all = "snake_case")]
233+
#[ts(export_to = "v2/")]
234+
pub struct ToolsV2 {
235+
#[serde(alias = "web_search_request")]
236+
pub web_search: Option<bool>,
237+
pub view_image: Option<bool>,
238+
}
239+
240+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
241+
#[serde(rename_all = "snake_case")]
242+
#[ts(export_to = "v2/")]
243+
pub struct ProfileV2 {
244+
pub model: Option<String>,
245+
pub model_provider: Option<String>,
246+
pub approval_policy: Option<AskForApproval>,
247+
pub model_reasoning_effort: Option<ReasoningEffort>,
248+
pub model_reasoning_summary: Option<ReasoningSummary>,
249+
pub model_verbosity: Option<Verbosity>,
250+
pub chatgpt_base_url: Option<String>,
251+
#[serde(default, flatten)]
252+
pub additional: HashMap<String, JsonValue>,
253+
}
254+
255+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
256+
#[serde(rename_all = "snake_case")]
257+
#[ts(export_to = "v2/")]
258+
pub struct Config {
259+
pub model: Option<String>,
260+
pub review_model: Option<String>,
261+
pub model_context_window: Option<i64>,
262+
pub model_auto_compact_token_limit: Option<i64>,
263+
pub model_provider: Option<String>,
264+
pub approval_policy: Option<AskForApproval>,
265+
pub sandbox_mode: Option<SandboxMode>,
266+
pub sandbox_workspace_write: Option<SandboxWorkspaceWrite>,
267+
pub forced_chatgpt_workspace_id: Option<String>,
268+
pub forced_login_method: Option<ForcedLoginMethod>,
269+
pub tools: Option<ToolsV2>,
270+
pub profile: Option<String>,
271+
#[serde(default)]
272+
pub profiles: HashMap<String, ProfileV2>,
273+
pub instructions: Option<String>,
274+
pub developer_instructions: Option<String>,
275+
pub compact_prompt: Option<String>,
276+
pub model_reasoning_effort: Option<ReasoningEffort>,
277+
pub model_reasoning_summary: Option<ReasoningSummary>,
278+
pub model_verbosity: Option<Verbosity>,
279+
#[serde(default, flatten)]
280+
pub additional: HashMap<String, JsonValue>,
281+
}
282+
162283
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
163284
#[serde(rename_all = "camelCase")]
164285
#[ts(export_to = "v2/")]
@@ -237,7 +358,7 @@ pub struct ConfigReadParams {
237358
#[serde(rename_all = "camelCase")]
238359
#[ts(export_to = "v2/")]
239360
pub struct ConfigReadResponse {
240-
pub config: JsonValue,
361+
pub config: Config,
241362
pub origins: HashMap<String, ConfigLayerMetadata>,
242363
#[serde(skip_serializing_if = "Option::is_none")]
243364
pub layers: Option<Vec<ConfigLayer>>,

codex-rs/app-server/src/config_api.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::error_code::INTERNAL_ERROR_CODE;
22
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
3+
use codex_app_server_protocol::Config;
34
use codex_app_server_protocol::ConfigBatchWriteParams;
45
use codex_app_server_protocol::ConfigLayer;
56
use codex_app_server_protocol::ConfigLayerMetadata;
@@ -75,8 +76,10 @@ impl ConfigApi {
7576
let effective = layers.effective_config();
7677
validate_config(&effective).map_err(|err| internal_error("invalid configuration", err))?;
7778

79+
let config: Config = serde_json::from_value(to_json_value(&effective))
80+
.map_err(|err| internal_error("failed to deserialize configuration", err))?;
7881
let response = ConfigReadResponse {
79-
config: to_json_value(&effective),
82+
config,
8083
origins: layers.origins(),
8184
layers: params.include_layers.then(|| layers.layers_high_to_low()),
8285
};
@@ -773,6 +776,7 @@ fn config_write_error(code: ConfigWriteErrorCode, message: impl Into<String>) ->
773776
mod tests {
774777
use super::*;
775778
use anyhow::Result;
779+
use codex_app_server_protocol::AskForApproval;
776780
use pretty_assertions::assert_eq;
777781
use tempfile::tempdir;
778782

@@ -895,10 +899,7 @@ remote_compaction = true
895899
.await
896900
.expect("response");
897901

898-
assert_eq!(
899-
response.config.get("approval_policy"),
900-
Some(&json!("never"))
901-
);
902+
assert_eq!(response.config.approval_policy, Some(AskForApproval::Never));
902903

903904
assert_eq!(
904905
response
@@ -953,8 +954,10 @@ remote_compaction = true
953954
})
954955
.await
955956
.expect("read");
956-
let config_object = read_after.config.as_object().expect("object");
957-
assert_eq!(config_object.get("approval_policy"), Some(&json!("never")));
957+
assert_eq!(
958+
read_after.config.approval_policy,
959+
Some(AskForApproval::Never)
960+
);
958961
assert_eq!(
959962
read_after
960963
.origins
@@ -1093,7 +1096,7 @@ remote_compaction = true
10931096
.await
10941097
.expect("response");
10951098

1096-
assert_eq!(response.config.get("model"), Some(&json!("system")));
1099+
assert_eq!(response.config.model.as_deref(), Some("system"));
10971100
assert_eq!(
10981101
response.origins.get("model").expect("origin").name,
10991102
ConfigLayerName::System

0 commit comments

Comments
 (0)