mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +00:00
codex impl
This commit is contained in:
@@ -52,6 +52,8 @@ use codex_app_server_protocol::ServerRequestPayload;
|
||||
use codex_app_server_protocol::SessionConfiguredNotification;
|
||||
use codex_app_server_protocol::SetDefaultModelParams;
|
||||
use codex_app_server_protocol::SetDefaultModelResponse;
|
||||
use codex_app_server_protocol::UpdateConfigParams;
|
||||
use codex_app_server_protocol::UpdateConfigResponse;
|
||||
use codex_app_server_protocol::UserInfoResponse;
|
||||
use codex_app_server_protocol::UserSavedConfig;
|
||||
use codex_backend_client::Client as BackendClient;
|
||||
@@ -74,6 +76,7 @@ use codex_core::config::load_config_as_toml;
|
||||
use codex_core::config_edit::CONFIG_KEY_EFFORT;
|
||||
use codex_core::config_edit::CONFIG_KEY_MODEL;
|
||||
use codex_core::config_edit::persist_overrides_and_clear_if_none;
|
||||
use codex_core::config_edit::persist_user_saved_config;
|
||||
use codex_core::default_client::get_codex_user_agent;
|
||||
use codex_core::exec::ExecParams;
|
||||
use codex_core::exec_env::create_env;
|
||||
@@ -664,9 +667,43 @@ impl CodexMessageProcessor {
|
||||
|
||||
async fn update_user_saved_config(&self, request_id: RequestId, params: UpdateConfigParams) {
|
||||
let UpdateConfigParams { config } = params;
|
||||
let config: UserSavedConfig = config.into();
|
||||
// TODO(owen): actually persist config
|
||||
let response = UpdateConfigResponse { config };
|
||||
if let Err(err) = persist_user_saved_config(&self.config.codex_home, &config).await {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to persist config.toml: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
|
||||
let toml_value = match load_config_as_toml(&self.config.codex_home).await {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to load config.toml: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let cfg: ConfigToml = match toml_value.try_into() {
|
||||
Ok(cfg) => cfg,
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to parse config.toml: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let response = UpdateConfigResponse { config: cfg.into() };
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
use crate::config::CONFIG_TOML_FILE;
|
||||
use crate::config_types::McpServerConfig;
|
||||
use crate::config_types::McpServerTransportConfig;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
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::SandboxSettings;
|
||||
use codex_app_server_protocol::Tools;
|
||||
use codex_app_server_protocol::UserSavedConfig;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tempfile::NamedTempFile;
|
||||
use toml_edit::Array as TomlArray;
|
||||
use toml_edit::DocumentMut;
|
||||
use toml_edit::Item as TomlItem;
|
||||
use toml_edit::Table as TomlTable;
|
||||
|
||||
pub const CONFIG_KEY_MODEL: &str = "model";
|
||||
pub const CONFIG_KEY_EFFORT: &str = "model_reasoning_effort";
|
||||
@@ -49,6 +67,87 @@ pub async fn persist_overrides_and_clear_if_none(
|
||||
persist_overrides_with_behavior(codex_home, profile, overrides, NoneBehavior::Remove).await
|
||||
}
|
||||
|
||||
pub async fn persist_user_saved_config(codex_home: &Path, config: &UserSavedConfig) -> Result<()> {
|
||||
let servers = convert_mcp_servers(&config.mcp_servers)?;
|
||||
|
||||
let config_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
let existing = tokio::fs::read_to_string(&config_path).await;
|
||||
let mut doc = match existing {
|
||||
Ok(contents) => contents.parse::<DocumentMut>()?,
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => DocumentMut::new(),
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
{
|
||||
let root = doc.as_table_mut();
|
||||
set_string_option(
|
||||
root,
|
||||
"approval_policy",
|
||||
config.approval_policy.map(|value| value.to_string()),
|
||||
);
|
||||
set_string_option(
|
||||
root,
|
||||
"sandbox_mode",
|
||||
config.sandbox_mode.map(|mode| mode.to_string()),
|
||||
);
|
||||
set_sandbox_workspace_write(root, config.sandbox_settings.as_ref())?;
|
||||
set_string_option(
|
||||
root,
|
||||
"forced_chatgpt_workspace_id",
|
||||
config.forced_chatgpt_workspace_id.clone(),
|
||||
);
|
||||
set_string_option(
|
||||
root,
|
||||
"forced_login_method",
|
||||
config.forced_login_method.map(|mode| mode.to_string()),
|
||||
);
|
||||
set_string_option(root, "model", config.model.clone());
|
||||
set_string_option(
|
||||
root,
|
||||
"model_reasoning_effort",
|
||||
config
|
||||
.model_reasoning_effort
|
||||
.map(|effort| effort.to_string()),
|
||||
);
|
||||
set_string_option(
|
||||
root,
|
||||
"model_reasoning_summary",
|
||||
config
|
||||
.model_reasoning_summary
|
||||
.map(|summary| summary.to_string()),
|
||||
);
|
||||
set_string_option(
|
||||
root,
|
||||
"model_verbosity",
|
||||
config
|
||||
.model_verbosity
|
||||
.map(|verbosity| verbosity.to_string()),
|
||||
);
|
||||
set_tools(root, config.tools.as_ref())?;
|
||||
set_string_option(
|
||||
root,
|
||||
"mcp_oauth_credentials_store",
|
||||
config
|
||||
.mcp_oauth_credentials_store
|
||||
.map(protocol_oauth_mode_to_str)
|
||||
.map(String::from),
|
||||
);
|
||||
set_string_option(root, "profile", config.profile.clone());
|
||||
set_profiles(root, &config.profiles)?;
|
||||
set_mcp_servers(root, &servers)?;
|
||||
}
|
||||
|
||||
tokio::fs::create_dir_all(codex_home)
|
||||
.await
|
||||
.with_context(|| format!("failed to create Codex home at {}", codex_home.display()))?;
|
||||
|
||||
let tmp_file = NamedTempFile::new_in(codex_home)?;
|
||||
tokio::fs::write(tmp_file.path(), doc.to_string()).await?;
|
||||
tmp_file.persist(config_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply a single override onto a `toml_edit` document while preserving
|
||||
/// existing formatting/comments.
|
||||
/// The key is expressed as explicit segments to correctly handle keys that
|
||||
@@ -212,6 +311,340 @@ fn remove_toml_edit_segments(doc: &mut DocumentMut, segments: &[&str]) -> bool {
|
||||
current.remove(segments[segments.len() - 1]).is_some()
|
||||
}
|
||||
|
||||
fn set_string_option(table: &mut TomlTable, key: &str, value: Option<String>) {
|
||||
match value {
|
||||
Some(value) => {
|
||||
table[key] = toml_edit::value(value);
|
||||
}
|
||||
None => {
|
||||
table.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_sandbox_workspace_write(
|
||||
table: &mut TomlTable,
|
||||
settings: Option<&SandboxSettings>,
|
||||
) -> Result<()> {
|
||||
table.remove("sandbox_workspace_write");
|
||||
|
||||
let Some(settings) = settings else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut sandbox = TomlTable::new();
|
||||
sandbox.set_implicit(false);
|
||||
|
||||
if !settings.writable_roots.is_empty() {
|
||||
let mut roots = TomlArray::new();
|
||||
for root_path in &settings.writable_roots {
|
||||
roots.push(root_path.to_string_lossy().to_string());
|
||||
}
|
||||
sandbox["writable_roots"] = TomlItem::Value(roots.into());
|
||||
}
|
||||
|
||||
if let Some(network_access) = settings.network_access {
|
||||
sandbox["network_access"] = toml_edit::value(network_access);
|
||||
}
|
||||
|
||||
if let Some(exclude_tmpdir_env_var) = settings.exclude_tmpdir_env_var {
|
||||
sandbox["exclude_tmpdir_env_var"] = toml_edit::value(exclude_tmpdir_env_var);
|
||||
}
|
||||
|
||||
if let Some(exclude_slash_tmp) = settings.exclude_slash_tmp {
|
||||
sandbox["exclude_slash_tmp"] = toml_edit::value(exclude_slash_tmp);
|
||||
}
|
||||
|
||||
if sandbox.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
table.insert("sandbox_workspace_write", TomlItem::Table(sandbox));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_tools(table: &mut TomlTable, tools: Option<&Tools>) -> Result<()> {
|
||||
table.remove("tools");
|
||||
|
||||
let Some(tools) = tools else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut tools_table = TomlTable::new();
|
||||
tools_table.set_implicit(false);
|
||||
|
||||
if let Some(web_search) = tools.web_search {
|
||||
tools_table["web_search"] = toml_edit::value(web_search);
|
||||
}
|
||||
|
||||
if let Some(view_image) = tools.view_image {
|
||||
tools_table["view_image"] = toml_edit::value(view_image);
|
||||
}
|
||||
|
||||
if tools_table.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
table.insert("tools", TomlItem::Table(tools_table));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_profiles(table: &mut TomlTable, profiles: &HashMap<String, Profile>) -> Result<()> {
|
||||
table.remove("profiles");
|
||||
|
||||
if profiles.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut profiles_table = TomlTable::new();
|
||||
profiles_table.set_implicit(true);
|
||||
|
||||
let mut keys: Vec<_> = profiles.keys().cloned().collect();
|
||||
keys.sort();
|
||||
|
||||
for key in keys {
|
||||
let profile = profiles.get(&key).expect("profile key should exist");
|
||||
let mut profile_table = TomlTable::new();
|
||||
profile_table.set_implicit(false);
|
||||
|
||||
if let Some(model) = profile.model.clone() {
|
||||
profile_table["model"] = toml_edit::value(model);
|
||||
}
|
||||
|
||||
if let Some(model_provider) = profile.model_provider.clone() {
|
||||
profile_table["model_provider"] = toml_edit::value(model_provider);
|
||||
}
|
||||
|
||||
if let Some(approval_policy) = profile.approval_policy {
|
||||
profile_table["approval_policy"] = toml_edit::value(approval_policy.to_string());
|
||||
}
|
||||
|
||||
if let Some(effort) = profile.model_reasoning_effort {
|
||||
profile_table["model_reasoning_effort"] = toml_edit::value(effort.to_string());
|
||||
}
|
||||
|
||||
if let Some(summary) = profile.model_reasoning_summary {
|
||||
profile_table["model_reasoning_summary"] = toml_edit::value(summary.to_string());
|
||||
}
|
||||
|
||||
if let Some(verbosity) = profile.model_verbosity {
|
||||
profile_table["model_verbosity"] = toml_edit::value(verbosity.to_string());
|
||||
}
|
||||
|
||||
if let Some(chatgpt_base_url) = profile.chatgpt_base_url.clone() {
|
||||
profile_table["chatgpt_base_url"] = toml_edit::value(chatgpt_base_url);
|
||||
}
|
||||
|
||||
profiles_table.insert(&key, TomlItem::Table(profile_table));
|
||||
}
|
||||
|
||||
table.insert("profiles", TomlItem::Table(profiles_table));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_mcp_servers(
|
||||
table: &mut TomlTable,
|
||||
servers: &BTreeMap<String, McpServerConfig>,
|
||||
) -> Result<()> {
|
||||
table.remove("mcp_servers");
|
||||
|
||||
if servers.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut servers_table = TomlTable::new();
|
||||
servers_table.set_implicit(true);
|
||||
|
||||
for (name, config) in servers {
|
||||
let mut entry = TomlTable::new();
|
||||
entry.set_implicit(false);
|
||||
|
||||
match &config.transport {
|
||||
McpServerTransportConfig::Stdio {
|
||||
command,
|
||||
args,
|
||||
env,
|
||||
env_vars,
|
||||
cwd,
|
||||
} => {
|
||||
entry["command"] = toml_edit::value(command.clone());
|
||||
|
||||
if !args.is_empty() {
|
||||
let mut args_array = TomlArray::new();
|
||||
for arg in args {
|
||||
args_array.push(arg.clone());
|
||||
}
|
||||
entry["args"] = TomlItem::Value(args_array.into());
|
||||
}
|
||||
|
||||
if let Some(env) = env
|
||||
&& !env.is_empty()
|
||||
{
|
||||
let mut env_table = TomlTable::new();
|
||||
env_table.set_implicit(false);
|
||||
let mut pairs: Vec<_> = env.iter().collect();
|
||||
pairs.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
for (key, value) in pairs {
|
||||
env_table.insert(key, toml_edit::value(value.clone()));
|
||||
}
|
||||
entry["env"] = TomlItem::Table(env_table);
|
||||
}
|
||||
|
||||
if !env_vars.is_empty() {
|
||||
let mut vars = TomlArray::new();
|
||||
for var in env_vars {
|
||||
vars.push(var.clone());
|
||||
}
|
||||
entry["env_vars"] = TomlItem::Value(vars.into());
|
||||
}
|
||||
|
||||
if let Some(cwd) = cwd {
|
||||
entry["cwd"] = toml_edit::value(cwd.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
McpServerTransportConfig::StreamableHttp {
|
||||
url,
|
||||
bearer_token_env_var,
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
} => {
|
||||
entry["url"] = toml_edit::value(url.clone());
|
||||
|
||||
if let Some(env_var) = bearer_token_env_var {
|
||||
entry["bearer_token_env_var"] = toml_edit::value(env_var.clone());
|
||||
}
|
||||
|
||||
if let Some(headers) = http_headers
|
||||
&& !headers.is_empty()
|
||||
{
|
||||
let mut headers_table = TomlTable::new();
|
||||
headers_table.set_implicit(false);
|
||||
let mut pairs: Vec<_> = headers.iter().collect();
|
||||
pairs.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
for (key, value) in pairs {
|
||||
headers_table.insert(key, toml_edit::value(value.clone()));
|
||||
}
|
||||
entry["http_headers"] = TomlItem::Table(headers_table);
|
||||
}
|
||||
|
||||
if let Some(headers) = env_http_headers
|
||||
&& !headers.is_empty()
|
||||
{
|
||||
let mut headers_table = TomlTable::new();
|
||||
headers_table.set_implicit(false);
|
||||
let mut pairs: Vec<_> = headers.iter().collect();
|
||||
pairs.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
for (key, value) in pairs {
|
||||
headers_table.insert(key, toml_edit::value(value.clone()));
|
||||
}
|
||||
entry["env_http_headers"] = TomlItem::Table(headers_table);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entry["enabled"] = toml_edit::value(config.enabled);
|
||||
|
||||
if let Some(startup) = config.startup_timeout_sec {
|
||||
entry["startup_timeout_sec"] = toml_edit::value(startup.as_secs_f64());
|
||||
}
|
||||
|
||||
if let Some(tool_timeout) = config.tool_timeout_sec {
|
||||
entry["tool_timeout_sec"] = toml_edit::value(tool_timeout.as_secs_f64());
|
||||
}
|
||||
|
||||
if let Some(enabled_tools) = config.enabled_tools.as_ref()
|
||||
&& !enabled_tools.is_empty()
|
||||
{
|
||||
let mut tools = TomlArray::new();
|
||||
for tool in enabled_tools {
|
||||
tools.push(tool.clone());
|
||||
}
|
||||
entry["enabled_tools"] = TomlItem::Value(tools.into());
|
||||
}
|
||||
|
||||
if let Some(disabled_tools) = config.disabled_tools.as_ref()
|
||||
&& !disabled_tools.is_empty()
|
||||
{
|
||||
let mut tools = TomlArray::new();
|
||||
for tool in disabled_tools {
|
||||
tools.push(tool.clone());
|
||||
}
|
||||
entry["disabled_tools"] = TomlItem::Value(tools.into());
|
||||
}
|
||||
|
||||
servers_table.insert(name, TomlItem::Table(entry));
|
||||
}
|
||||
|
||||
table.insert("mcp_servers", TomlItem::Table(servers_table));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn convert_mcp_servers(
|
||||
servers: &HashMap<String, ProtocolMcpServerConfig>,
|
||||
) -> Result<BTreeMap<String, McpServerConfig>> {
|
||||
let mut result = BTreeMap::new();
|
||||
for (name, config) in servers {
|
||||
result.insert(name.clone(), convert_mcp_server_config(config)?);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn convert_mcp_server_config(config: &ProtocolMcpServerConfig) -> Result<McpServerConfig> {
|
||||
let transport = match &config.transport {
|
||||
ProtocolMcpServerTransportConfig::Stdio {
|
||||
command,
|
||||
args,
|
||||
env,
|
||||
env_vars,
|
||||
cwd,
|
||||
} => McpServerTransportConfig::Stdio {
|
||||
command: command.clone(),
|
||||
args: args.clone(),
|
||||
env: env.clone(),
|
||||
env_vars: env_vars.clone(),
|
||||
cwd: cwd.clone(),
|
||||
},
|
||||
ProtocolMcpServerTransportConfig::StreamableHttp {
|
||||
url,
|
||||
bearer_token_env_var,
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
} => McpServerTransportConfig::StreamableHttp {
|
||||
url: url.clone(),
|
||||
bearer_token_env_var: bearer_token_env_var.clone(),
|
||||
http_headers: http_headers.clone(),
|
||||
env_http_headers: env_http_headers.clone(),
|
||||
},
|
||||
};
|
||||
|
||||
Ok(McpServerConfig {
|
||||
transport,
|
||||
enabled: config.enabled,
|
||||
startup_timeout_sec: config
|
||||
.startup_timeout_sec
|
||||
.map(duration_from_secs)
|
||||
.transpose()?,
|
||||
tool_timeout_sec: config
|
||||
.tool_timeout_sec
|
||||
.map(duration_from_secs)
|
||||
.transpose()?,
|
||||
enabled_tools: config.enabled_tools.clone(),
|
||||
disabled_tools: config.disabled_tools.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn duration_from_secs(value: f64) -> Result<Duration> {
|
||||
Duration::try_from_secs_f64(value).map_err(|err| anyhow!(err))
|
||||
}
|
||||
|
||||
fn protocol_oauth_mode_to_str(mode: McpOAuthCredentialsStoreMode) -> &'static str {
|
||||
match mode {
|
||||
McpOAuthCredentialsStoreMode::Auto => "auto",
|
||||
McpOAuthCredentialsStoreMode::File => "file",
|
||||
McpOAuthCredentialsStoreMode::Keyring => "keyring",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user