From e70beb5b934464fddc4f1376adf5edab344a0e8e Mon Sep 17 00:00:00 2001 From: Channing Conger Date: Mon, 17 Nov 2025 16:56:00 -0800 Subject: [PATCH] Refactor to commoncli type so clap can merge --- codex-rs/app-server/src/lib.rs | 3 +- codex-rs/app-server/src/main.rs | 7 +- codex-rs/cli/src/main.rs | 254 ++++++++++++++++++++---------- codex-rs/common/src/common_cli.rs | 108 +++++++++++++ codex-rs/common/src/lib.rs | 6 + codex-rs/exec/src/cli.rs | 51 +----- codex-rs/exec/src/lib.rs | 22 ++- codex-rs/tui/src/cli.rs | 55 +------ codex-rs/tui/src/lib.rs | 35 ++-- 9 files changed, 336 insertions(+), 205 deletions(-) create mode 100644 codex-rs/common/src/common_cli.rs diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index 4b65e66d2c..c680d534cd 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -44,6 +44,7 @@ const CHANNEL_CAPACITY: usize = 128; pub async fn run_main( codex_linux_sandbox_exe: Option, cli_config_overrides: CliConfigOverrides, + config_overrides: ConfigOverrides, ) -> IoResult<()> { // Set up channels. let (incoming_tx, mut incoming_rx) = mpsc::channel::(CHANNEL_CAPACITY); @@ -80,7 +81,7 @@ pub async fn run_main( format!("error parsing -c overrides: {e}"), ) })?; - let config = Config::load_with_cli_overrides(cli_kv_overrides, ConfigOverrides::default()) + let config = Config::load_with_cli_overrides(cli_kv_overrides, config_overrides) .await .map_err(|e| { std::io::Error::new(ErrorKind::InvalidData, format!("error loading config: {e}")) diff --git a/codex-rs/app-server/src/main.rs b/codex-rs/app-server/src/main.rs index 689ec0877a..7c61db0003 100644 --- a/codex-rs/app-server/src/main.rs +++ b/codex-rs/app-server/src/main.rs @@ -4,7 +4,12 @@ use codex_common::CliConfigOverrides; fn main() -> anyhow::Result<()> { arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { - run_main(codex_linux_sandbox_exe, CliConfigOverrides::default()).await?; + run_main( + codex_linux_sandbox_exe, + CliConfigOverrides::default(), + codex_core::config::ConfigOverrides::default(), + ) + .await?; Ok(()) }) } diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index cbb5cb507f..02c8d44b15 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -16,7 +16,10 @@ use codex_cli::login::run_login_with_chatgpt; use codex_cli::login::run_login_with_device_code; use codex_cli::login::run_logout; use codex_cloud_tasks::Cli as CloudTasksCli; +use codex_common::ApprovalModeCliArg; use codex_common::CliConfigOverrides; +use codex_common::CommonCli; +use codex_common::oss::get_default_model_for_oss_provider; use codex_exec::Cli as ExecCli; use codex_responses_api_proxy::Args as ResponsesApiProxyArgs; use codex_tui::AppExitInfo; @@ -32,9 +35,16 @@ mod wsl_paths; use crate::mcp_cmd::McpCli; +use codex_core::LMSTUDIO_OSS_PROVIDER_ID; +use codex_core::OLLAMA_OSS_PROVIDER_ID; use codex_core::config::Config; use codex_core::config::ConfigOverrides; +use codex_core::config::find_codex_home; +use codex_core::config::load_config_as_toml_with_cli_overrides; +use codex_core::config::resolve_oss_provider; use codex_core::features::is_known_feature_key; +use codex_core::protocol::AskForApproval; +use codex_protocol::config_types::SandboxMode; /// Codex CLI /// @@ -207,6 +217,20 @@ struct LogoutCommand { #[derive(Debug, Parser)] struct AppServerCommand { + #[clap(flatten)] + common: CommonCli, + + #[clap(flatten)] + config_overrides: CliConfigOverrides, + + /// Configure when the model requires human approval before executing a command. + #[arg(long = "ask-for-approval", short = 'a')] + approval_policy: Option, + + /// Enable web search (off by default). When enabled, the native Responses `web_search` tool is available to the model (no per‑call approval). + #[arg(long = "search", default_value_t = false)] + web_search: bool, + /// Omit to run the app server; specify a subcommand for tooling. #[command(subcommand)] subcommand: Option, @@ -417,7 +441,6 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() handle_app_exit(exit_info)?; } Some(Subcommand::Exec(mut exec_cli)) => { - merge_interactive_flags_into_exec(&mut exec_cli, &interactive); prepend_config_flags( &mut exec_cli.config_overrides, root_config_overrides.clone(), @@ -434,7 +457,29 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() } Some(Subcommand::AppServer(app_server_cli)) => match app_server_cli.subcommand { None => { - codex_app_server::run_main(codex_linux_sandbox_exe, root_config_overrides).await?; + let mut cli_config_overrides = app_server_cli.config_overrides; + if app_server_cli.web_search { + cli_config_overrides + .raw_overrides + .push("features.web_search_request=true".to_string()); + } + prepend_config_flags(&mut cli_config_overrides, root_config_overrides.clone()); + let cli_kv_overrides = cli_config_overrides + .parse_overrides() + .map_err(anyhow::Error::msg)?; + let config_overrides = app_server_config_overrides_from_common( + &app_server_cli.common, + app_server_cli.approval_policy, + cli_kv_overrides, + codex_linux_sandbox_exe.clone(), + ) + .await?; + codex_app_server::run_main( + codex_linux_sandbox_exe, + cli_config_overrides, + config_overrides, + ) + .await?; } Some(AppServerSubcommand::GenerateTs(gen_cli)) => { codex_app_server_protocol::generate_ts( @@ -577,7 +622,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() // Thread through relevant top-level flags (at minimum, `--profile`). let overrides = ConfigOverrides { - config_profile: interactive.config_profile.clone(), + config_profile: interactive.common.config_profile.clone(), ..Default::default() }; @@ -606,40 +651,83 @@ fn prepend_config_flags( .splice(0..0, cli_config_overrides.raw_overrides); } -/// Propagate top-level flags (parsed into the interactive CLI) into the exec subcommand -/// unless the exec-specific flags were explicitly set. -fn merge_interactive_flags_into_exec(exec_cli: &mut ExecCli, interactive: &TuiCli) { - if exec_cli.model.is_none() { - exec_cli.model = interactive.model.clone(); - } - if !exec_cli.oss { - exec_cli.oss = interactive.oss; - } - if exec_cli.oss_provider.is_none() { - exec_cli.oss_provider = interactive.oss_provider.clone(); - } - if exec_cli.config_profile.is_none() { - exec_cli.config_profile = interactive.config_profile.clone(); - } - if exec_cli.sandbox_mode.is_none() { - exec_cli.sandbox_mode = interactive.sandbox_mode; - } - if !exec_cli.full_auto { - exec_cli.full_auto = interactive.full_auto; - } - if !exec_cli.dangerously_bypass_approvals_and_sandbox { - exec_cli.dangerously_bypass_approvals_and_sandbox = - interactive.dangerously_bypass_approvals_and_sandbox; - } - if exec_cli.cwd.is_none() { - exec_cli.cwd = interactive.cwd.clone(); - } - if exec_cli.images.is_empty() { - exec_cli.images = interactive.images.clone(); - } - if exec_cli.add_dir.is_empty() { - exec_cli.add_dir = interactive.add_dir.clone(); - } +async fn app_server_config_overrides_from_common( + common: &CommonCli, + approval_policy: Option, + cli_kv_overrides: Vec<(String, toml::Value)>, + codex_linux_sandbox_exe: Option, +) -> anyhow::Result { + let (sandbox_mode, approval_policy) = if common.full_auto { + ( + Some(SandboxMode::WorkspaceWrite), + Some(AskForApproval::OnRequest), + ) + } else if common.dangerously_bypass_approvals_and_sandbox { + ( + Some(SandboxMode::DangerFullAccess), + Some(AskForApproval::Never), + ) + } else { + ( + common.sandbox_mode.map(Into::into), + approval_policy.map(Into::into), + ) + }; + + let model_provider = if common.oss { + let codex_home = find_codex_home()?; + let config_toml = + load_config_as_toml_with_cli_overrides(&codex_home, cli_kv_overrides).await?; + Some( + resolve_oss_provider( + common.oss_provider.as_deref(), + &config_toml, + common.config_profile.clone(), + ) + .ok_or_else(|| { + anyhow::anyhow!(format!( + "No default OSS provider configured. Use --local-provider=provider or set oss_provider to either {LMSTUDIO_OSS_PROVIDER_ID} or {OLLAMA_OSS_PROVIDER_ID} in config.toml" + )) + })?, + ) + } else { + None + }; + + let model = if let Some(model) = &common.model { + Some(model.clone()) + } else if common.oss { + model_provider + .as_deref() + .and_then(get_default_model_for_oss_provider) + .map(std::borrow::ToOwned::to_owned) + } else { + None + }; + + let cwd = common + .cwd + .as_ref() + .map(|p| p.canonicalize().unwrap_or_else(|_| p.to_path_buf())); + + Ok(ConfigOverrides { + model, + review_model: None, + cwd, + approval_policy, + sandbox_mode, + model_provider, + config_profile: common.config_profile.clone(), + codex_linux_sandbox_exe, + base_instructions: None, + developer_instructions: None, + compact_prompt: None, + include_apply_patch_tool: None, + show_raw_agent_reasoning: common.oss.then_some(true), + tools_web_search_request: None, + experimental_sandbox_command_assessment: None, + additional_writable_roots: common.add_dir.clone(), + }) } /// Build the final `TuiCli` for a `codex resume` invocation. @@ -670,39 +758,13 @@ fn finalize_resume_interactive( /// root-level flags. Only overrides fields explicitly set on the resume-scoped /// CLI. Also appends `-c key=value` overrides with highest precedence. fn merge_resume_cli_flags(interactive: &mut TuiCli, resume_cli: TuiCli) { - if let Some(model) = resume_cli.model { - interactive.model = Some(model); - } - if resume_cli.oss { - interactive.oss = true; - } - if let Some(profile) = resume_cli.config_profile { - interactive.config_profile = Some(profile); - } - if let Some(sandbox) = resume_cli.sandbox_mode { - interactive.sandbox_mode = Some(sandbox); - } + interactive.common.apply_overrides(&resume_cli.common); if let Some(approval) = resume_cli.approval_policy { interactive.approval_policy = Some(approval); } - if resume_cli.full_auto { - interactive.full_auto = true; - } - if resume_cli.dangerously_bypass_approvals_and_sandbox { - interactive.dangerously_bypass_approvals_and_sandbox = true; - } - if let Some(cwd) = resume_cli.cwd { - interactive.cwd = Some(cwd); - } if resume_cli.web_search { interactive.web_search = true; } - if !resume_cli.images.is_empty() { - interactive.images = resume_cli.images; - } - if !resume_cli.add_dir.is_empty() { - interactive.add_dir.extend(resume_cli.add_dir); - } if let Some(prompt) = resume_cli.prompt { interactive.prompt = Some(prompt); } @@ -800,28 +862,51 @@ mod tests { fn exec_inherits_root_model_flag() { let cli = MultitoolCli::try_parse_from(["codex", "-m", "codex-rapidash-300", "exec", "hi"]) .expect("parse"); - let MultitoolCli { - interactive, - config_overrides: root_overrides, - subcommand, - feature_toggles: _, - } = cli; + let MultitoolCli { subcommand, .. } = cli; - let Subcommand::Exec(mut exec_cli) = subcommand.expect("exec present") else { + let Subcommand::Exec(exec_cli) = subcommand.expect("exec present") else { unreachable!() }; - merge_interactive_flags_into_exec(&mut exec_cli, &interactive); - prepend_config_flags(&mut exec_cli.config_overrides, root_overrides); - assert_eq!(exec_cli.model.as_deref(), Some("codex-rapidash-300")); + assert_eq!(exec_cli.common.model.as_deref(), Some("codex-rapidash-300")); assert_eq!(exec_cli.prompt.as_deref(), Some("hi")); } + #[tokio::test] + async fn app_server_inherits_root_model_flag() { + let cli = MultitoolCli::try_parse_from(["codex", "-m", "codex-rapidash-300", "app-server"]) + .expect("parse"); + let MultitoolCli { subcommand, .. } = cli; + + let Subcommand::AppServer(app_server_cli) = subcommand.expect("app-server present") else { + unreachable!() + }; + assert!(app_server_cli.subcommand.is_none()); + + let cli_kv_overrides = app_server_cli + .config_overrides + .parse_overrides() + .expect("parse"); + let config_overrides = app_server_config_overrides_from_common( + &app_server_cli.common, + app_server_cli.approval_policy, + cli_kv_overrides, + None, + ) + .await + .expect("merge"); + + assert_eq!( + config_overrides.model.as_deref(), + Some("codex-rapidash-300") + ); + } + #[test] fn resume_model_flag_applies_when_no_root_flags() { let interactive = finalize_from_args(["codex", "resume", "-m", "gpt-5-test"].as_ref()); - assert_eq!(interactive.model.as_deref(), Some("gpt-5-test")); + assert_eq!(interactive.common.model.as_deref(), Some("gpt-5-test")); assert!(interactive.resume_picker); assert!(!interactive.resume_last); assert_eq!(interactive.resume_session_id, None); @@ -877,28 +962,33 @@ mod tests { .as_ref(), ); - assert_eq!(interactive.model.as_deref(), Some("gpt-5-test")); - assert!(interactive.oss); - assert_eq!(interactive.config_profile.as_deref(), Some("my-profile")); + assert_eq!(interactive.common.model.as_deref(), Some("gpt-5-test")); + assert!(interactive.common.oss); + assert_eq!( + interactive.common.config_profile.as_deref(), + Some("my-profile") + ); assert_matches!( - interactive.sandbox_mode, + interactive.common.sandbox_mode, Some(codex_common::SandboxModeCliArg::WorkspaceWrite) ); assert_matches!( interactive.approval_policy, Some(codex_common::ApprovalModeCliArg::OnRequest) ); - assert!(interactive.full_auto); + assert!(interactive.common.full_auto); assert_eq!( - interactive.cwd.as_deref(), + interactive.common.cwd.as_deref(), Some(std::path::Path::new("/tmp")) ); assert!(interactive.web_search); let has_a = interactive + .common .images .iter() .any(|p| p == std::path::Path::new("/tmp/a.png")); let has_b = interactive + .common .images .iter() .any(|p| p == std::path::Path::new("/tmp/b.png")); @@ -918,7 +1008,7 @@ mod tests { ] .as_ref(), ); - assert!(interactive.dangerously_bypass_approvals_and_sandbox); + assert!(interactive.common.dangerously_bypass_approvals_and_sandbox); assert!(interactive.resume_picker); assert!(!interactive.resume_last); assert_eq!(interactive.resume_session_id, None); diff --git a/codex-rs/common/src/common_cli.rs b/codex-rs/common/src/common_cli.rs new file mode 100644 index 0000000000..201856cb46 --- /dev/null +++ b/codex-rs/common/src/common_cli.rs @@ -0,0 +1,108 @@ +use clap::Parser; +use clap::ValueHint; +use std::path::PathBuf; + +use crate::SandboxModeCliArg; + +/// Flags shared across multiple Codex CLIs. +#[derive(Parser, Debug, Default, Clone)] +pub struct CommonCli { + /// Optional image(s) to attach to the initial prompt. + #[arg( + long = "image", + short = 'i', + value_name = "FILE", + value_delimiter = ',', + num_args = 1.., + global = true + )] + pub images: Vec, + + /// Model the agent should use. + #[arg(long, short = 'm', global = true)] + pub model: Option, + + /// Convenience flag to select the local open source model provider. Equivalent to -c + /// model_provider=oss; verifies a local LM Studio or Ollama server is running. + #[arg(long = "oss", default_value_t = false, global = true)] + pub oss: bool, + + /// Specify which local provider to use (lmstudio or ollama). + /// If not specified with --oss, will use config default or show selection. + #[arg(long = "local-provider", global = true)] + pub oss_provider: Option, + + /// Configuration profile from config.toml to specify default options. + #[arg(long = "profile", short = 'p', global = true)] + pub config_profile: Option, + + /// Select the sandbox policy to use when executing model-generated shell + /// commands. + #[arg(long = "sandbox", short = 's', value_enum, global = true)] + pub sandbox_mode: Option, + + /// Convenience alias for low-friction sandboxed automatic execution (-a on-request, --sandbox workspace-write). + #[arg(long = "full-auto", default_value_t = false, global = true)] + pub full_auto: bool, + + /// Skip all confirmation prompts and execute commands without sandboxing. + /// EXTREMELY DANGEROUS. Intended solely for running in environments that are externally sandboxed. + #[arg( + long = "dangerously-bypass-approvals-and-sandbox", + alias = "yolo", + default_value_t = false, + conflicts_with = "full_auto", + global = true + )] + pub dangerously_bypass_approvals_and_sandbox: bool, + + /// Tell the agent to use the specified directory as its working root. + #[clap(long = "cd", short = 'C', value_name = "DIR", global = true)] + pub cwd: Option, + + /// Additional directories that should be writable alongside the primary workspace. + #[arg( + long = "add-dir", + value_name = "DIR", + value_hint = ValueHint::DirPath, + global = true + )] + pub add_dir: Vec, +} + +impl CommonCli { + /// Apply overrides from another `CommonCli`, giving precedence to fields that + /// are explicitly set on `other`. + pub fn apply_overrides(&mut self, other: &CommonCli) { + if let Some(model) = &other.model { + self.model = Some(model.clone()); + } + if other.oss { + self.oss = true; + } + if let Some(provider) = &other.oss_provider { + self.oss_provider = Some(provider.clone()); + } + if let Some(profile) = &other.config_profile { + self.config_profile = Some(profile.clone()); + } + if let Some(sandbox) = other.sandbox_mode { + self.sandbox_mode = Some(sandbox); + } + if other.full_auto { + self.full_auto = true; + } + if other.dangerously_bypass_approvals_and_sandbox { + self.dangerously_bypass_approvals_and_sandbox = true; + } + if let Some(cwd) = &other.cwd { + self.cwd = Some(cwd.clone()); + } + if !other.images.is_empty() { + self.images = other.images.clone(); + } + if !other.add_dir.is_empty() { + self.add_dir.extend(other.add_dir.clone()); + } + } +} diff --git a/codex-rs/common/src/lib.rs b/codex-rs/common/src/lib.rs index 5092b3be24..250ffdc227 100644 --- a/codex-rs/common/src/lib.rs +++ b/codex-rs/common/src/lib.rs @@ -10,6 +10,9 @@ pub use approval_mode_cli_arg::ApprovalModeCliArg; #[cfg(feature = "cli")] mod sandbox_mode_cli_arg; +#[cfg(feature = "cli")] +mod common_cli; + #[cfg(feature = "cli")] pub use sandbox_mode_cli_arg::SandboxModeCliArg; @@ -22,6 +25,9 @@ mod config_override; #[cfg(feature = "cli")] pub use config_override::CliConfigOverrides; +#[cfg(feature = "cli")] +pub use common_cli::CommonCli; + mod sandbox_summary; #[cfg(feature = "sandbox_summary")] diff --git a/codex-rs/exec/src/cli.rs b/codex-rs/exec/src/cli.rs index ef20bf6fc7..e737eeb5b5 100644 --- a/codex-rs/exec/src/cli.rs +++ b/codex-rs/exec/src/cli.rs @@ -1,6 +1,7 @@ use clap::Parser; use clap::ValueEnum; use codex_common::CliConfigOverrides; +use codex_common::CommonCli; use std::path::PathBuf; #[derive(Parser, Debug)] @@ -10,58 +11,14 @@ pub struct Cli { #[command(subcommand)] pub command: Option, - /// Optional image(s) to attach to the initial prompt. - #[arg(long = "image", short = 'i', value_name = "FILE", value_delimiter = ',', num_args = 1..)] - pub images: Vec, - - /// Model the agent should use. - #[arg(long, short = 'm')] - pub model: Option, - - /// Use open-source provider. - #[arg(long = "oss", default_value_t = false)] - pub oss: bool, - - /// Specify which local provider to use (lmstudio or ollama). - /// If not specified with --oss, will use config default or show selection. - #[arg(long = "local-provider")] - pub oss_provider: Option, - - /// Select the sandbox policy to use when executing model-generated shell - /// commands. - #[arg(long = "sandbox", short = 's', value_enum)] - pub sandbox_mode: Option, - - /// Configuration profile from config.toml to specify default options. - #[arg(long = "profile", short = 'p')] - pub config_profile: Option, - - /// Convenience alias for low-friction sandboxed automatic execution (-a on-request, --sandbox workspace-write). - #[arg(long = "full-auto", default_value_t = false)] - pub full_auto: bool, - - /// Skip all confirmation prompts and execute commands without sandboxing. - /// EXTREMELY DANGEROUS. Intended solely for running in environments that are externally sandboxed. - #[arg( - long = "dangerously-bypass-approvals-and-sandbox", - alias = "yolo", - default_value_t = false, - conflicts_with = "full_auto" - )] - pub dangerously_bypass_approvals_and_sandbox: bool, - - /// Tell the agent to use the specified directory as its working root. - #[clap(long = "cd", short = 'C', value_name = "DIR")] - pub cwd: Option, + /// Model and execution controls shared with the interactive CLI. + #[clap(flatten)] + pub common: CommonCli, /// Allow running Codex outside a Git repository. #[arg(long = "skip-git-repo-check", default_value_t = false)] pub skip_git_repo_check: bool, - /// Additional directories that should be writable alongside the primary workspace. - #[arg(long = "add-dir", value_name = "DIR", value_hint = clap::ValueHint::DirPath)] - pub add_dir: Vec, - /// Path to a JSON Schema file describing the model's final response shape. #[arg(long = "output-schema", value_name = "FILE")] pub output_schema: Option, diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index a003b4ff21..cf32609b82 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -11,6 +11,7 @@ pub mod event_processor_with_jsonl_output; pub mod exec_events; pub use cli::Cli; +use codex_common::CommonCli; use codex_common::oss::ensure_oss_provider_ready; use codex_common::oss::get_default_model_for_oss_provider; use codex_core::AuthManager; @@ -59,6 +60,17 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any let Cli { command, + common, + skip_git_repo_check, + color, + last_message_file, + json: json_mode, + prompt, + output_schema: output_schema_path, + config_overrides, + } = cli; + + let CommonCli { images, model: model_cli_arg, oss, @@ -67,16 +79,10 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any full_auto, dangerously_bypass_approvals_and_sandbox, cwd, - skip_git_repo_check, add_dir, - color, - last_message_file, - json: json_mode, sandbox_mode: sandbox_mode_cli_arg, - prompt, - output_schema: output_schema_path, - config_overrides, - } = cli; + .. + } = common; // Determine the prompt source (parent or subcommand) and read from stdin if needed. let prompt_arg = match &command { diff --git a/codex-rs/tui/src/cli.rs b/codex-rs/tui/src/cli.rs index e7a0c945bf..4d3a926bd9 100644 --- a/codex-rs/tui/src/cli.rs +++ b/codex-rs/tui/src/cli.rs @@ -1,20 +1,18 @@ use clap::Parser; -use clap::ValueHint; use codex_common::ApprovalModeCliArg; use codex_common::CliConfigOverrides; -use std::path::PathBuf; +use codex_common::CommonCli; #[derive(Parser, Debug)] #[command(version)] pub struct Cli { + #[clap(flatten)] + pub common: CommonCli, + /// Optional user prompt to start the session. #[arg(value_name = "PROMPT", value_hint = clap::ValueHint::Other)] pub prompt: Option, - /// Optional image(s) to attach to the initial prompt. - #[arg(long = "image", short = 'i', value_name = "FILE", value_delimiter = ',', num_args = 1..)] - pub images: Vec, - // Internal controls set by the top-level `codex resume` subcommand. // These are not exposed as user flags on the base `codex` command. #[clap(skip)] @@ -28,59 +26,14 @@ pub struct Cli { #[clap(skip)] pub resume_session_id: Option, - /// Model the agent should use. - #[arg(long, short = 'm')] - pub model: Option, - - /// Convenience flag to select the local open source model provider. Equivalent to -c - /// model_provider=oss; verifies a local LM Studio or Ollama server is running. - #[arg(long = "oss", default_value_t = false)] - pub oss: bool, - - /// Specify which local provider to use (lmstudio or ollama). - /// If not specified with --oss, will use config default or show selection. - #[arg(long = "local-provider")] - pub oss_provider: Option, - - /// Configuration profile from config.toml to specify default options. - #[arg(long = "profile", short = 'p')] - pub config_profile: Option, - - /// Select the sandbox policy to use when executing model-generated shell - /// commands. - #[arg(long = "sandbox", short = 's')] - pub sandbox_mode: Option, - /// Configure when the model requires human approval before executing a command. #[arg(long = "ask-for-approval", short = 'a')] pub approval_policy: Option, - /// Convenience alias for low-friction sandboxed automatic execution (-a on-request, --sandbox workspace-write). - #[arg(long = "full-auto", default_value_t = false)] - pub full_auto: bool, - - /// Skip all confirmation prompts and execute commands without sandboxing. - /// EXTREMELY DANGEROUS. Intended solely for running in environments that are externally sandboxed. - #[arg( - long = "dangerously-bypass-approvals-and-sandbox", - alias = "yolo", - default_value_t = false, - conflicts_with_all = ["approval_policy", "full_auto"] - )] - pub dangerously_bypass_approvals_and_sandbox: bool, - - /// Tell the agent to use the specified directory as its working root. - #[clap(long = "cd", short = 'C', value_name = "DIR")] - pub cwd: Option, - /// Enable web search (off by default). When enabled, the native Responses `web_search` tool is available to the model (no per‑call approval). #[arg(long = "search", default_value_t = false)] pub web_search: bool, - /// Additional directories that should be writable alongside the primary workspace. - #[arg(long = "add-dir", value_name = "DIR", value_hint = ValueHint::DirPath)] - pub add_dir: Vec, - #[clap(skip)] pub config_overrides: CliConfigOverrides, } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index ca7cba9c24..5f9e67d34b 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -102,19 +102,19 @@ pub async fn run_main( mut cli: Cli, codex_linux_sandbox_exe: Option, ) -> std::io::Result { - let (sandbox_mode, approval_policy) = if cli.full_auto { + let (sandbox_mode, approval_policy) = if cli.common.full_auto { ( Some(SandboxMode::WorkspaceWrite), Some(AskForApproval::OnRequest), ) - } else if cli.dangerously_bypass_approvals_and_sandbox { + } else if cli.common.dangerously_bypass_approvals_and_sandbox { ( Some(SandboxMode::DangerFullAccess), Some(AskForApproval::Never), ) } else { ( - cli.sandbox_mode.map(Into::::into), + cli.common.sandbox_mode.map(Into::::into), cli.approval_policy.map(Into::into), ) }; @@ -161,11 +161,11 @@ pub async fn run_main( } }; - let model_provider_override = if cli.oss { + let model_provider_override = if cli.common.oss { let resolved = resolve_oss_provider( - cli.oss_provider.as_deref(), + cli.common.oss_provider.as_deref(), &config_toml, - cli.config_profile.clone(), + cli.common.config_profile.clone(), ); if let Some(provider) = resolved { @@ -185,9 +185,9 @@ pub async fn run_main( }; // When using `--oss`, let the bootstrapper pick the model based on selected provider - let model = if let Some(model) = &cli.model { + let model = if let Some(model) = &cli.common.model { Some(model.clone()) - } else if cli.oss { + } else if cli.common.oss { // Use the provider from model_provider_override model_provider_override .as_ref() @@ -198,8 +198,12 @@ pub async fn run_main( }; // canonicalize the cwd - let cwd = cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p)); - let additional_dirs = cli.add_dir.clone(); + let cwd = cli + .common + .cwd + .clone() + .map(|p| p.canonicalize().unwrap_or(p)); + let additional_dirs = cli.common.add_dir.clone(); let overrides = ConfigOverrides { model, @@ -208,13 +212,13 @@ pub async fn run_main( sandbox_mode, cwd, model_provider: model_provider_override.clone(), - config_profile: cli.config_profile.clone(), + config_profile: cli.common.config_profile.clone(), codex_linux_sandbox_exe, base_instructions: None, developer_instructions: None, compact_prompt: None, include_apply_patch_tool: None, - show_raw_agent_reasoning: cli.oss.then_some(true), + show_raw_agent_reasoning: cli.common.oss.then_some(true), tools_web_search_request: None, experimental_sandbox_command_assessment: None, additional_writable_roots: additional_dirs, @@ -222,7 +226,7 @@ pub async fn run_main( let config = load_config_or_exit(cli_kv_overrides.clone(), overrides.clone()).await; - if let Some(warning) = add_dir_warning_message(&cli.add_dir, &config.sandbox_policy) { + if let Some(warning) = add_dir_warning_message(&cli.common.add_dir, &config.sandbox_policy) { #[allow(clippy::print_stderr)] { eprintln!("Error adding directories: {warning}"); @@ -280,7 +284,7 @@ pub async fn run_main( .with_target(false) .with_filter(targets); - if cli.oss && model_provider_override.is_some() { + if cli.common.oss && model_provider_override.is_some() { // We're in the oss section, so provider_id should be Some // Let's handle None case gracefully though just in case let provider_id = match model_provider_override.as_ref() { @@ -508,7 +512,8 @@ async fn run_ratatui_app( resume_picker::ResumeSelection::StartFresh }; - let Cli { prompt, images, .. } = cli; + let Cli { prompt, common, .. } = cli; + let images = common.images; let app_result = App::run( &mut tui,