diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index ba3ae1d52..e8594558c 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -15,32 +15,51 @@ path = "src/lib.rs" [lints] workspace = true +[features] +default = ["full"] + +# Full build with all features (original behavior) +full = [ + "app-server", + "mcp-server", + "exec-mode", + "cloud-tasks", + "http-providers", + "responses-proxy", +] + +# Minimal ACP-only build: CLI + TUI + ACP +minimal = [] + +# Optional CLI modes +app-server = ["dep:codex-app-server"] +mcp-server = ["dep:codex-mcp-server"] +exec-mode = ["dep:codex-exec"] +cloud-tasks = ["dep:codex-cloud-tasks"] + +# Legacy HTTP provider support (OpenAI, ChatGPT login, etc.) +http-providers = ["dep:codex-chatgpt", "dep:codex-login", "dep:codex-rmcp-client"] +responses-proxy = ["dep:codex-responses-api-proxy"] + [dependencies] +# === Always required (core CLI + TUI + ACP) === anyhow = { workspace = true } clap = { workspace = true, features = ["derive"] } clap_complete = { workspace = true } codex-acp = { workspace = true } -codex-app-server = { workspace = true } codex-app-server-protocol = { workspace = true } codex-arg0 = { workspace = true } -codex-chatgpt = { workspace = true } -codex-cloud-tasks = { path = "../cloud-tasks" } codex-common = { workspace = true, features = ["cli"] } codex-core = { workspace = true } -codex-exec = { workspace = true } codex-execpolicy = { workspace = true } -codex-login = { workspace = true } -codex-mcp-server = { workspace = true } codex-process-hardening = { workspace = true } codex-protocol = { workspace = true } -codex-responses-api-proxy = { workspace = true } -codex-rmcp-client = { workspace = true } codex-stdio-to-uds = { workspace = true } codex-tui = { workspace = true } ctor = { workspace = true } libc = { workspace = true } owo-colors = { workspace = true } -regex-lite = { workspace = true} +regex-lite = { workspace = true } serde_json = { workspace = true } supports-color = { workspace = true } toml = { workspace = true } @@ -53,6 +72,16 @@ tokio = { workspace = true, features = [ ] } tracing = { workspace = true } +# === Feature-gated optional dependencies === +codex-app-server = { workspace = true, optional = true } +codex-chatgpt = { workspace = true, optional = true } +codex-cloud-tasks = { path = "../cloud-tasks", optional = true } +codex-exec = { workspace = true, optional = true } +codex-login = { workspace = true, optional = true } +codex-mcp-server = { workspace = true, optional = true } +codex-responses-api-proxy = { workspace = true, optional = true } +codex-rmcp-client = { workspace = true, optional = true } + [target.'cfg(target_os = "windows")'.dependencies] codex_windows_sandbox = { package = "codex-windows-sandbox", path = "../windows-sandbox-rs" } diff --git a/codex-rs/cli/src/lib.rs b/codex-rs/cli/src/lib.rs index e9f60eba7..1e259bdc2 100644 --- a/codex-rs/cli/src/lib.rs +++ b/codex-rs/cli/src/lib.rs @@ -1,5 +1,6 @@ pub mod debug_sandbox; mod exit_status; +#[cfg(feature = "http-providers")] pub mod login; use clap::Parser; diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 2109f381a..8d1dcb4bb 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -1,37 +1,51 @@ -use clap::Args; use clap::CommandFactory; use clap::Parser; use clap_complete::Shell; use clap_complete::generate; use codex_acp::init_file_tracing; use codex_arg0::arg0_dispatch_or_else; -use codex_chatgpt::apply_command::ApplyCommand; -use codex_chatgpt::apply_command::run_apply_command; use codex_cli::LandlockCommand; use codex_cli::SeatbeltCommand; use codex_cli::WindowsCommand; +use codex_common::CliConfigOverrides; +use codex_execpolicy::ExecPolicyCheckCommand; +use codex_tui::AppExitInfo; +use codex_tui::Cli as TuiCli; +use codex_tui::update_action::UpdateAction; +use owo_colors::OwoColorize; +use std::path::PathBuf; +use supports_color::Stream; + +// Feature-gated imports +#[cfg(feature = "http-providers")] +use codex_chatgpt::apply_command::ApplyCommand; +#[cfg(feature = "http-providers")] +use codex_chatgpt::apply_command::run_apply_command; +#[cfg(feature = "http-providers")] use codex_cli::login::read_api_key_from_stdin; +#[cfg(feature = "http-providers")] use codex_cli::login::run_login_status; +#[cfg(feature = "http-providers")] use codex_cli::login::run_login_with_api_key; +#[cfg(feature = "http-providers")] use codex_cli::login::run_login_with_chatgpt; +#[cfg(feature = "http-providers")] use codex_cli::login::run_login_with_device_code; +#[cfg(feature = "http-providers")] use codex_cli::login::run_logout; +#[cfg(feature = "cloud-tasks")] use codex_cloud_tasks::Cli as CloudTasksCli; -use codex_common::CliConfigOverrides; +#[cfg(feature = "exec-mode")] use codex_exec::Cli as ExecCli; -use codex_execpolicy::ExecPolicyCheckCommand; +#[cfg(feature = "responses-proxy")] use codex_responses_api_proxy::Args as ResponsesApiProxyArgs; -use codex_tui::AppExitInfo; -use codex_tui::Cli as TuiCli; -use codex_tui::update_action::UpdateAction; -use owo_colors::OwoColorize; -use std::path::PathBuf; -use supports_color::Stream; +#[cfg(feature = "mcp-server")] mod mcp_cmd; #[cfg(not(windows))] mod wsl_paths; +#[cfg(feature = "mcp-server")] use crate::mcp_cmd::McpCli; use codex_core::config::Config; @@ -70,22 +84,28 @@ struct MultitoolCli { #[derive(Debug, clap::Subcommand)] enum Subcommand { /// Run Codex non-interactively. + #[cfg(feature = "exec-mode")] #[clap(visible_alias = "e")] Exec(ExecCli), /// Manage login. + #[cfg(feature = "http-providers")] Login(LoginCommand), /// Remove stored authentication credentials. + #[cfg(feature = "http-providers")] Logout(LogoutCommand), /// [experimental] Run Codex as an MCP server and manage MCP servers. + #[cfg(feature = "mcp-server")] Mcp(McpCli), /// [experimental] Run the Codex MCP server (stdio transport). + #[cfg(feature = "mcp-server")] McpServer, /// [experimental] Run the app server or related tooling. + #[cfg(feature = "app-server")] AppServer(AppServerCommand), /// Generate shell completion scripts. @@ -100,6 +120,7 @@ enum Subcommand { Execpolicy(ExecpolicyCommand), /// Apply the latest diff produced by Codex agent as a `git apply` to your local working tree. + #[cfg(feature = "http-providers")] #[clap(visible_alias = "a")] Apply(ApplyCommand), @@ -107,10 +128,12 @@ enum Subcommand { Resume(ResumeCommand), /// [EXPERIMENTAL] Browse tasks from Codex Cloud and apply changes locally. + #[cfg(feature = "cloud-tasks")] #[clap(name = "cloud", alias = "cloud-tasks")] Cloud(CloudTasksCli), /// Internal: run the responses API proxy. + #[cfg(feature = "responses-proxy")] #[clap(hide = true)] ResponsesApiProxy(ResponsesApiProxyArgs), @@ -181,6 +204,7 @@ enum ExecpolicySubcommand { Check(ExecPolicyCheckCommand), } +#[cfg(feature = "http-providers")] #[derive(Debug, Parser)] struct LoginCommand { #[clap(skip)] @@ -216,18 +240,21 @@ struct LoginCommand { action: Option, } +#[cfg(feature = "http-providers")] #[derive(Debug, clap::Subcommand)] enum LoginSubcommand { /// Show login status. Status, } +#[cfg(feature = "http-providers")] #[derive(Debug, Parser)] struct LogoutCommand { #[clap(skip)] config_overrides: CliConfigOverrides, } +#[cfg(feature = "app-server")] #[derive(Debug, Parser)] struct AppServerCommand { /// Omit to run the app server; specify a subcommand for tooling. @@ -235,6 +262,7 @@ struct AppServerCommand { subcommand: Option, } +#[cfg(feature = "app-server")] #[derive(Debug, clap::Subcommand)] enum AppServerSubcommand { /// [experimental] Generate TypeScript bindings for the app server protocol. @@ -244,7 +272,8 @@ enum AppServerSubcommand { GenerateJsonSchema(GenerateJsonSchemaCommand), } -#[derive(Debug, Args)] +#[cfg(feature = "app-server")] +#[derive(Debug, clap::Args)] struct GenerateTsCommand { /// Output directory where .ts files will be written #[arg(short = 'o', long = "out", value_name = "DIR")] @@ -255,7 +284,8 @@ struct GenerateTsCommand { prettier: Option, } -#[derive(Debug, Args)] +#[cfg(feature = "app-server")] +#[derive(Debug, clap::Args)] struct GenerateJsonSchemaCommand { /// Output directory where the schema bundle will be written #[arg(short = 'o', long = "out", value_name = "DIR")] @@ -451,6 +481,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() let exit_info = codex_tui::run_main(interactive, codex_linux_sandbox_exe).await?; handle_app_exit(exit_info)?; } + #[cfg(feature = "exec-mode")] Some(Subcommand::Exec(mut exec_cli)) => { prepend_config_flags( &mut exec_cli.config_overrides, @@ -458,14 +489,17 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() ); codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?; } + #[cfg(feature = "mcp-server")] Some(Subcommand::McpServer) => { codex_mcp_server::run_main(codex_linux_sandbox_exe, root_config_overrides).await?; } + #[cfg(feature = "mcp-server")] Some(Subcommand::Mcp(mut mcp_cli)) => { // Propagate any root-level config overrides (e.g. `-c key=value`). prepend_config_flags(&mut mcp_cli.config_overrides, root_config_overrides.clone()); mcp_cli.run().await?; } + #[cfg(feature = "app-server")] 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?; @@ -497,6 +531,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() let exit_info = codex_tui::run_main(interactive, codex_linux_sandbox_exe).await?; handle_app_exit(exit_info)?; } + #[cfg(feature = "http-providers")] Some(Subcommand::Login(mut login_cli)) => { prepend_config_flags( &mut login_cli.config_overrides, @@ -528,6 +563,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() } } } + #[cfg(feature = "http-providers")] Some(Subcommand::Logout(mut logout_cli)) => { prepend_config_flags( &mut logout_cli.config_overrides, @@ -538,6 +574,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() Some(Subcommand::Completion(completion_cli)) => { print_completion(completion_cli); } + #[cfg(feature = "cloud-tasks")] Some(Subcommand::Cloud(mut cloud_cli)) => { prepend_config_flags( &mut cloud_cli.config_overrides, @@ -583,6 +620,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() Some(Subcommand::Execpolicy(ExecpolicyCommand { sub })) => match sub { ExecpolicySubcommand::Check(cmd) => run_execpolicycheck(cmd)?, }, + #[cfg(feature = "http-providers")] Some(Subcommand::Apply(mut apply_cli)) => { prepend_config_flags( &mut apply_cli.config_overrides, @@ -590,6 +628,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() ); run_apply_command(apply_cli, None).await?; } + #[cfg(feature = "responses-proxy")] Some(Subcommand::ResponsesApiProxy(args)) => { tokio::task::spawn_blocking(move || codex_responses_api_proxy::run_main(args)) .await??; diff --git a/codex-rs/common/Cargo.toml b/codex-rs/common/Cargo.toml index 377d05448..3760ab8a3 100644 --- a/codex-rs/common/Cargo.toml +++ b/codex-rs/common/Cargo.toml @@ -11,15 +11,23 @@ workspace = true clap = { workspace = true, features = ["derive", "wrap_help"], optional = true } codex-app-server-protocol = { workspace = true } codex-core = { workspace = true } -codex-lmstudio = { workspace = true } -codex-ollama = { workspace = true } codex-protocol = { workspace = true } once_cell = { workspace = true } serde = { workspace = true, optional = true } toml = { workspace = true, optional = true } +# Feature-gated optional dependencies (OSS providers) +codex-lmstudio = { workspace = true, optional = true } +codex-ollama = { workspace = true, optional = true } + [features] +default = ["oss-providers"] + # Separate feature so that `clap` is not a mandatory dependency. -cli = ["clap", "serde", "toml"] +cli = ["dep:clap", "dep:serde", "dep:toml"] + +# Local OSS model providers (Ollama, LM Studio) +oss-providers = ["dep:codex-ollama", "dep:codex-lmstudio"] + elapsed = [] sandbox_summary = [] diff --git a/codex-rs/common/src/oss.rs b/codex-rs/common/src/oss.rs index b2f511e47..59bc5616b 100644 --- a/codex-rs/common/src/oss.rs +++ b/codex-rs/common/src/oss.rs @@ -1,10 +1,13 @@ //! OSS provider utilities shared between TUI and exec. +#[cfg(feature = "oss-providers")] use codex_core::LMSTUDIO_OSS_PROVIDER_ID; +#[cfg(feature = "oss-providers")] use codex_core::OLLAMA_OSS_PROVIDER_ID; use codex_core::config::Config; /// Returns the default model for a given OSS provider. +#[cfg(feature = "oss-providers")] pub fn get_default_model_for_oss_provider(provider_id: &str) -> Option<&'static str> { match provider_id { LMSTUDIO_OSS_PROVIDER_ID => Some(codex_lmstudio::DEFAULT_OSS_MODEL), @@ -13,7 +16,15 @@ pub fn get_default_model_for_oss_provider(provider_id: &str) -> Option<&'static } } +/// Returns the default model for a given OSS provider. +/// Stub version when OSS providers are not compiled in. +#[cfg(not(feature = "oss-providers"))] +pub fn get_default_model_for_oss_provider(_provider_id: &str) -> Option<&'static str> { + None +} + /// Ensures the specified OSS provider is ready (models downloaded, service reachable). +#[cfg(feature = "oss-providers")] pub async fn ensure_oss_provider_ready( provider_id: &str, config: &Config, @@ -36,7 +47,18 @@ pub async fn ensure_oss_provider_ready( Ok(()) } -#[cfg(test)] +/// Ensures the specified OSS provider is ready. +/// Stub version when OSS providers are not compiled in. +#[cfg(not(feature = "oss-providers"))] +pub async fn ensure_oss_provider_ready( + _provider_id: &str, + _config: &Config, +) -> Result<(), std::io::Error> { + // OSS providers not available in this build + Ok(()) +} + +#[cfg(all(test, feature = "oss-providers"))] mod tests { use super::*; diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index b13e09542..767743730 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -13,6 +13,14 @@ name = "codex_tui" path = "src/lib.rs" [features] +default = ["http-fallback", "sentry"] + +# Enable HTTP API fallback when ACP model not found +http-fallback = ["dep:codex-backend-client", "dep:codex-login"] + +# Sentry error reporting +sentry = ["dep:codex-feedback"] + # Enable vt100-based tests (emulator) when running with `--features vt100-tests`. vt100-tests = [] # Gate verbose debug logging inside the TUI implementation. @@ -31,17 +39,21 @@ codex-acp = { workspace = true } codex-ansi-escape = { workspace = true } codex-app-server-protocol = { workspace = true } codex-arg0 = { workspace = true } -codex-backend-client = { workspace = true } codex-common = { workspace = true, features = [ "cli", "elapsed", "sandbox_summary", ] } codex-core = { workspace = true } -codex-feedback = { workspace = true } codex-file-search = { workspace = true } -codex-login = { workspace = true } codex-protocol = { workspace = true } + +# Feature-gated optional dependencies (HTTP fallback) +codex-backend-client = { workspace = true, optional = true } +codex-login = { workspace = true, optional = true } + +# Feature-gated optional dependencies (telemetry) +codex-feedback = { workspace = true, optional = true } color-eyre = { workspace = true } crossterm = { workspace = true, features = ["bracketed-paste", "event-stream"] } derive_more = { workspace = true, features = ["is_variant"] } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 2eaba17eb..cd2aee7e4 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -303,6 +303,7 @@ impl App { }; ChatWidget::new(init, conversation_manager.clone()) } + #[cfg(feature = "http-fallback")] ResumeSelection::Resume(path) => { let resumed = conversation_manager .resume_conversation_from_rollout( @@ -331,6 +332,12 @@ impl App { resumed.session_configured, ) } + #[cfg(not(feature = "http-fallback"))] + ResumeSelection::Resume(_path) => { + anyhow::bail!( + "Session resume is not available in this build. HTTP fallback is required." + ); + } }; chat_widget.maybe_prompt_windows_sandbox_enable(); diff --git a/codex-rs/tui/src/app_backtrack.rs b/codex-rs/tui/src/app_backtrack.rs index 910b0bb8e..a0705d056 100644 --- a/codex-rs/tui/src/app_backtrack.rs +++ b/codex-rs/tui/src/app_backtrack.rs @@ -295,6 +295,7 @@ impl App { } /// Fork the conversation using provided history and switch UI/state accordingly. + #[cfg(feature = "http-fallback")] async fn fork_and_switch_to_new_conversation( &mut self, tui: &mut tui::Tui, @@ -315,7 +316,22 @@ impl App { } } + /// Fork is not available without HTTP fallback. + #[cfg(not(feature = "http-fallback"))] + async fn fork_and_switch_to_new_conversation( + &mut self, + _tui: &mut tui::Tui, + _ev: ConversationPathResponseEvent, + _nth_user_message: usize, + _prefill: String, + ) { + tracing::error!( + "Conversation forking is not available in this build. HTTP fallback is required." + ); + } + /// Thin wrapper around ConversationManager::fork_conversation. + #[cfg(feature = "http-fallback")] async fn perform_fork( &self, path: PathBuf, @@ -328,6 +344,7 @@ impl App { } /// Install a forked conversation into the ChatWidget and update UI to reflect selection. + #[cfg(feature = "http-fallback")] fn install_forked_conversation( &mut self, tui: &mut tui::Tui, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 8ea7f7421..52814be42 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -113,6 +113,7 @@ mod pending_exec_cells; use self::pending_exec_cells::PendingExecCellTracker; mod agent; use self::agent::spawn_agent; +#[cfg(feature = "http-fallback")] use self::agent::spawn_agent_from_existing; mod session_header; use self::session_header::SessionHeader; @@ -1316,6 +1317,7 @@ impl ChatWidget { } /// Create a ChatWidget attached to an existing conversation (e.g., a fork). + #[cfg(feature = "http-fallback")] pub(crate) fn new_from_existing( common: ChatWidgetInit, conversation: std::sync::Arc, diff --git a/codex-rs/tui/src/chatwidget/agent.rs b/codex-rs/tui/src/chatwidget/agent.rs index 788962065..6492fa181 100644 --- a/codex-rs/tui/src/chatwidget/agent.rs +++ b/codex-rs/tui/src/chatwidget/agent.rs @@ -3,8 +3,10 @@ use std::sync::Arc; use codex_acp::AcpBackend; use codex_acp::AcpBackendConfig; use codex_acp::get_agent_config; +#[cfg(feature = "http-fallback")] use codex_core::CodexConversation; use codex_core::ConversationManager; +#[cfg(feature = "http-fallback")] use codex_core::NewConversation; use codex_core::config::Config; use codex_core::protocol::Event; @@ -24,6 +26,7 @@ use crate::app_event_sender::AppEventSender; /// 1. If the model is registered in the ACP registry, use ACP mode /// 2. If the model is NOT registered and `acp_allow_http_fallback` is true, use HTTP mode /// 3. If the model is NOT registered and `acp_allow_http_fallback` is false (default), error +#[cfg(feature = "http-fallback")] pub(crate) fn spawn_agent( config: Config, app_event_tx: AppEventSender, @@ -51,6 +54,31 @@ pub(crate) fn spawn_agent( } } +/// Spawn the agent bootstrapper and op forwarding loop, returning the +/// `UnboundedSender` used by the UI to submit operations. +/// +/// ACP-only version: HTTP fallback is not available. +#[cfg(not(feature = "http-fallback"))] +pub(crate) fn spawn_agent( + config: Config, + app_event_tx: AppEventSender, + _server: Arc, +) -> UnboundedSender { + let acp_agent_result = get_agent_config(&config.model); + + if acp_agent_result.is_ok() { + spawn_acp_agent(config, app_event_tx) + } else { + let error_msg = format!( + "Model '{}' is not registered as an ACP agent. \ + HTTP fallback is not available in this build. \ + Known ACP models: mock-model, mock-model-alt, claude, claude-acp, gemini-2.5-flash, gemini-acp", + config.model + ); + spawn_error_agent(error_msg, app_event_tx) + } +} + /// Spawn an agent that emits an error and exits after a brief delay. /// /// The delay allows the TUI to render the error message before exiting, @@ -138,6 +166,7 @@ fn spawn_acp_agent(config: Config, app_event_tx: AppEventSender) -> UnboundedSen /// Spawn an HTTP agent (the original implementation). /// /// This uses `codex_core` to communicate with LLM providers via HTTP APIs. +#[cfg(feature = "http-fallback")] fn spawn_http_agent( config: Config, app_event_tx: AppEventSender, @@ -196,6 +225,7 @@ fn spawn_http_agent( /// Spawn agent loops for an existing conversation (e.g., a forked conversation). /// Sends the provided `SessionConfiguredEvent` immediately, then forwards subsequent /// events and accepts Ops for submission. +#[cfg(feature = "http-fallback")] pub(crate) fn spawn_agent_from_existing( conversation: std::sync::Arc, session_configured: codex_core::protocol::SessionConfiguredEvent,