diff --git a/README.md b/README.md index 34070a9..6f0dba8 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,22 @@ Or via npm: npx @zed-industries/codex-acp ``` +#### Options + +Use `--ignore-user-config` to start the adapter without loading +`$CODEX_HOME/config.toml`. Authentication and other Codex state still use +`CODEX_HOME`. + +This is useful for ACP clients that provide their own session configuration and +want to avoid user-level hooks or MCP servers while still using the user's Codex +login. Configuration overrides passed with `-c key=value` still apply. If your +credential store mode is normally configured in `config.toml`, pass it +explicitly, for example: + +``` +codex-acp --ignore-user-config -c cli_auth_credentials_store=auto +``` + ## License Apache-2.0 diff --git a/npm/README.md b/npm/README.md index 554dc91..2503256 100644 --- a/npm/README.md +++ b/npm/README.md @@ -56,6 +56,22 @@ Or via npm: npx @zed-industries/codex-acp ``` +#### Options + +Use `--ignore-user-config` to start the adapter without loading +`$CODEX_HOME/config.toml`. Authentication and other Codex state still use +`CODEX_HOME`. + +This is useful for ACP clients that provide their own session configuration and +want to avoid user-level hooks or MCP servers while still using the user's Codex +login. Configuration overrides passed with `-c key=value` still apply. If your +credential store mode is normally configured in `config.toml`, pass it +explicitly, for example: + +``` +codex-acp --ignore-user-config -c cli_auth_credentials_store=auto +``` + ## License Apache-2.0 diff --git a/src/lib.rs b/src/lib.rs index 595403d..397cc3b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,8 @@ #![deny(clippy::print_stdout, clippy::print_stderr)] use agent_client_protocol::ByteStreams; -use codex_core::config::{Config, ConfigOverrides}; +use codex_config::LoaderOverrides; +use codex_core::config::{Config, ConfigBuilder, ConfigOverrides}; use codex_utils_cli::CliConfigOverrides; use std::path::PathBuf; use std::sync::Arc; @@ -12,6 +13,12 @@ use tracing_subscriber::EnvFilter; mod codex_agent; mod thread; +#[derive(Debug, Clone, Copy, Default)] +pub struct RunOptions { + /// Do not load `$CODEX_HOME/config.toml`; auth still uses `CODEX_HOME`. + pub ignore_user_config: bool, +} + /// Run the Codex ACP agent. /// /// This sets up an ACP agent that communicates over stdio, bridging @@ -23,6 +30,24 @@ mod thread; pub async fn run_main( codex_linux_sandbox_exe: Option, cli_config_overrides: CliConfigOverrides, +) -> std::io::Result<()> { + run_main_with_options( + codex_linux_sandbox_exe, + cli_config_overrides, + RunOptions::default(), + ) + .await +} + +/// Run the Codex ACP agent with explicit adapter options. +/// +/// # Errors +/// +/// If unable to parse the config or start the program. +pub async fn run_main_with_options( + codex_linux_sandbox_exe: Option, + cli_config_overrides: CliConfigOverrides, + options: RunOptions, ) -> std::io::Result<()> { // Install a simple subscriber so `tracing` output is visible. // Users can control the log level with `RUST_LOG`. @@ -31,28 +56,12 @@ pub async fn run_main( .with_env_filter(EnvFilter::from_default_env()) .init(); - // Parse CLI overrides and load configuration - let cli_kv_overrides = cli_config_overrides.parse_overrides().map_err(|e| { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - format!("error parsing -c overrides: {e}"), - ) - })?; - - let config_overrides = ConfigOverrides { - codex_linux_sandbox_exe: codex_linux_sandbox_exe.clone(), - ..ConfigOverrides::default() - }; - - let config = - Config::load_with_cli_overrides_and_harness_overrides(cli_kv_overrides, config_overrides) - .await - .map_err(|e| { - std::io::Error::new( - std::io::ErrorKind::InvalidData, - format!("error loading config: {e}"), - ) - })?; + let config = load_config( + codex_linux_sandbox_exe.clone(), + cli_config_overrides, + options, + ) + .await?; // Apply residency requirement so the HTTP client sends the // x-openai-internal-codex-residency header on all requests. codex_login::default_client::set_default_client_residency_requirement( @@ -72,8 +81,151 @@ pub async fn run_main( Ok(()) } +async fn load_config( + codex_linux_sandbox_exe: Option, + cli_config_overrides: CliConfigOverrides, + options: RunOptions, +) -> std::io::Result { + let cli_kv_overrides = cli_config_overrides.parse_overrides().map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("error parsing -c overrides: {e}"), + ) + })?; + let config_overrides = ConfigOverrides { + codex_linux_sandbox_exe, + ..ConfigOverrides::default() + }; + let loader_overrides = LoaderOverrides { + ignore_user_config: options.ignore_user_config, + ..Default::default() + }; + + ConfigBuilder::default() + .cli_overrides(cli_kv_overrides) + .harness_overrides(config_overrides) + .loader_overrides(loader_overrides) + .build() + .await + .map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("error loading config: {e}"), + ) + }) +} + // Re-export the MCP server types for compatibility pub use codex_mcp_server::{ CodexToolCallParam, CodexToolCallReplyParam, ExecApprovalElicitRequestParams, ExecApprovalResponse, PatchApprovalElicitRequestParams, PatchApprovalResponse, }; + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::path::{Path, PathBuf}; + use uuid::Uuid; + + struct TestDir(PathBuf); + + impl TestDir { + fn new() -> std::io::Result { + let path = std::env::temp_dir().join(format!("codex-acp-{}", Uuid::new_v4())); + fs::create_dir_all(&path)?; + Ok(Self(path)) + } + + fn path(&self) -> &Path { + &self.0 + } + } + + impl Drop for TestDir { + fn drop(&mut self) { + drop(fs::remove_dir_all(&self.0)); + } + } + + async fn load_config_for_test( + codex_home: PathBuf, + raw_overrides: Vec<&str>, + options: RunOptions, + ) -> std::io::Result { + let cli_config_overrides = CliConfigOverrides { + raw_overrides: raw_overrides.into_iter().map(str::to_string).collect(), + }; + let cli_kv_overrides = cli_config_overrides.parse_overrides().map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("error parsing -c overrides: {e}"), + ) + })?; + let config_overrides = ConfigOverrides { + cwd: Some(codex_home.clone()), + ..ConfigOverrides::default() + }; + let mut loader_overrides = LoaderOverrides::without_managed_config_for_tests(); + loader_overrides.ignore_user_config = options.ignore_user_config; + + ConfigBuilder::default() + .codex_home(codex_home) + .cli_overrides(cli_kv_overrides) + .harness_overrides(config_overrides) + .loader_overrides(loader_overrides) + .build() + .await + } + + #[tokio::test] + async fn ignore_user_config_ignores_invalid_user_config_but_applies_cli_overrides() + -> std::io::Result<()> { + let codex_home = TestDir::new()?; + fs::write( + codex_home.path().join("config.toml"), + "model = \"from-user-config\"\ninvalid = [", + )?; + + let config = load_config_for_test( + codex_home.path().to_path_buf(), + vec!["model=\"from-cli\""], + RunOptions { + ignore_user_config: true, + }, + ) + .await?; + + assert_eq!(config.codex_home.as_path(), codex_home.path()); + assert_eq!(config.model.as_deref(), Some("from-cli")); + Ok(()) + } + + #[tokio::test] + async fn ignore_user_config_does_not_load_user_notify_or_mcp_servers() -> std::io::Result<()> { + let codex_home = TestDir::new()?; + fs::write( + codex_home.path().join("config.toml"), + r#" +notify = ["notify-send", "Codex"] + +[mcp_servers.user_config_server] +command = "node" +args = ["server.js"] +"#, + )?; + + let config = load_config_for_test( + codex_home.path().to_path_buf(), + vec![], + RunOptions { + ignore_user_config: true, + }, + ) + .await?; + + assert_eq!(config.notify, None); + assert!(!config.mcp_servers.get().contains_key("user_config_server")); + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 182167a..70c5b5f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,10 +3,54 @@ use clap::Parser; use codex_arg0::arg0_dispatch_or_else; use codex_utils_cli::CliConfigOverrides; +#[derive(Parser, Debug)] +#[command(version, about = "ACP adapter for Codex")] +struct Cli { + #[clap(flatten)] + config_overrides: CliConfigOverrides, + + /// Do not load `$CODEX_HOME/config.toml`; auth still uses `CODEX_HOME`. + #[arg(long = "ignore-user-config", default_value_t = false)] + ignore_user_config: bool, +} + fn main() -> Result<()> { arg0_dispatch_or_else(|args| async move { - let cli_config_overrides = CliConfigOverrides::parse(); - codex_acp::run_main(args.codex_linux_sandbox_exe, cli_config_overrides).await?; + let cli = Cli::parse(); + codex_acp::run_main_with_options( + args.codex_linux_sandbox_exe, + cli.config_overrides, + codex_acp::RunOptions { + ignore_user_config: cli.ignore_user_config, + }, + ) + .await?; Ok(()) }) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_ignore_user_config_with_config_overrides() { + let cli = Cli::parse_from([ + "codex-acp", + "--ignore-user-config", + "-c", + "model=gpt-5.5", + "-c", + "cli_auth_credentials_store=auto", + ]); + + assert!(cli.ignore_user_config); + assert_eq!( + cli.config_overrides.raw_overrides, + vec![ + "model=gpt-5.5".to_string(), + "cli_auth_credentials_store=auto".to_string() + ] + ); + } +}