mirror of
https://github.com/openai/codex.git
synced 2026-04-20 20:54:48 +00:00
Compare commits
4 Commits
dev/window
...
dev/cc/tmp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50437ca302 | ||
|
|
e591cde2d5 | ||
|
|
c1162dedca | ||
|
|
303ea308bc |
@@ -13,6 +13,7 @@ use std::time::Duration;
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
|
use clap::ArgAction;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
use codex_app_server_protocol::AddConversationListenerParams;
|
use codex_app_server_protocol::AddConversationListenerParams;
|
||||||
@@ -65,6 +66,19 @@ struct Cli {
|
|||||||
#[arg(long, env = "CODEX_BIN", default_value = "codex")]
|
#[arg(long, env = "CODEX_BIN", default_value = "codex")]
|
||||||
codex_bin: String,
|
codex_bin: String,
|
||||||
|
|
||||||
|
/// Forwarded to the `codex` CLI as `--config key=value`. Repeatable.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// `--config 'model_providers.mock.base_url="http://localhost:4010/v2"'`
|
||||||
|
#[arg(
|
||||||
|
short = 'c',
|
||||||
|
long = "config",
|
||||||
|
value_name = "key=value",
|
||||||
|
action = ArgAction::Append,
|
||||||
|
global = true
|
||||||
|
)]
|
||||||
|
config_overrides: Vec<String>,
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: CliCommand,
|
command: CliCommand,
|
||||||
}
|
}
|
||||||
@@ -116,29 +130,42 @@ enum CliCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let Cli { codex_bin, command } = Cli::parse();
|
let Cli {
|
||||||
|
codex_bin,
|
||||||
|
config_overrides,
|
||||||
|
command,
|
||||||
|
} = Cli::parse();
|
||||||
|
|
||||||
match command {
|
match command {
|
||||||
CliCommand::SendMessage { user_message } => send_message(codex_bin, user_message),
|
CliCommand::SendMessage { user_message } => {
|
||||||
CliCommand::SendMessageV2 { user_message } => send_message_v2(codex_bin, user_message),
|
send_message(&codex_bin, &config_overrides, user_message)
|
||||||
|
}
|
||||||
|
CliCommand::SendMessageV2 { user_message } => {
|
||||||
|
send_message_v2(&codex_bin, &config_overrides, user_message)
|
||||||
|
}
|
||||||
CliCommand::TriggerCmdApproval { user_message } => {
|
CliCommand::TriggerCmdApproval { user_message } => {
|
||||||
trigger_cmd_approval(codex_bin, user_message)
|
trigger_cmd_approval(&codex_bin, &config_overrides, user_message)
|
||||||
}
|
}
|
||||||
CliCommand::TriggerPatchApproval { user_message } => {
|
CliCommand::TriggerPatchApproval { user_message } => {
|
||||||
trigger_patch_approval(codex_bin, user_message)
|
trigger_patch_approval(&codex_bin, &config_overrides, user_message)
|
||||||
}
|
}
|
||||||
CliCommand::NoTriggerCmdApproval => no_trigger_cmd_approval(codex_bin),
|
CliCommand::NoTriggerCmdApproval => no_trigger_cmd_approval(&codex_bin, &config_overrides),
|
||||||
CliCommand::SendFollowUpV2 {
|
CliCommand::SendFollowUpV2 {
|
||||||
first_message,
|
first_message,
|
||||||
follow_up_message,
|
follow_up_message,
|
||||||
} => send_follow_up_v2(codex_bin, first_message, follow_up_message),
|
} => send_follow_up_v2(
|
||||||
CliCommand::TestLogin => test_login(codex_bin),
|
&codex_bin,
|
||||||
CliCommand::GetAccountRateLimits => get_account_rate_limits(codex_bin),
|
&config_overrides,
|
||||||
|
first_message,
|
||||||
|
follow_up_message,
|
||||||
|
),
|
||||||
|
CliCommand::TestLogin => test_login(&codex_bin, &config_overrides),
|
||||||
|
CliCommand::GetAccountRateLimits => get_account_rate_limits(&codex_bin, &config_overrides),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_message(codex_bin: String, user_message: String) -> Result<()> {
|
fn send_message(codex_bin: &str, config_overrides: &[String], user_message: String) -> Result<()> {
|
||||||
let mut client = CodexClient::spawn(codex_bin)?;
|
let mut client = CodexClient::spawn(codex_bin, config_overrides)?;
|
||||||
|
|
||||||
let initialize = client.initialize()?;
|
let initialize = client.initialize()?;
|
||||||
println!("< initialize response: {initialize:?}");
|
println!("< initialize response: {initialize:?}");
|
||||||
@@ -159,46 +186,61 @@ fn send_message(codex_bin: String, user_message: String) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_message_v2(codex_bin: String, user_message: String) -> Result<()> {
|
fn send_message_v2(
|
||||||
send_message_v2_with_policies(codex_bin, user_message, None, None)
|
codex_bin: &str,
|
||||||
|
config_overrides: &[String],
|
||||||
|
user_message: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
send_message_v2_with_policies(codex_bin, config_overrides, user_message, None, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn trigger_cmd_approval(codex_bin: String, user_message: Option<String>) -> Result<()> {
|
fn trigger_cmd_approval(
|
||||||
|
codex_bin: &str,
|
||||||
|
config_overrides: &[String],
|
||||||
|
user_message: Option<String>,
|
||||||
|
) -> Result<()> {
|
||||||
let default_prompt =
|
let default_prompt =
|
||||||
"Run `touch /tmp/should-trigger-approval` so I can confirm the file exists.";
|
"Run `touch /tmp/should-trigger-approval` so I can confirm the file exists.";
|
||||||
let message = user_message.unwrap_or_else(|| default_prompt.to_string());
|
let message = user_message.unwrap_or_else(|| default_prompt.to_string());
|
||||||
send_message_v2_with_policies(
|
send_message_v2_with_policies(
|
||||||
codex_bin,
|
codex_bin,
|
||||||
|
config_overrides,
|
||||||
message,
|
message,
|
||||||
Some(AskForApproval::OnRequest),
|
Some(AskForApproval::OnRequest),
|
||||||
Some(SandboxPolicy::ReadOnly),
|
Some(SandboxPolicy::ReadOnly),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn trigger_patch_approval(codex_bin: String, user_message: Option<String>) -> Result<()> {
|
fn trigger_patch_approval(
|
||||||
|
codex_bin: &str,
|
||||||
|
config_overrides: &[String],
|
||||||
|
user_message: Option<String>,
|
||||||
|
) -> Result<()> {
|
||||||
let default_prompt =
|
let default_prompt =
|
||||||
"Create a file named APPROVAL_DEMO.txt containing a short hello message using apply_patch.";
|
"Create a file named APPROVAL_DEMO.txt containing a short hello message using apply_patch.";
|
||||||
let message = user_message.unwrap_or_else(|| default_prompt.to_string());
|
let message = user_message.unwrap_or_else(|| default_prompt.to_string());
|
||||||
send_message_v2_with_policies(
|
send_message_v2_with_policies(
|
||||||
codex_bin,
|
codex_bin,
|
||||||
|
config_overrides,
|
||||||
message,
|
message,
|
||||||
Some(AskForApproval::OnRequest),
|
Some(AskForApproval::OnRequest),
|
||||||
Some(SandboxPolicy::ReadOnly),
|
Some(SandboxPolicy::ReadOnly),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn no_trigger_cmd_approval(codex_bin: String) -> Result<()> {
|
fn no_trigger_cmd_approval(codex_bin: &str, config_overrides: &[String]) -> Result<()> {
|
||||||
let prompt = "Run `touch should_not_trigger_approval.txt`";
|
let prompt = "Run `touch should_not_trigger_approval.txt`";
|
||||||
send_message_v2_with_policies(codex_bin, prompt.to_string(), None, None)
|
send_message_v2_with_policies(codex_bin, config_overrides, prompt.to_string(), None, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_message_v2_with_policies(
|
fn send_message_v2_with_policies(
|
||||||
codex_bin: String,
|
codex_bin: &str,
|
||||||
|
config_overrides: &[String],
|
||||||
user_message: String,
|
user_message: String,
|
||||||
approval_policy: Option<AskForApproval>,
|
approval_policy: Option<AskForApproval>,
|
||||||
sandbox_policy: Option<SandboxPolicy>,
|
sandbox_policy: Option<SandboxPolicy>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut client = CodexClient::spawn(codex_bin)?;
|
let mut client = CodexClient::spawn(codex_bin, config_overrides)?;
|
||||||
|
|
||||||
let initialize = client.initialize()?;
|
let initialize = client.initialize()?;
|
||||||
println!("< initialize response: {initialize:?}");
|
println!("< initialize response: {initialize:?}");
|
||||||
@@ -222,11 +264,12 @@ fn send_message_v2_with_policies(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn send_follow_up_v2(
|
fn send_follow_up_v2(
|
||||||
codex_bin: String,
|
codex_bin: &str,
|
||||||
|
config_overrides: &[String],
|
||||||
first_message: String,
|
first_message: String,
|
||||||
follow_up_message: String,
|
follow_up_message: String,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut client = CodexClient::spawn(codex_bin)?;
|
let mut client = CodexClient::spawn(codex_bin, config_overrides)?;
|
||||||
|
|
||||||
let initialize = client.initialize()?;
|
let initialize = client.initialize()?;
|
||||||
println!("< initialize response: {initialize:?}");
|
println!("< initialize response: {initialize:?}");
|
||||||
@@ -259,8 +302,8 @@ fn send_follow_up_v2(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_login(codex_bin: String) -> Result<()> {
|
fn test_login(codex_bin: &str, config_overrides: &[String]) -> Result<()> {
|
||||||
let mut client = CodexClient::spawn(codex_bin)?;
|
let mut client = CodexClient::spawn(codex_bin, config_overrides)?;
|
||||||
|
|
||||||
let initialize = client.initialize()?;
|
let initialize = client.initialize()?;
|
||||||
println!("< initialize response: {initialize:?}");
|
println!("< initialize response: {initialize:?}");
|
||||||
@@ -289,8 +332,8 @@ fn test_login(codex_bin: String) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_account_rate_limits(codex_bin: String) -> Result<()> {
|
fn get_account_rate_limits(codex_bin: &str, config_overrides: &[String]) -> Result<()> {
|
||||||
let mut client = CodexClient::spawn(codex_bin)?;
|
let mut client = CodexClient::spawn(codex_bin, config_overrides)?;
|
||||||
|
|
||||||
let initialize = client.initialize()?;
|
let initialize = client.initialize()?;
|
||||||
println!("< initialize response: {initialize:?}");
|
println!("< initialize response: {initialize:?}");
|
||||||
@@ -309,8 +352,12 @@ struct CodexClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CodexClient {
|
impl CodexClient {
|
||||||
fn spawn(codex_bin: String) -> Result<Self> {
|
fn spawn(codex_bin: &str, config_overrides: &[String]) -> Result<Self> {
|
||||||
let mut codex_app_server = Command::new(&codex_bin)
|
let mut cmd = Command::new(codex_bin);
|
||||||
|
for override_kv in config_overrides {
|
||||||
|
cmd.arg("--config").arg(override_kv);
|
||||||
|
}
|
||||||
|
let mut codex_app_server = cmd
|
||||||
.arg("app-server")
|
.arg("app-server")
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ Example (from OpenAI's official VSCode extension):
|
|||||||
- `mcpServerStatus/list` — enumerate configured MCP servers with their tools, resources, resource templates, and auth status; supports cursor+limit pagination.
|
- `mcpServerStatus/list` — enumerate configured MCP servers with their tools, resources, resource templates, and auth status; supports cursor+limit pagination.
|
||||||
- `feedback/upload` — submit a feedback report (classification + optional reason/logs and conversation_id); returns the tracking thread id.
|
- `feedback/upload` — submit a feedback report (classification + optional reason/logs and conversation_id); returns the tracking thread id.
|
||||||
- `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation).
|
- `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation).
|
||||||
- `config/read` — fetch the effective config on disk after resolving config layering.
|
- `config/read` — fetch the effective config on disk after resolving config layering (thread-agnostic; does not include in-repo `.codex/` layers).
|
||||||
- `config/value/write` — write a single config key/value to the user's config.toml on disk.
|
- `config/value/write` — write a single config key/value to the user's config.toml on disk.
|
||||||
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk.
|
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::bespoke_event_handling::apply_bespoke_event_handling;
|
use crate::bespoke_event_handling::apply_bespoke_event_handling;
|
||||||
|
use crate::config_api::ConfigApi;
|
||||||
use crate::error_code::INTERNAL_ERROR_CODE;
|
use crate::error_code::INTERNAL_ERROR_CODE;
|
||||||
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
||||||
use crate::fuzzy_file_search::run_fuzzy_file_search;
|
use crate::fuzzy_file_search::run_fuzzy_file_search;
|
||||||
@@ -155,7 +156,6 @@ use codex_protocol::protocol::SessionMetaLine;
|
|||||||
use codex_protocol::protocol::USER_MESSAGE_BEGIN;
|
use codex_protocol::protocol::USER_MESSAGE_BEGIN;
|
||||||
use codex_protocol::user_input::UserInput as CoreInputItem;
|
use codex_protocol::user_input::UserInput as CoreInputItem;
|
||||||
use codex_rmcp_client::perform_oauth_login_return_url;
|
use codex_rmcp_client::perform_oauth_login_return_url;
|
||||||
use codex_utils_json_to_toml::json_to_toml;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
@@ -215,7 +215,7 @@ pub(crate) struct CodexMessageProcessor {
|
|||||||
outgoing: Arc<OutgoingMessageSender>,
|
outgoing: Arc<OutgoingMessageSender>,
|
||||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||||
config: Arc<Config>,
|
config: Arc<Config>,
|
||||||
cli_overrides: Vec<(String, TomlValue)>,
|
config_api: ConfigApi,
|
||||||
conversation_listeners: HashMap<Uuid, oneshot::Sender<()>>,
|
conversation_listeners: HashMap<Uuid, oneshot::Sender<()>>,
|
||||||
active_login: Arc<Mutex<Option<ActiveLogin>>>,
|
active_login: Arc<Mutex<Option<ActiveLogin>>>,
|
||||||
// Queue of pending interrupt requests per conversation. We reply when TurnAborted arrives.
|
// Queue of pending interrupt requests per conversation. We reply when TurnAborted arrives.
|
||||||
@@ -265,13 +265,14 @@ impl CodexMessageProcessor {
|
|||||||
cli_overrides: Vec<(String, TomlValue)>,
|
cli_overrides: Vec<(String, TomlValue)>,
|
||||||
feedback: CodexFeedback,
|
feedback: CodexFeedback,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let config_api = ConfigApi::new(config.codex_home.clone(), cli_overrides.clone());
|
||||||
Self {
|
Self {
|
||||||
auth_manager,
|
auth_manager,
|
||||||
conversation_manager,
|
conversation_manager,
|
||||||
outgoing,
|
outgoing,
|
||||||
codex_linux_sandbox_exe,
|
codex_linux_sandbox_exe,
|
||||||
config,
|
config,
|
||||||
cli_overrides,
|
config_api,
|
||||||
conversation_listeners: HashMap::new(),
|
conversation_listeners: HashMap::new(),
|
||||||
active_login: Arc::new(Mutex::new(None)),
|
active_login: Arc::new(Mutex::new(None)),
|
||||||
pending_interrupts: Arc::new(Mutex::new(HashMap::new())),
|
pending_interrupts: Arc::new(Mutex::new(HashMap::new())),
|
||||||
@@ -282,13 +283,7 @@ impl CodexMessageProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn load_latest_config(&self) -> Result<Config, JSONRPCErrorError> {
|
async fn load_latest_config(&self) -> Result<Config, JSONRPCErrorError> {
|
||||||
Config::load_with_cli_overrides(self.cli_overrides.clone())
|
self.config_api.load_latest_thread_agnostic_config().await
|
||||||
.await
|
|
||||||
.map_err(|err| JSONRPCErrorError {
|
|
||||||
code: INTERNAL_ERROR_CODE,
|
|
||||||
message: format!("failed to reload config: {err}"),
|
|
||||||
data: None,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn review_request_from_target(
|
fn review_request_from_target(
|
||||||
@@ -1278,18 +1273,20 @@ impl CodexMessageProcessor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = match derive_config_from_params(overrides, Some(cli_overrides)).await {
|
let config =
|
||||||
Ok(config) => config,
|
match derive_config_from_params(&self.config_api, overrides, Some(cli_overrides)).await
|
||||||
Err(err) => {
|
{
|
||||||
let error = JSONRPCErrorError {
|
Ok(config) => config,
|
||||||
code: INVALID_REQUEST_ERROR_CODE,
|
Err(err) => {
|
||||||
message: format!("error deriving config: {err}"),
|
let error = JSONRPCErrorError {
|
||||||
data: None,
|
code: INVALID_REQUEST_ERROR_CODE,
|
||||||
};
|
message: format!("error deriving config: {err}"),
|
||||||
self.outgoing.send_error(request_id, error).await;
|
data: None,
|
||||||
return;
|
};
|
||||||
}
|
self.outgoing.send_error(request_id, error).await;
|
||||||
};
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
match self.conversation_manager.new_conversation(config).await {
|
match self.conversation_manager.new_conversation(config).await {
|
||||||
Ok(conversation_id) => {
|
Ok(conversation_id) => {
|
||||||
@@ -1328,18 +1325,19 @@ impl CodexMessageProcessor {
|
|||||||
params.developer_instructions,
|
params.developer_instructions,
|
||||||
);
|
);
|
||||||
|
|
||||||
let config = match derive_config_from_params(overrides, params.config).await {
|
let config =
|
||||||
Ok(config) => config,
|
match derive_config_from_params(&self.config_api, overrides, params.config).await {
|
||||||
Err(err) => {
|
Ok(config) => config,
|
||||||
let error = JSONRPCErrorError {
|
Err(err) => {
|
||||||
code: INVALID_REQUEST_ERROR_CODE,
|
let error = JSONRPCErrorError {
|
||||||
message: format!("error deriving config: {err}"),
|
code: INVALID_REQUEST_ERROR_CODE,
|
||||||
data: None,
|
message: format!("error deriving config: {err}"),
|
||||||
};
|
data: None,
|
||||||
self.outgoing.send_error(request_id, error).await;
|
};
|
||||||
return;
|
self.outgoing.send_error(request_id, error).await;
|
||||||
}
|
return;
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
match self.conversation_manager.new_conversation(config).await {
|
match self.conversation_manager.new_conversation(config).await {
|
||||||
Ok(new_conv) => {
|
Ok(new_conv) => {
|
||||||
@@ -1567,7 +1565,7 @@ impl CodexMessageProcessor {
|
|||||||
base_instructions,
|
base_instructions,
|
||||||
developer_instructions,
|
developer_instructions,
|
||||||
);
|
);
|
||||||
match derive_config_from_params(overrides, cli_overrides).await {
|
match derive_config_from_params(&self.config_api, overrides, cli_overrides).await {
|
||||||
Ok(config) => config,
|
Ok(config) => config,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let error = JSONRPCErrorError {
|
let error = JSONRPCErrorError {
|
||||||
@@ -2228,7 +2226,7 @@ impl CodexMessageProcessor {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
derive_config_from_params(overrides, Some(cli_overrides)).await
|
derive_config_from_params(&self.config_api, overrides, Some(cli_overrides)).await
|
||||||
}
|
}
|
||||||
None => Ok(self.config.as_ref().clone()),
|
None => Ok(self.config.as_ref().clone()),
|
||||||
};
|
};
|
||||||
@@ -3344,16 +3342,13 @@ fn errors_to_info(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn derive_config_from_params(
|
async fn derive_config_from_params(
|
||||||
|
config_api: &ConfigApi,
|
||||||
overrides: ConfigOverrides,
|
overrides: ConfigOverrides,
|
||||||
cli_overrides: Option<HashMap<String, serde_json::Value>>,
|
cli_overrides: Option<HashMap<String, serde_json::Value>>,
|
||||||
) -> std::io::Result<Config> {
|
) -> std::io::Result<Config> {
|
||||||
let cli_overrides = cli_overrides
|
config_api
|
||||||
.unwrap_or_default()
|
.load_thread_agnostic_config(overrides, cli_overrides)
|
||||||
.into_iter()
|
.await
|
||||||
.map(|(k, v)| (k, json_to_toml(v)))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Config::load_with_cli_overrides_and_harness_overrides(cli_overrides, overrides).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn read_summary_from_rollout(
|
async fn read_summary_from_rollout(
|
||||||
|
|||||||
@@ -7,21 +7,28 @@ use codex_app_server_protocol::ConfigValueWriteParams;
|
|||||||
use codex_app_server_protocol::ConfigWriteErrorCode;
|
use codex_app_server_protocol::ConfigWriteErrorCode;
|
||||||
use codex_app_server_protocol::ConfigWriteResponse;
|
use codex_app_server_protocol::ConfigWriteResponse;
|
||||||
use codex_app_server_protocol::JSONRPCErrorError;
|
use codex_app_server_protocol::JSONRPCErrorError;
|
||||||
|
use codex_core::config::Config;
|
||||||
|
use codex_core::config::ConfigBuilder;
|
||||||
use codex_core::config::ConfigService;
|
use codex_core::config::ConfigService;
|
||||||
use codex_core::config::ConfigServiceError;
|
use codex_core::config::ConfigServiceError;
|
||||||
|
use codex_utils_json_to_toml::json_to_toml;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use toml::Value as TomlValue;
|
use toml::Value as TomlValue;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct ConfigApi {
|
pub(crate) struct ConfigApi {
|
||||||
|
codex_home: PathBuf,
|
||||||
|
cli_overrides: Vec<(String, TomlValue)>,
|
||||||
service: ConfigService,
|
service: ConfigService,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigApi {
|
impl ConfigApi {
|
||||||
pub(crate) fn new(codex_home: PathBuf, cli_overrides: Vec<(String, TomlValue)>) -> Self {
|
pub(crate) fn new(codex_home: PathBuf, cli_overrides: Vec<(String, TomlValue)>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
service: ConfigService::new(codex_home, cli_overrides),
|
service: ConfigService::new(codex_home.clone(), cli_overrides.clone()),
|
||||||
|
codex_home,
|
||||||
|
cli_overrides,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +39,30 @@ impl ConfigApi {
|
|||||||
self.service.read(params).await.map_err(map_error)
|
self.service.read(params).await.map_err(map_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn load_thread_agnostic_config(
|
||||||
|
&self,
|
||||||
|
overrides: codex_core::config::ConfigOverrides,
|
||||||
|
request_cli_overrides: Option<std::collections::HashMap<String, serde_json::Value>>,
|
||||||
|
) -> std::io::Result<Config> {
|
||||||
|
// Apply the app server's startup `--config` overrides, then apply request-scoped overrides
|
||||||
|
// with higher precedence.
|
||||||
|
let mut merged_cli_overrides = self.cli_overrides.clone();
|
||||||
|
merged_cli_overrides.extend(
|
||||||
|
request_cli_overrides
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k, json_to_toml(v))),
|
||||||
|
);
|
||||||
|
|
||||||
|
ConfigBuilder::default()
|
||||||
|
.codex_home(self.codex_home.clone())
|
||||||
|
.cli_overrides(merged_cli_overrides)
|
||||||
|
.harness_overrides(overrides)
|
||||||
|
.thread_agnostic()
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn write_value(
|
pub(crate) async fn write_value(
|
||||||
&self,
|
&self,
|
||||||
params: ConfigValueWriteParams,
|
params: ConfigValueWriteParams,
|
||||||
@@ -45,6 +76,18 @@ impl ConfigApi {
|
|||||||
) -> Result<ConfigWriteResponse, JSONRPCErrorError> {
|
) -> Result<ConfigWriteResponse, JSONRPCErrorError> {
|
||||||
self.service.batch_write(params).await.map_err(map_error)
|
self.service.batch_write(params).await.map_err(map_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn load_latest_thread_agnostic_config(
|
||||||
|
&self,
|
||||||
|
) -> Result<Config, JSONRPCErrorError> {
|
||||||
|
self.load_thread_agnostic_config(codex_core::config::ConfigOverrides::default(), None)
|
||||||
|
.await
|
||||||
|
.map_err(|err| JSONRPCErrorError {
|
||||||
|
code: INTERNAL_ERROR_CODE,
|
||||||
|
message: format!("failed to reload config: {err}"),
|
||||||
|
data: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_error(err: ConfigServiceError) -> JSONRPCErrorError {
|
fn map_error(err: ConfigServiceError) -> JSONRPCErrorError {
|
||||||
|
|||||||
@@ -363,6 +363,7 @@ pub struct ConfigBuilder {
|
|||||||
cli_overrides: Option<Vec<(String, TomlValue)>>,
|
cli_overrides: Option<Vec<(String, TomlValue)>>,
|
||||||
harness_overrides: Option<ConfigOverrides>,
|
harness_overrides: Option<ConfigOverrides>,
|
||||||
loader_overrides: Option<LoaderOverrides>,
|
loader_overrides: Option<LoaderOverrides>,
|
||||||
|
thread_agnostic: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigBuilder {
|
impl ConfigBuilder {
|
||||||
@@ -371,6 +372,13 @@ impl ConfigBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load a "thread-agnostic" config stack, which intentionally ignores any
|
||||||
|
/// in-repo `.codex/` config layers (because there is no cwd/project context).
|
||||||
|
pub fn thread_agnostic(mut self) -> Self {
|
||||||
|
self.thread_agnostic = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn cli_overrides(mut self, cli_overrides: Vec<(String, TomlValue)>) -> Self {
|
pub fn cli_overrides(mut self, cli_overrides: Vec<(String, TomlValue)>) -> Self {
|
||||||
self.cli_overrides = Some(cli_overrides);
|
self.cli_overrides = Some(cli_overrides);
|
||||||
self
|
self
|
||||||
@@ -392,18 +400,22 @@ impl ConfigBuilder {
|
|||||||
cli_overrides,
|
cli_overrides,
|
||||||
harness_overrides,
|
harness_overrides,
|
||||||
loader_overrides,
|
loader_overrides,
|
||||||
|
thread_agnostic,
|
||||||
} = self;
|
} = self;
|
||||||
let codex_home = codex_home.map_or_else(find_codex_home, std::io::Result::Ok)?;
|
let codex_home = codex_home.map_or_else(find_codex_home, std::io::Result::Ok)?;
|
||||||
let cli_overrides = cli_overrides.unwrap_or_default();
|
let cli_overrides = cli_overrides.unwrap_or_default();
|
||||||
let harness_overrides = harness_overrides.unwrap_or_default();
|
let harness_overrides = harness_overrides.unwrap_or_default();
|
||||||
let loader_overrides = loader_overrides.unwrap_or_default();
|
let loader_overrides = loader_overrides.unwrap_or_default();
|
||||||
let cwd = match harness_overrides.cwd.as_deref() {
|
let cwd = if thread_agnostic {
|
||||||
Some(path) => AbsolutePathBuf::try_from(path)?,
|
None
|
||||||
None => AbsolutePathBuf::current_dir()?,
|
} else {
|
||||||
|
Some(match harness_overrides.cwd.as_deref() {
|
||||||
|
Some(path) => AbsolutePathBuf::try_from(path)?,
|
||||||
|
None => AbsolutePathBuf::current_dir()?,
|
||||||
|
})
|
||||||
};
|
};
|
||||||
let config_layer_stack =
|
let config_layer_stack =
|
||||||
load_config_layers_state(&codex_home, Some(cwd), &cli_overrides, loader_overrides)
|
load_config_layers_state(&codex_home, cwd, &cli_overrides, loader_overrides).await?;
|
||||||
.await?;
|
|
||||||
let merged_toml = config_layer_stack.effective_config();
|
let merged_toml = config_layer_stack.effective_config();
|
||||||
|
|
||||||
// Note that each layer in ConfigLayerStack should have resolved
|
// Note that each layer in ConfigLayerStack should have resolved
|
||||||
@@ -2082,6 +2094,43 @@ trust_level = "trusted"
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn config_builder_thread_agnostic_ignores_project_layers() -> anyhow::Result<()> {
|
||||||
|
let tmp = TempDir::new()?;
|
||||||
|
let codex_home = tmp.path().join("codex_home");
|
||||||
|
std::fs::create_dir_all(&codex_home)?;
|
||||||
|
std::fs::write(codex_home.join(CONFIG_TOML_FILE), "model = \"from-user\"\n")?;
|
||||||
|
|
||||||
|
let project = tmp.path().join("project");
|
||||||
|
std::fs::create_dir_all(project.join(".codex"))?;
|
||||||
|
std::fs::write(
|
||||||
|
project.join(".codex").join(CONFIG_TOML_FILE),
|
||||||
|
"model = \"from-project\"\n",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let harness_overrides = ConfigOverrides {
|
||||||
|
cwd: Some(project),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let with_project_layers = ConfigBuilder::default()
|
||||||
|
.codex_home(codex_home.clone())
|
||||||
|
.harness_overrides(harness_overrides.clone())
|
||||||
|
.build()
|
||||||
|
.await?;
|
||||||
|
assert_eq!(with_project_layers.model.as_deref(), Some("from-project"));
|
||||||
|
|
||||||
|
let thread_agnostic = ConfigBuilder::default()
|
||||||
|
.codex_home(codex_home)
|
||||||
|
.harness_overrides(harness_overrides)
|
||||||
|
.thread_agnostic()
|
||||||
|
.build()
|
||||||
|
.await?;
|
||||||
|
assert_eq!(thread_agnostic.model.as_deref(), Some("from-user"));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn load_global_mcp_servers_returns_empty_if_missing() -> anyhow::Result<()> {
|
async fn load_global_mcp_servers_returns_empty_if_missing() -> anyhow::Result<()> {
|
||||||
let codex_home = TempDir::new()?;
|
let codex_home = TempDir::new()?;
|
||||||
|
|||||||
Reference in New Issue
Block a user