codex impl

This commit is contained in:
Owen Lin
2025-10-24 15:10:36 -07:00
parent 19146ee74e
commit 6379bcadf6
5 changed files with 200 additions and 3 deletions
@@ -78,6 +78,9 @@ macro_rules! for_each_schema_type {
$macro!(crate::LoginChatGptResponse);
$macro!(crate::LogoutChatGptParams);
$macro!(crate::LogoutChatGptResponse);
$macro!(crate::McpOAuthCredentialsStoreMode);
$macro!(crate::McpServerConfig);
$macro!(crate::McpServerTransportConfig);
$macro!(crate::NewConversationParams);
$macro!(crate::NewConversationResponse);
$macro!(crate::Profile);
+63 -3
View File
@@ -134,13 +134,15 @@ client_request_definitions! {
#[ts(rename = "config/read")]
GetConfig {
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
response: Config,
response: UserSavedConfig,
},
#[serde(rename = "config/update")]
#[ts(rename = "config/update")]
UpdateConfig {
params: UpdateConfigParams,
// TODO(owen): or should we return UserSavedConfig directly?
// First, figure out how we want to represent errors.
response: UpdateConfigResponse,
},
@@ -426,13 +428,13 @@ pub struct LogoutAccountResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct UpdateConfigParams {
pub config: Config,
pub config: UserSavedConfig,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct UpdateConfigResponse {
pub config: Config,
pub config: UserSavedConfig,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -652,6 +654,12 @@ pub struct UserSavedConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Tools>,
/// MCP servers
#[serde(default)]
pub mcp_servers: HashMap<String, McpServerConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mcp_oauth_credentials_store: Option<McpOAuthCredentialsStoreMode>,
/// Profiles
#[serde(skip_serializing_if = "Option::is_none")]
pub profile: Option<String>,
@@ -673,6 +681,58 @@ pub struct Profile {
pub model_verbosity: Option<Verbosity>,
pub chatgpt_base_url: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct McpServerConfig {
pub transport: McpServerTransportConfig,
#[serde(default)]
pub enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub startup_timeout_sec: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_timeout_sec: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled_tools: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub disabled_tools: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum McpServerTransportConfig {
#[serde(rename_all = "camelCase")]
Stdio {
command: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
args: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
env: Option<HashMap<String, String>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
env_vars: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
cwd: Option<PathBuf>,
},
#[serde(rename_all = "camelCase")]
StreamableHttp {
url: String,
#[serde(skip_serializing_if = "Option::is_none")]
bearer_token_env_var: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
http_headers: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
env_http_headers: Option<HashMap<String, String>>,
},
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS, Default)]
#[serde(rename_all = "lowercase")]
pub enum McpOAuthCredentialsStoreMode {
#[default]
Auto,
File,
Keyring,
}
/// MCP representation of a [`codex_core::config::ToolsToml`].
#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
+71
View File
@@ -5,6 +5,9 @@ use app_test_support::McpProcess;
use app_test_support::to_response;
use codex_app_server_protocol::GetUserSavedConfigResponse;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::McpOAuthCredentialsStoreMode;
use codex_app_server_protocol::McpServerConfig as ProtocolMcpServerConfig;
use codex_app_server_protocol::McpServerTransportConfig as ProtocolMcpServerTransportConfig;
use codex_app_server_protocol::Profile;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SandboxSettings;
@@ -36,6 +39,7 @@ model_verbosity = "medium"
profile = "test"
forced_chatgpt_workspace_id = "12345678-0000-0000-0000-000000000000"
forced_login_method = "chatgpt"
mcp_oauth_credentials_store = "keyring"
[sandbox_workspace_write]
writable_roots = ["/tmp"]
@@ -55,6 +59,32 @@ model_reasoning_summary = "detailed"
model_verbosity = "medium"
model_provider = "openai"
chatgpt_base_url = "https://api.chatgpt.com"
[mcp_servers.docs]
command = "codex-docs"
args = ["serve"]
env_vars = ["DOCS_TOKEN"]
cwd = "/tmp/docs"
startup_timeout_sec = 12.5
tool_timeout_sec = 42.0
enabled = false
enabled_tools = ["read_docs"]
disabled_tools = ["delete_docs"]
[mcp_servers.docs.env]
PLAN = "gold"
[mcp_servers.issues]
url = "https://example.com/mcp"
bearer_token_env_var = "MCP_TOKEN"
startup_timeout_sec = 30.0
tool_timeout_sec = 15.0
[mcp_servers.issues.http_headers]
"X-Test" = "42"
[mcp_servers.issues.env_http_headers]
"X-Token" = "TOKEN_ENV"
"#,
)
}
@@ -105,6 +135,45 @@ async fn get_config_toml_parses_all_fields() {
web_search: Some(false),
view_image: Some(true),
}),
mcp_servers: HashMap::from([
(
"docs".into(),
ProtocolMcpServerConfig {
transport: ProtocolMcpServerTransportConfig::Stdio {
command: "codex-docs".into(),
args: vec!["serve".into()],
env: Some(HashMap::from([("PLAN".into(), "gold".into())])),
env_vars: vec!["DOCS_TOKEN".into()],
cwd: Some("/tmp/docs".into()),
},
enabled: false,
startup_timeout_sec: Some(12.5),
tool_timeout_sec: Some(42.0),
enabled_tools: Some(vec!["read_docs".into()]),
disabled_tools: Some(vec!["delete_docs".into()]),
},
),
(
"issues".into(),
ProtocolMcpServerConfig {
transport: ProtocolMcpServerTransportConfig::StreamableHttp {
url: "https://example.com/mcp".into(),
bearer_token_env_var: Some("MCP_TOKEN".into()),
http_headers: Some(HashMap::from([("X-Test".into(), "42".into())])),
env_http_headers: Some(HashMap::from([(
"X-Token".into(),
"TOKEN_ENV".into(),
)])),
},
enabled: true,
startup_timeout_sec: Some(30.0),
tool_timeout_sec: Some(15.0),
enabled_tools: None,
disabled_tools: None,
},
),
]),
mcp_oauth_credentials_store: Some(McpOAuthCredentialsStoreMode::Keyring),
profile: Some("test".to_string()),
profiles: HashMap::from([(
"test".into(),
@@ -161,6 +230,8 @@ async fn get_config_toml_empty() {
model_reasoning_summary: None,
model_verbosity: None,
tools: None,
mcp_servers: HashMap::new(),
mcp_oauth_credentials_store: None,
profile: None,
profiles: HashMap::new(),
},
+17
View File
@@ -967,6 +967,11 @@ impl From<ConfigToml> for UserSavedConfig {
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect();
let mcp_servers = config_toml
.mcp_servers
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect();
Self {
approval_policy: config_toml.approval_policy,
@@ -979,12 +984,24 @@ impl From<ConfigToml> for UserSavedConfig {
model_reasoning_summary: config_toml.model_reasoning_summary,
model_verbosity: config_toml.model_verbosity,
tools: config_toml.tools.map(From::from),
mcp_servers,
mcp_oauth_credentials_store: config_toml.mcp_oauth_credentials_store.map(From::from),
profile: config_toml.profile,
profiles,
}
}
}
impl From<OAuthCredentialsStoreMode> for codex_app_server_protocol::McpOAuthCredentialsStoreMode {
fn from(mode: OAuthCredentialsStoreMode) -> Self {
match mode {
OAuthCredentialsStoreMode::Auto => Self::Auto,
OAuthCredentialsStoreMode::File => Self::File,
OAuthCredentialsStoreMode::Keyring => Self::Keyring,
}
}
}
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct ProjectConfig {
pub trust_level: Option<String>,
+46
View File
@@ -388,6 +388,52 @@ impl From<SandboxWorkspaceWrite> for codex_app_server_protocol::SandboxSettings
}
}
impl From<McpServerConfig> for codex_app_server_protocol::McpServerConfig {
fn from(cfg: McpServerConfig) -> Self {
Self {
transport: cfg.transport.into(),
enabled: cfg.enabled,
startup_timeout_sec: cfg
.startup_timeout_sec
.map(|duration| duration.as_secs_f64()),
tool_timeout_sec: cfg.tool_timeout_sec.map(|duration| duration.as_secs_f64()),
enabled_tools: cfg.enabled_tools,
disabled_tools: cfg.disabled_tools,
}
}
}
impl From<McpServerTransportConfig> for codex_app_server_protocol::McpServerTransportConfig {
fn from(cfg: McpServerTransportConfig) -> Self {
match cfg {
McpServerTransportConfig::Stdio {
command,
args,
env,
env_vars,
cwd,
} => Self::Stdio {
command,
args,
env,
env_vars,
cwd,
},
McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var,
http_headers,
env_http_headers,
} => Self::StreamableHttp {
url,
bearer_token_env_var,
http_headers,
env_http_headers,
},
}
}
}
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
#[serde(rename_all = "kebab-case")]
pub enum ShellEnvironmentPolicyInherit {