codex / codex-rs /cli /src /main.rs
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
afa0cbf verified
Raw
History Blame Contribute Delete
190 kB
use clap::Args;
use clap::CommandFactory;
use clap::Parser;
use clap_complete::Shell;
use clap_complete::generate;
use codex_app_server_daemon::BootstrapOptions as AppServerBootstrapOptions;
use codex_app_server_daemon::LifecycleCommand as AppServerLifecycleCommand;
use codex_app_server_daemon::RemoteControlMode as AppServerRemoteControlMode;
use codex_arg0::Arg0DispatchPaths;
use codex_arg0::arg0_dispatch_or_else;
use codex_chatgpt::apply_command::ApplyCommand;
use codex_chatgpt::apply_command::run_apply_command;
use codex_cli::read_access_token_from_stdin;
use codex_cli::read_api_key_from_stdin;
use codex_cli::run_login_status;
use codex_cli::run_login_with_access_token;
use codex_cli::run_login_with_api_key;
use codex_cli::run_login_with_chatgpt;
use codex_cli::run_login_with_device_code;
use codex_cli::run_logout;
use codex_cloud_config::cloud_config_bundle_loader_for_storage;
use codex_cloud_tasks::Cli as CloudTasksCli;
use codex_exec::Cli as ExecCli;
use codex_exec::Command as ExecCommand;
use codex_exec::ReviewArgs;
use codex_exec_server::ExecServerRuntimePaths;
use codex_execpolicy::ExecPolicyCheckCommand;
use codex_http_client::HttpClientFactory;
use codex_http_client::OutboundProxyPolicy;
use codex_responses_api_proxy::Args as ResponsesApiProxyArgs;
use codex_rollout_trace::REDUCED_STATE_FILE_NAME;
use codex_rollout_trace::replay_bundle;
use codex_state::StateRuntime;
use codex_tui::AppExitInfo;
use codex_tui::Cli as TuiCli;
use codex_tui::ExitReason;
use codex_tui::UpdateAction;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_cli::CliConfigOverrides;
use codex_utils_cli::ProfileV2Name;
use codex_utils_cli::SharedCliOptions;
use std::collections::HashSet;
use std::io::IsTerminal;
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use supports_color::Stream;
#[cfg(all(
target_os = "linux",
target_env = "musl",
any(target_arch = "x86_64", target_arch = "aarch64")
))]
#[global_allocator]
static ALLOCATOR: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
#[cfg(any(target_os = "macos", target_os = "windows"))]
mod app_cmd;
mod cloud_config;
mod daemon_install;
#[cfg(any(target_os = "macos", target_os = "windows"))]
mod desktop_app;
mod doctor;
#[cfg(test)]
#[path = "exec_server_args_tests.rs"]
mod exec_server_args_tests;
mod exec_server_auth;
mod exec_server_telemetry;
mod marketplace_cmd;
mod mcp_cmd;
mod mcp_login;
mod migrate_rollouts;
mod plugin_cmd;
mod queue_cmd;
mod remote_control_cmd;
#[cfg(target_os = "windows")]
mod sandbox_setup;
mod state_db_recovery;
#[cfg(not(windows))]
mod wsl_paths;
use crate::mcp_cmd::McpCli;
use crate::plugin_cmd::PluginCli;
use crate::plugin_cmd::PluginSubcommand;
use crate::queue_cmd::QueueCommand;
use crate::remote_control_cmd::RemoteControlCommand;
use doctor::DoctorCommand;
use state_db_recovery as local_state_db;
use codex_config::LoaderOverrides;
use codex_core::build_models_manager;
use codex_core::config::Config;
use codex_core::config::ConfigBuilder;
use codex_core::config::ConfigLoadOptions;
use codex_core::config::ConfigOverrides;
use codex_core::config::bootstrap_auth_config;
use codex_core::config::edit::ConfigEditsBuilder;
use codex_core::config::find_codex_home;
use codex_core::config::load_config_toml_with_layer_stack;
use codex_core::config::resolve_profile_v2_config_path;
use codex_features::FEATURES;
use codex_features::Stage;
use codex_features::is_known_feature_key;
use codex_home::CodexHomeUserInstructionsProvider;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_login::is_workload_identity_selected;
use codex_login::read_codex_access_token_from_env;
use codex_memories_write::clear_memory_roots_contents;
use codex_models_manager::bundled_models_response;
use codex_models_manager::manager::RefreshStrategy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::user_input::UserInput;
use codex_terminal_detection::TerminalName;
/// Codex CLI
///
/// If no subcommand is specified, options will be forwarded to the interactive CLI.
#[derive(Debug, Parser)]
#[clap(
author,
version,
// If a sub‑command is given, ignore requirements of the default args.
subcommand_negates_reqs = true,
// The executable is sometimes invoked via a platform‑specific name like
// `codex-x86_64-unknown-linux-musl`, but the help output should always use
// the generic `codex` command name that users run.
bin_name = "codex",
override_usage = "codex [OPTIONS] [PROMPT]\n codex [OPTIONS] <COMMAND> [ARGS]"
)]
struct MultitoolCli {
#[clap(flatten)]
pub config_overrides: CliConfigOverrides,
#[clap(flatten)]
pub feature_toggles: FeatureToggles,
#[clap(flatten)]
remote: InteractiveRemoteOptions,
#[clap(flatten)]
interactive: TuiCli,
#[clap(subcommand)]
subcommand: Option<Subcommand>,
}
#[derive(Debug, clap::Subcommand)]
enum Subcommand {
/// Browse all agent sessions on the shared local app-server daemon.
Agents(AgentsCommand),
/// Internal: forward a local TCP socket through an HTTP/3 CONNECT proxy.
#[clap(hide = true)]
TcpTunnel(codex_tcp_tunnel::Args),
/// Run Codex non-interactively.
#[clap(visible_alias = "e")]
Exec(ExecCli),
/// Run a code review non-interactively.
Review(ReviewCommand),
/// Manage login.
Login(LoginCommand),
/// Remove stored authentication credentials.
Logout(LogoutCommand),
/// Manage external MCP servers for Codex.
Mcp(McpCli),
/// Manage Codex plugins.
Plugin(PluginCli),
/// [experimental] Run the app server or related tooling.
AppServer(AppServerCommand),
/// [experimental] Manage the app-server daemon with remote control enabled.
RemoteControl(RemoteControlCommand),
/// Launch the Desktop app (opens the app installer if missing).
#[cfg(any(target_os = "macos", target_os = "windows"))]
App(app_cmd::AppCommand),
/// Generate shell completion scripts.
Completion(CompletionCommand),
/// Update Codex to the latest version.
Update,
/// Diagnose local Codex installation, config, auth, and runtime health.
Doctor(DoctorCommand),
/// Run commands within a Codex-provided sandbox.
Sandbox(HostSandboxArgs),
/// Debugging tools.
Debug(DebugCommand),
/// Execpolicy tooling.
#[clap(hide = true)]
Execpolicy(ExecpolicyCommand),
/// Apply the latest diff produced by Codex agent as a `git apply` to your local working tree.
#[clap(visible_alias = "a")]
Apply(ApplyCommand),
/// Resume a previous interactive session (picker by default; use --last to continue the most recent).
Resume(ResumeCommand),
/// Queue a message for an existing session.
Queue(QueueCommand),
/// Archive a saved session by id or session name.
Archive(SessionArchiveCommand),
/// Permanently delete a saved session by id or session name.
Delete(DeleteCommand),
/// Inspect or migrate legacy local sessions to paginated thread history.
MigrateRollouts(migrate_rollouts::MigrateRolloutsCommand),
/// Unarchive a saved session by id or session name.
Unarchive(SessionArchiveCommand),
/// Fork a previous interactive session (picker by default; use --last to fork the most recent).
Fork(ForkCommand),
/// [EXPERIMENTAL] Browse tasks from Codex Cloud and apply changes locally.
#[clap(name = "cloud", alias = "cloud-tasks")]
Cloud(CloudTasksCli),
/// Internal: run the responses API proxy.
#[clap(hide = true)]
ResponsesApiProxy(ResponsesApiProxyArgs),
/// Internal: relay stdio to a Unix domain socket.
#[clap(hide = true, name = "stdio-to-uds")]
StdioToUds(StdioToUdsCommand),
/// [EXPERIMENTAL] Run the standalone exec-server service.
ExecServer(ExecServerCommand),
/// Inspect feature flags.
Features(FeaturesCli),
}
#[derive(Debug, Parser)]
struct CompletionCommand {
/// Shell to generate completions for
#[clap(value_enum, default_value_t = Shell::Bash)]
shell: Shell,
}
#[derive(Debug, Parser)]
struct DebugCommand {
#[command(subcommand)]
subcommand: DebugSubcommand,
}
#[derive(Debug, clap::Subcommand)]
enum DebugSubcommand {
/// Render the raw model catalog as JSON.
Models(DebugModelsCommand),
/// Tooling: helps debug the app server.
AppServer(DebugAppServerCommand),
/// Render the model-visible prompt input list as JSON.
PromptInput(DebugPromptInputCommand),
/// Replay a rollout trace bundle and write reduced state JSON.
#[clap(hide = true)]
TraceReduce(DebugTraceReduceCommand),
/// Internal: reset local memory state for a fresh start.
#[clap(hide = true)]
ClearMemories,
}
#[derive(Debug, Parser)]
struct DebugAppServerCommand {
#[command(subcommand)]
subcommand: DebugAppServerSubcommand,
}
#[derive(Debug, clap::Subcommand)]
enum DebugAppServerSubcommand {
// Send message to app server V2.
SendMessageV2(DebugAppServerSendMessageV2Command),
}
#[derive(Debug, Parser)]
struct DebugAppServerSendMessageV2Command {
#[arg(value_name = "USER_MESSAGE", required = true)]
user_message: String,
}
#[derive(Debug, Parser)]
struct DebugPromptInputCommand {
/// Optional user prompt to append after session context.
#[arg(value_name = "PROMPT")]
prompt: Option<String>,
/// Optional image(s) to attach to the user prompt.
#[arg(long = "image", short = 'i', value_name = "FILE", value_delimiter = ',', num_args = 1..)]
images: Vec<PathBuf>,
}
#[derive(Debug, Parser)]
struct DebugModelsCommand {
/// Skip refresh and dump only the bundled catalog shipped with this binary.
#[arg(long = "bundled", default_value_t = false)]
bundled: bool,
}
#[derive(Debug, Parser)]
struct ReviewCommand {
/// Error out when config.toml contains fields that are not recognized by this version of Codex.
#[arg(long = "strict-config", default_value_t = false)]
strict_config: bool,
#[clap(flatten)]
args: ReviewArgs,
}
#[derive(Debug, Parser)]
struct DebugTraceReduceCommand {
/// Trace bundle directory containing manifest.json and trace.jsonl.
#[arg(value_name = "TRACE_BUNDLE")]
trace_bundle: PathBuf,
/// Output path for reduced RolloutTrace JSON. Defaults to TRACE_BUNDLE/state.json.
#[arg(long = "output", short = 'o', value_name = "FILE")]
output: Option<PathBuf>,
}
#[derive(Debug, Parser)]
struct AgentsCommand {
#[clap(flatten)]
remote: InteractiveRemoteOptions,
/// Use this directory for new tasks on a remote server.
#[arg(long = "cd", short = 'C', value_name = "DIR")]
cwd: Option<PathBuf>,
/// Disable alternate screen mode.
#[arg(long = "no-alt-screen", default_value_t = false)]
no_alt_screen: bool,
}
#[derive(Debug, Parser)]
struct ResumeCommand {
/// Session id (UUID) or session name. UUIDs take precedence if it parses.
/// If omitted, use --last to pick the most recent recorded session.
#[arg(value_name = "SESSION_ID")]
session_id: Option<String>,
/// Continue the most recent session without showing the picker.
#[arg(long = "last", default_value_t = false)]
last: bool,
/// Show all sessions (disables cwd filtering and shows CWD column).
#[arg(long = "all", default_value_t = false)]
all: bool,
/// Include non-interactive sessions in the resume picker and --last selection.
#[arg(long = "include-non-interactive", default_value_t = false)]
include_non_interactive: bool,
#[clap(flatten)]
remote: InteractiveRemoteOptions,
#[clap(flatten)]
config_overrides: SessionTuiCli,
}
#[derive(Debug, Parser)]
struct SessionArchiveCommand {
/// Session id (UUID) or session name. UUIDs take precedence if it parses.
#[arg(value_name = "SESSION")]
target: String,
#[clap(flatten)]
remote: InteractiveRemoteOptions,
#[clap(flatten)]
config_overrides: SessionArchiveConfigOverrides,
}
#[derive(Debug, Args, Clone, Default)]
struct SessionArchiveConfigOverrides {
#[clap(flatten)]
shared: SharedCliOptions,
/// Error out when config.toml contains fields that are not recognized by this version of Codex.
#[arg(long = "strict-config", default_value_t = false)]
strict_config: bool,
#[clap(flatten)]
config_overrides: CliConfigOverrides,
}
#[derive(Debug, Args)]
struct DeleteCommand {
#[clap(flatten)]
session: SessionArchiveCommand,
/// Delete without prompting. SESSION must be a UUID.
#[arg(long, default_value_t = false)]
force: bool,
}
#[derive(Debug, Parser)]
struct ForkCommand {
/// Conversation/session id (UUID). When provided, forks this session.
/// If omitted, use --last to pick the most recent recorded session.
#[arg(value_name = "SESSION_ID")]
session_id: Option<String>,
/// Fork the most recent session without showing the picker.
#[arg(long = "last", default_value_t = false)]
last: bool,
/// Show all sessions (disables cwd filtering and shows CWD column).
#[arg(long = "all", default_value_t = false)]
all: bool,
#[clap(flatten)]
remote: InteractiveRemoteOptions,
#[clap(flatten)]
config_overrides: SessionTuiCli,
}
/// TUI arguments for session commands where a parsed prompt implies an explicit session id.
///
/// This keeps `--last PROMPT` valid while rejecting `--last SESSION_ID PROMPT`.
#[derive(Debug)]
struct SessionTuiCli(TuiCli);
impl Args for SessionTuiCli {
fn augment_args(cmd: clap::Command) -> clap::Command {
TuiCli::augment_args(cmd).mut_arg("prompt", |arg| arg.conflicts_with("last"))
}
fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
TuiCli::augment_args_for_update(cmd).mut_arg("prompt", |arg| arg.conflicts_with("last"))
}
}
impl clap::FromArgMatches for SessionTuiCli {
fn from_arg_matches(matches: &clap::ArgMatches) -> Result<Self, clap::Error> {
TuiCli::from_arg_matches(matches).map(Self)
}
fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> {
self.0.update_from_arg_matches(matches)
}
}
#[cfg(target_os = "macos")]
type HostSandboxArgs = codex_cli::SeatbeltCommand;
#[cfg(target_os = "linux")]
type HostSandboxArgs = codex_cli::LandlockCommand;
#[cfg(target_os = "windows")]
type HostSandboxArgs = codex_cli::WindowsCommand;
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
type HostSandboxArgs = UnsupportedSandboxArgs;
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
#[derive(Debug, Parser)]
struct UnsupportedSandboxArgs {
/// Layer $CODEX_HOME/<name>.config.toml on top of the base user config.
#[arg(long = "profile", short = 'p')]
pub config_profile: Option<ProfileV2Name>,
#[clap(skip)]
pub config_overrides: CliConfigOverrides,
/// Full command args to run under the host sandbox.
#[arg(trailing_var_arg = true)]
pub command: Vec<String>,
}
#[derive(Debug, Parser)]
struct ExecpolicyCommand {
#[command(subcommand)]
sub: ExecpolicySubcommand,
}
#[derive(Debug, clap::Subcommand)]
enum ExecpolicySubcommand {
/// Check execpolicy files against a command.
#[clap(name = "check")]
Check(ExecPolicyCheckCommand),
}
#[derive(Debug, Parser)]
struct LoginCommand {
#[clap(skip)]
config_overrides: CliConfigOverrides,
#[arg(
long = "with-api-key",
help = "Read the API key from stdin (e.g. `printenv OPENAI_API_KEY | codex login --with-api-key`)"
)]
with_api_key: bool,
#[arg(
long = "with-access-token",
help = "Read the access token from stdin (e.g. `printenv CODEX_ACCESS_TOKEN | codex login --with-access-token`)"
)]
with_access_token: bool,
#[arg(
long = "api-key",
num_args = 0..=1,
default_missing_value = "",
value_name = "API_KEY",
help = "(deprecated) Previously accepted the API key directly; now exits with guidance to use --with-api-key",
hide = true
)]
api_key: Option<String>,
#[arg(long = "device-auth")]
use_device_code: bool,
/// EXPERIMENTAL: Use custom OAuth issuer base URL (advanced)
/// Override the OAuth issuer base URL (advanced)
#[arg(long = "experimental_issuer", value_name = "URL", hide = true)]
issuer_base_url: Option<String>,
/// EXPERIMENTAL: Use custom OAuth client ID (advanced)
#[arg(long = "experimental_client-id", value_name = "CLIENT_ID", hide = true)]
client_id: Option<String>,
#[command(subcommand)]
action: Option<LoginSubcommand>,
}
#[derive(Debug, clap::Subcommand)]
enum LoginSubcommand {
/// Show login status.
Status,
}
#[derive(Debug, Parser)]
struct LogoutCommand {
#[clap(skip)]
config_overrides: CliConfigOverrides,
}
#[derive(Debug, Parser)]
struct AppServerCommand {
/// Omit to run the app server; specify a subcommand for tooling.
#[command(subcommand)]
subcommand: Option<AppServerSubcommand>,
#[command(flatten)]
code_mode_host: codex_app_server::AppServerCodeModeHostArgs,
/// Error out when config.toml contains fields that are not recognized by this version of Codex.
#[arg(long = "strict-config", default_value_t = false)]
strict_config: bool,
/// Transport endpoint URL. Supported values: `stdio://` (default),
/// `unix://`, `unix://PATH`, `ws://IP:PORT`, `off`.
#[arg(
long = "listen",
value_name = "URL",
default_value = codex_app_server::AppServerTransport::DEFAULT_LISTEN_URL
)]
listen: codex_app_server::AppServerTransport,
/// Use stdio as the transport (equivalent to `--listen stdio://`).
#[arg(long = "stdio", conflicts_with = "listen")]
stdio: bool,
/// Enable remote control for this app-server process without changing persistence.
#[arg(long = "remote-control", hide = true)]
remote_control: bool,
/// Save loaded threads during managed daemon shutdown.
#[arg(long, hide = true)]
managed_daemon: bool,
/// Controls whether analytics are enabled by default.
///
/// Analytics are disabled by default for app-server. Users have to explicitly opt in
/// via the `analytics` section in the config.toml file.
///
/// However, for first-party use cases like the VSCode IDE extension, we default analytics
/// to be enabled by default by setting this flag. Users can still opt out by setting this
/// in their config.toml:
///
/// ```toml
/// [analytics]
/// enabled = false
/// ```
///
/// See https://developers.openai.com/codex/config-advanced/#metrics for more details.
#[arg(long = "analytics-default-enabled")]
analytics_default_enabled: bool,
#[command(flatten)]
auth: codex_app_server::AppServerWebsocketAuthArgs,
}
#[derive(Debug, Parser)]
struct ExecServerCommand {
#[command(subcommand)]
command: Option<ExecServerSubcommand>,
/// Error out when config.toml contains fields that are not recognized by this version of Codex.
#[arg(
id = "exec_server_strict_config",
long = "strict-config",
default_value_t = false,
global = true
)]
strict_config: bool,
/// Maximum number of requests to process concurrently on each connection.
#[arg(
long = "concurrent-requests",
value_name = "COUNT",
default_value = "1"
)]
request_dispatch_mode: codex_exec_server::RequestDispatchMode,
/// Transport endpoint URL. Supported values: `ws://IP:PORT` (default), `stdio`, `stdio://`.
#[arg(
long = "listen",
value_name = "URL",
conflicts_with = "exec_server_remote"
)]
listen: Option<String>,
/// Register this exec-server as a remote environment using the given base URL.
#[arg(
long = "remote",
id = "exec_server_remote",
value_name = "URL",
requires = "environment_id",
global = true
)]
remote: Option<String>,
/// Transport used for the remote executor connection.
#[arg(
long = "remote-transport",
value_enum,
default_value_t = ExecServerRemoteTransport::Noise,
requires = "exec_server_remote",
requires_if("direct", "aws_sigv4"),
global = true
)]
remote_transport: ExecServerRemoteTransport,
/// Environment id to attach to when registering remotely.
#[arg(long = "environment-id", value_name = "ID", global = true)]
environment_id: Option<String>,
/// Human-readable environment name.
#[arg(long = "name", value_name = "NAME", global = true)]
name: Option<String>,
/// Use Agent Identity auth from CODEX_ACCESS_TOKEN for remote registration.
#[arg(
long = "use-agent-identity-auth",
requires = "exec_server_remote",
conflicts_with = "aws_sigv4",
global = true
)]
use_agent_identity_auth: bool,
/// Sign Direct registration and WebSocket handshake requests with AWS SigV4.
#[arg(long = "aws-sigv4", requires = "exec_server_remote", global = true)]
aws_sigv4: bool,
/// AWS profile used for SigV4 authentication.
#[arg(
long = "aws-profile",
value_name = "PROFILE",
requires = "aws_sigv4",
global = true
)]
aws_profile: Option<String>,
/// AWS signing region. Uses the SDK region chain when omitted.
#[arg(
long = "aws-region",
value_name = "REGION",
requires = "aws_sigv4",
global = true
)]
aws_region: Option<String>,
/// AWS signing service.
#[arg(
long = "aws-service",
value_name = "SERVICE",
default_value = "execute-api",
requires = "aws_sigv4",
global = true
)]
aws_service: String,
/// Exit when the parent-owned standard-input pipe closes.
#[arg(
long = "exit-on-stdin-close",
env = codex_exec_server::CODEX_EXEC_SERVER_EXIT_ON_STDIN_CLOSE_ENV_VAR,
requires_if("true", "exec_server_remote"),
global = true
)]
exit_on_stdin_close: bool,
}
impl ExecServerCommand {
fn validate_remote_transport(&self) -> anyhow::Result<()> {
match (self.remote_transport, self.aws_sigv4) {
(ExecServerRemoteTransport::Noise, true) => {
anyhow::bail!("--aws-sigv4 requires --remote-transport direct");
}
(ExecServerRemoteTransport::Direct, false) => {
anyhow::bail!("--remote-transport direct requires --aws-sigv4");
}
(ExecServerRemoteTransport::Noise, false)
| (ExecServerRemoteTransport::Direct, true) => {}
}
if self.remote_transport == ExecServerRemoteTransport::Direct
&& matches!(
self.command.as_ref(),
Some(ExecServerSubcommand::Forward { .. })
)
{
anyhow::bail!("direct exec-server transport does not support forwarding");
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, clap::ValueEnum)]
enum ExecServerRemoteTransport {
#[default]
Noise,
Direct,
}
#[derive(Debug, clap::Subcommand)]
enum ExecServerSubcommand {
/// Register an existing WebSocket exec-server as a remote environment.
Forward {
/// Destination exec-server WebSocket URL.
#[arg(long, value_name = "URL", requires = "exec_server_remote")]
connect: String,
},
}
#[derive(Debug, clap::Subcommand)]
#[allow(clippy::enum_variant_names)]
enum AppServerSubcommand {
/// Manage the local app-server daemon.
Daemon(AppServerDaemonCommand),
/// Proxy stdio bytes to the running app-server control socket.
Proxy(AppServerProxyCommand),
/// [experimental] Generate TypeScript bindings for the app server protocol.
GenerateTs(GenerateTsCommand),
/// [experimental] Generate JSON Schema for the app server protocol.
GenerateJsonSchema(GenerateJsonSchemaCommand),
/// [internal] Generate internal JSON Schema artifacts for Codex tooling.
#[clap(hide = true)]
GenerateInternalJsonSchema(GenerateInternalJsonSchemaCommand),
}
#[derive(Debug, Args)]
struct AppServerDaemonCommand {
#[command(subcommand)]
subcommand: AppServerDaemonSubcommand,
}
#[derive(Debug, clap::Subcommand)]
enum AppServerDaemonSubcommand {
/// Install durable local app-server management for SSH-driven use.
Bootstrap(AppServerBootstrapCommand),
/// Start the local app server daemon if it is not already running.
Start,
/// Restart the local app server daemon.
Restart,
/// Update the daemon package (may interrupt running work).
Update {
/// Copy and pin this CLI package.
#[arg(long)]
from_cli: bool,
/// Confirm replacing the daemon package without an interactive prompt.
#[arg(short = 'y', long, requires = "from_cli")]
yes: bool,
},
/// Enable remote control for future starts and a currently running managed daemon.
EnableRemoteControl,
/// Disable remote control for future starts and a currently running managed daemon.
DisableRemoteControl,
/// Stop the local app server daemon.
Stop,
/// Print local CLI and running app-server versions as JSON.
Version,
/// [internal] Run the detached pid-backed standalone updater loop.
#[clap(hide = true)]
PidUpdateLoop {
/// Check support for daemon-owned packages without starting the updater.
#[arg(long, hide = true)]
check_package_ownership: bool,
/// Authorize one production restoration of this selected release.
#[arg(long, hide = true)]
restore_release: Option<String>,
},
}
#[derive(Debug, Args)]
struct AppServerProxyCommand {
/// Path to the app-server Unix domain socket to connect to.
#[arg(long = "sock", value_name = "SOCKET_PATH", value_parser = parse_socket_path)]
socket_path: Option<AbsolutePathBuf>,
}
#[derive(Debug, Args)]
struct AppServerBootstrapCommand {
/// Launch the managed app-server with remote control enabled.
#[arg(long = "remote-control")]
remote_control: bool,
}
#[derive(Debug, Args)]
struct GenerateTsCommand {
/// Output directory where .ts files will be written
#[arg(short = 'o', long = "out", value_name = "DIR")]
out_dir: PathBuf,
/// Optional path to the Prettier executable to format generated files
#[arg(short = 'p', long = "prettier", value_name = "PRETTIER_BIN")]
prettier: Option<PathBuf>,
/// Include experimental methods and fields in the generated output
#[arg(long = "experimental", default_value_t = false)]
experimental: bool,
}
#[derive(Debug, Args)]
struct GenerateJsonSchemaCommand {
/// Output directory where the schema bundle will be written
#[arg(short = 'o', long = "out", value_name = "DIR")]
out_dir: PathBuf,
/// Include experimental methods and fields in the generated output
#[arg(long = "experimental", default_value_t = false)]
experimental: bool,
}
#[derive(Debug, Args)]
struct GenerateInternalJsonSchemaCommand {
/// Output directory where internal JSON Schema artifacts will be written
#[arg(short = 'o', long = "out", value_name = "DIR")]
out_dir: PathBuf,
}
#[derive(Debug, Parser)]
struct StdioToUdsCommand {
/// Path to the Unix domain socket to connect to.
#[arg(value_name = "SOCKET_PATH", value_parser = parse_socket_path)]
socket_path: AbsolutePathBuf,
}
fn parse_socket_path(raw: &str) -> Result<AbsolutePathBuf, String> {
AbsolutePathBuf::relative_to_current_dir(raw)
.map_err(|err| format!("failed to resolve socket path `{raw}`: {err}"))
}
/// Handle the app exit and print the results. Optionally run the update action.
fn handle_app_exit(
exit_info: AppExitInfo,
cli_executable: Option<&std::path::Path>,
) -> anyhow::Result<()> {
let is_fatal = match &exit_info.exit_reason {
ExitReason::Fatal(message) => {
eprintln!("ERROR: {message}");
true
}
ExitReason::UserRequested
| ExitReason::Archived(_)
| ExitReason::TurnInterrupted
| ExitReason::ThreadRemoved => false,
};
let update_action = exit_info.update_action;
if !matches!(update_action, Some(UpdateAction::Daemon(_))) {
let color_enabled = supports_color::on(Stream::Stdout).is_some();
for line in exit_info.format_exit_messages(color_enabled) {
println!("{line}");
}
}
if is_fatal {
std::io::stdout().flush()?;
std::process::exit(1);
}
if let Some(action) = update_action {
run_update_action(action, cli_executable)?;
}
Ok(())
}
/// Run the update action and print the result.
fn run_update_action(
action: UpdateAction,
cli_executable: Option<&std::path::Path>,
) -> anyhow::Result<()> {
if let UpdateAction::Daemon(source) = action {
let executable = cli_executable
.ok_or_else(|| anyhow::anyhow!("Cannot locate the launching Codex CLI"))?;
println!("Updating the local background server...");
let status = std::process::Command::new(executable)
.args(source.command_args())
.status()?;
anyhow::ensure!(
status.success(),
"Daemon update failed with status {status}"
);
println!("Relaunch Codex to reconnect.");
return Ok(());
}
println!();
let cmd_str = action.command_str();
println!("Updating Codex via `{cmd_str}`...");
let status = {
#[cfg(windows)]
{
let (cmd, args) = action.command_args();
let cmd = if action == UpdateAction::StandaloneWindows {
// These args contain PowerShell metacharacters, so do not let
// PATHEXT select a batch shim for this action.
"powershell.exe"
} else {
cmd
};
let path_env =
std::env::var_os("PATH").ok_or_else(|| anyhow::anyhow!("PATH is not set"))?;
let command_path = resolve_windows_update_command_from_path(cmd, &path_env)?;
// Do not let a project-local command or package-manager config
// influence the updater after the user accepts the update prompt.
let update_cwd = tempfile::tempdir()?;
// Resolve through PATH without consulting the project cwd. When
// this returns a .cmd/.bat shim, std::process::Command routes the
// absolute path through the system command processor.
std::process::Command::new(command_path)
.args(args)
.current_dir(update_cwd.path())
.status()?
}
#[cfg(not(windows))]
{
let (cmd, args) = action.command_args();
let command_path = crate::wsl_paths::normalize_for_wsl(cmd);
let normalized_args: Vec<String> = args
.iter()
.map(crate::wsl_paths::normalize_for_wsl)
.collect();
std::process::Command::new(&command_path)
.args(&normalized_args)
.status()?
}
};
if !status.success() {
anyhow::bail!("`{cmd_str}` failed with status {status}");
}
println!("\n🎉 Update ran successfully! Please restart Codex.");
Ok(())
}
#[cfg(windows)]
fn resolve_windows_update_command_from_path(
command: &str,
path_env: &std::ffi::OsStr,
) -> anyhow::Result<PathBuf> {
let path_env =
std::env::join_paths(std::env::split_paths(path_env).filter(|path| path.is_absolute()))?;
if path_env.is_empty() {
anyhow::bail!(
"Could not find an absolute update command `{command}` on PATH. Please update manually: https://developers.openai.com/codex/cli/"
);
}
which::which_in_global(command, Some(&path_env))?
.next()
.ok_or_else(|| anyhow::anyhow!("could not find update command `{command}` on PATH"))
}
fn run_update_command() -> anyhow::Result<()> {
#[cfg(debug_assertions)]
{
anyhow::bail!(
"`codex update` is not available in debug builds. Install a release build of Codex to use this command."
);
}
#[cfg(not(debug_assertions))]
{
let Some(action) = codex_tui::get_update_action() else {
anyhow::bail!(
"Could not detect the Codex installation method. Please update manually: https://developers.openai.com/codex/cli/"
);
};
run_update_action(action, /*cli_executable*/ None)
}
}
fn run_execpolicycheck(cmd: ExecPolicyCheckCommand) -> anyhow::Result<()> {
cmd.run()
}
async fn run_session_archive_cli_command(
action: codex_tui::SessionArchiveAction,
cmd: SessionArchiveCommand,
mut interactive: TuiCli,
root_config_overrides: CliConfigOverrides,
root_remote: Option<String>,
root_remote_auth_token_env: Option<String>,
arg0_paths: Arg0DispatchPaths,
) -> anyhow::Result<String> {
let SessionArchiveCommand {
target,
remote,
config_overrides,
} = cmd;
interactive =
finalize_session_archive_interactive(interactive, root_config_overrides, config_overrides);
let explicit_remote_endpoint = resolve_remote_endpoint(
remote.remote.or(root_remote),
remote.remote_auth_token_env.or(root_remote_auth_token_env),
)?;
codex_tui::run_session_archive_command(
action,
target,
codex_tui::SessionArchiveCommandOptions {
cli: interactive,
arg0_paths,
explicit_remote_endpoint,
},
)
.await
.map_err(|err| anyhow::anyhow!("{err}"))
}
fn delete_action(target: &str, force: bool) -> anyhow::Result<codex_tui::SessionArchiveAction> {
if force && codex_protocol::ThreadId::from_string(target).is_err() {
anyhow::bail!("--force requires a session UUID; names must be confirmed interactively");
}
let confirmation = match force {
true => codex_tui::DeleteConfirmation::Skip,
false => codex_tui::DeleteConfirmation::Prompt,
};
Ok(codex_tui::SessionArchiveAction::Delete(confirmation))
}
async fn run_debug_app_server_command(cmd: DebugAppServerCommand) -> anyhow::Result<()> {
match cmd.subcommand {
DebugAppServerSubcommand::SendMessageV2(cmd) => {
let codex_bin = std::env::current_exe()?;
codex_app_server_test_client::send_message_v2(&codex_bin, &[], cmd.user_message, &None)
.await
}
}
}
#[derive(Debug, Default, Parser, Clone)]
struct FeatureToggles {
/// Enable a feature (repeatable). Equivalent to `-c features.<name>=true`.
#[arg(long = "enable", value_name = "FEATURE", action = clap::ArgAction::Append, global = true)]
enable: Vec<String>,
/// Disable a feature (repeatable). Equivalent to `-c features.<name>=false`.
#[arg(long = "disable", value_name = "FEATURE", action = clap::ArgAction::Append, global = true)]
disable: Vec<String>,
}
#[derive(Debug, Default, Parser, Clone)]
struct InteractiveRemoteOptions {
/// Connect the TUI to a remote app server endpoint.
///
/// Accepted forms: `ws://host:port`, `wss://host:port`, `unix://`, or `unix://PATH`.
#[arg(long = "remote", value_name = "ADDR")]
remote: Option<String>,
/// Name of the environment variable containing the bearer token to send to
/// a remote app server websocket.
#[arg(long = "remote-auth-token-env", value_name = "ENV_VAR")]
remote_auth_token_env: Option<String>,
}
impl FeatureToggles {
fn to_overrides(&self) -> anyhow::Result<Vec<String>> {
let mut v = Vec::new();
for feature in &self.enable {
Self::validate_feature(feature)?;
v.push(format!("features.{feature}=true"));
}
for feature in &self.disable {
Self::validate_feature(feature)?;
v.push(format!("features.{feature}=false"));
}
Ok(v)
}
fn validate_feature(feature: &str) -> anyhow::Result<()> {
if is_known_feature_key(feature) {
Ok(())
} else {
anyhow::bail!("Unknown feature flag: {feature}")
}
}
}
#[derive(Debug, Parser)]
struct FeaturesCli {
#[command(subcommand)]
sub: FeaturesSubcommand,
}
#[derive(Debug, Parser)]
enum FeaturesSubcommand {
/// List known features with their stage and effective state.
List,
/// Enable a feature in config.toml.
Enable(FeatureSetArgs),
/// Disable a feature in config.toml.
Disable(FeatureSetArgs),
}
#[derive(Debug, Parser)]
struct FeatureSetArgs {
/// Feature key to update (for example: unified_exec).
feature: String,
}
fn stage_str(stage: Stage) -> &'static str {
match stage {
Stage::UnderDevelopment => "under development",
Stage::Experimental { .. } => "experimental",
Stage::Stable => "stable",
Stage::Deprecated => "deprecated",
Stage::Removed => "removed",
}
}
fn main() -> anyhow::Result<()> {
codex_build_info::initialize!();
let remote_control_disabled = codex_app_server::take_remote_control_disabled_env();
arg0_dispatch_or_else(move |arg0_paths: Arg0DispatchPaths| async move {
cli_main(arg0_paths, remote_control_disabled).await?;
Ok(())
})
}
async fn cli_main(
arg0_paths: Arg0DispatchPaths,
remote_control_disabled: bool,
) -> anyhow::Result<()> {
let MultitoolCli {
config_overrides: mut root_config_overrides,
feature_toggles,
remote,
mut interactive,
subcommand,
} = MultitoolCli::parse();
// Retain the launch target through TUI exit, even if a launcher changes selection.
let daemon_cli_executable = arg0_paths
.codex_self_exe
.as_ref()
.and_then(|path| path.canonicalize().ok());
interactive.daemon_cli_executable = daemon_cli_executable
.clone()
.and_then(|path| AbsolutePathBuf::from_absolute_path(path).ok());
reject_unsupported_worktree_for_subcommand(interactive.shared.worktree, &subcommand)?;
// Fold --enable/--disable into config overrides so they flow to all subcommands.
let toggle_overrides = feature_toggles.to_overrides()?;
root_config_overrides.raw_overrides.extend(toggle_overrides);
let agents_options = match &subcommand {
Some(Subcommand::Agents(options)) => Some(options),
_ => None,
};
if let Some(options) = agents_options
&& let Some(root_endpoint) = &remote.remote
&& let Some(agents_endpoint) = &options.remote.remote
&& root_endpoint != agents_endpoint
{
anyhow::bail!("`codex agents` received conflicting remote server endpoints");
}
let root_remote = agents_options
.and_then(|options| options.remote.remote.clone())
.or(remote.remote);
let root_remote_auth_token_env = agents_options
.and_then(|options| options.remote.remote_auth_token_env.clone())
.or(remote.remote_auth_token_env);
if let Some(options) = agents_options {
interactive.cwd = options.cwd.clone().or(interactive.cwd.take());
interactive.no_alt_screen |= options.no_alt_screen;
}
let root_strict_config = interactive.strict_config;
interactive
.shared
.take_auto_review_config_overrides(&mut root_config_overrides);
reject_root_strict_config_for_subcommand(root_strict_config, &subcommand)?;
if let Some(subcommand) = subcommand.as_ref() {
profile_v2_for_subcommand(&interactive, subcommand)?;
}
let open_agents_overview = matches!(&subcommand, Some(Subcommand::Agents(_)));
match subcommand {
None | Some(Subcommand::Agents(_)) => {
prepend_config_flags(
&mut interactive.config_overrides,
root_config_overrides.clone(),
);
if open_agents_overview {
if interactive.prompt.is_some() || !interactive.images.is_empty() {
anyhow::bail!("`codex agents` does not accept an initial prompt or images");
}
if root_remote.is_some()
&& (interactive.oss
|| interactive.oss_provider.is_some()
|| !interactive.add_dir.is_empty()
|| interactive
.config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?
.iter()
.any(|(key, value)| {
key == "sandbox_workspace_write.writable_roots"
|| (key == "sandbox_workspace_write"
&& value.get("writable_roots").is_some())
}))
{
anyhow::bail!(
"`codex agents` cannot apply local provider or additional-directory overrides to a remote server"
);
}
if is_workload_identity_selected() {
anyhow::bail!(
"`codex agents` is unavailable while workload identity is active"
);
}
if root_remote.is_none() {
resolve_remote_endpoint(
/*remote*/ None,
root_remote_auth_token_env.clone(),
)?;
#[cfg(not(any(unix, windows)))]
anyhow::bail!("`codex agents` requires `--remote` on this platform");
}
interactive.agents_overview = true;
}
let exit_info = run_interactive_tui(
interactive,
root_remote.clone(),
root_remote_auth_token_env.clone(),
arg0_paths.clone(),
)
.await?;
handle_app_exit(exit_info, daemon_cli_executable.as_deref())?;
}
Some(Subcommand::TcpTunnel(args)) => {
return codex_tcp_tunnel::run(args).await;
}
Some(Subcommand::Exec(mut exec_cli)) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"exec",
)?;
exec_cli
.shared
.inherit_exec_root_options(&interactive.shared);
exec_cli.strict_config |= root_strict_config;
prepend_config_flags(
&mut exec_cli.config_overrides,
root_config_overrides.clone(),
);
codex_exec::run_main(exec_cli, arg0_paths.clone()).await?;
}
Some(Subcommand::Review(ReviewCommand {
strict_config,
args: review_args,
})) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"review",
)?;
let mut exec_cli = ExecCli::try_parse_from(["codex", "exec"])?;
exec_cli
.shared
.inherit_exec_root_options(&interactive.shared);
exec_cli.command = Some(ExecCommand::Review(review_args));
exec_cli.strict_config = strict_config || root_strict_config;
prepend_config_flags(
&mut exec_cli.config_overrides,
root_config_overrides.clone(),
);
codex_exec::run_main(exec_cli, arg0_paths.clone()).await?;
}
Some(Subcommand::Mcp(mut mcp_cli)) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"mcp",
)?;
// Propagate any root-level config overrides (e.g. `-c key=value`).
prepend_config_flags(&mut mcp_cli.config_overrides, root_config_overrides.clone());
let loader_overrides =
loader_overrides_for_profile(interactive.config_profile_v2.as_ref())?;
mcp_cli.run(loader_overrides).await?;
}
Some(Subcommand::Plugin(plugin_cli)) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"plugin",
)?;
let PluginCli {
mut config_overrides,
subcommand,
} = plugin_cli;
prepend_config_flags(&mut config_overrides, root_config_overrides.clone());
match subcommand {
PluginSubcommand::Add(args) => {
let overrides = config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
plugin_cmd::run_plugin_add(overrides, args).await?;
}
PluginSubcommand::List(args) => {
let overrides = config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
plugin_cmd::run_plugin_list(overrides, args).await?;
}
PluginSubcommand::Marketplace(mut marketplace_cli) => {
prepend_config_flags(&mut marketplace_cli.config_overrides, config_overrides);
marketplace_cli.run().await?;
}
PluginSubcommand::Remove(args) => {
let overrides = config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
plugin_cmd::run_plugin_remove(overrides, args).await?;
}
}
}
Some(Subcommand::AppServer(app_server_cli)) => {
let AppServerCommand {
subcommand,
code_mode_host,
strict_config: app_server_strict_config,
listen,
stdio,
remote_control,
managed_daemon,
analytics_default_enabled,
auth,
} = app_server_cli;
let strict_config = app_server_strict_config || root_strict_config;
reject_strict_config_for_app_server_subcommand(strict_config, subcommand.as_ref())?;
reject_remote_mode_for_app_server_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
subcommand.as_ref(),
)?;
match subcommand {
None => {
let transport = if stdio {
codex_app_server::AppServerTransport::Stdio
} else {
listen
};
let auth = auth.try_into_settings()?;
let runtime_options = codex_app_server::AppServerRuntimeOptions {
code_mode_host_transport: code_mode_host.into(),
managed_daemon,
remote_control_startup_mode: match (remote_control, remote_control_disabled)
{
(true, _) => {
codex_app_server::RemoteControlStartupMode::EnabledEphemeral
}
(false, true) => {
codex_app_server::RemoteControlStartupMode::DisabledEphemeral
}
(false, false) => {
codex_app_server::RemoteControlStartupMode::ResolvePersisted
}
},
..Default::default()
};
let exit = codex_app_server::run_main_with_transport_options(
arg0_paths.clone(),
root_config_overrides,
LoaderOverrides::default(),
strict_config,
analytics_default_enabled,
transport,
codex_protocol::protocol::SessionSource::VSCode,
auth,
runtime_options,
)
.await?;
if exit == codex_app_server::AppServerExit::Forced {
// Runtime teardown can wait forever for blocked rollout I/O.
std::process::exit(0);
}
}
Some(AppServerSubcommand::Daemon(daemon_cli)) => match daemon_cli.subcommand {
AppServerDaemonSubcommand::Start => {
print_app_server_daemon_output(AppServerLifecycleCommand::Start).await?;
}
AppServerDaemonSubcommand::Bootstrap(bootstrap_cli) => {
let output =
codex_app_server_daemon::bootstrap(AppServerBootstrapOptions {
remote_control_enabled: bootstrap_cli.remote_control,
})
.await?;
println!("{}", serde_json::to_string(&output)?);
}
AppServerDaemonSubcommand::Restart => {
print_app_server_daemon_output(AppServerLifecycleCommand::Restart).await?;
}
AppServerDaemonSubcommand::Update {
from_cli: true,
yes,
} => {
if let Some(output) = codex_app_server_daemon::update_from_cli(|request| {
daemon_install::confirm_install(request, yes)
})
.await?
{
println!("{}", serde_json::to_string(&output)?);
}
}
AppServerDaemonSubcommand::EnableRemoteControl => {
print_app_server_remote_control_output(AppServerRemoteControlMode::Enabled)
.await?;
}
AppServerDaemonSubcommand::DisableRemoteControl => {
print_app_server_remote_control_output(
AppServerRemoteControlMode::Disabled,
)
.await?;
}
AppServerDaemonSubcommand::Stop => {
print_app_server_daemon_output(AppServerLifecycleCommand::Stop).await?;
}
AppServerDaemonSubcommand::Version => {
print_app_server_daemon_output(AppServerLifecycleCommand::Version).await?;
}
AppServerDaemonSubcommand::PidUpdateLoop {
check_package_ownership: true,
..
} => return Ok(()),
AppServerDaemonSubcommand::Update {
from_cli: false, ..
}
| AppServerDaemonSubcommand::PidUpdateLoop {
check_package_ownership: false,
..
} => {
let cli_overrides = root_config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
let config = ConfigBuilder::default()
.cli_overrides(cli_overrides)
.build()
.await
.map_err(anyhow::Error::from);
let http_client_factory = updater_http_client_factory(config);
if matches!(
daemon_cli.subcommand,
AppServerDaemonSubcommand::Update { .. }
) {
let output =
codex_app_server_daemon::update(http_client_factory).await?;
println!("{}", serde_json::to_string(&output)?);
} else {
let AppServerDaemonSubcommand::PidUpdateLoop {
restore_release, ..
} = daemon_cli.subcommand
else {
unreachable!()
};
codex_app_server_daemon::run_pid_update_loop(
http_client_factory,
restore_release,
)
.await?;
}
}
},
Some(AppServerSubcommand::Proxy(proxy_cli)) => {
let socket_path = match proxy_cli.socket_path {
Some(socket_path) => socket_path,
None => {
let codex_home = find_codex_home()?;
codex_app_server::app_server_control_socket_path(&codex_home)?
}
};
codex_stdio_to_uds::run(socket_path.as_path()).await?;
}
Some(AppServerSubcommand::GenerateTs(gen_cli)) => {
let options = codex_app_server_protocol::GenerateTsOptions {
experimental_api: gen_cli.experimental,
..Default::default()
};
codex_app_server_protocol::generate_ts_with_options(
&gen_cli.out_dir,
gen_cli.prettier.as_deref(),
options,
)?;
}
Some(AppServerSubcommand::GenerateJsonSchema(gen_cli)) => {
codex_app_server_protocol::generate_json_with_experimental(
&gen_cli.out_dir,
gen_cli.experimental,
)?;
}
Some(AppServerSubcommand::GenerateInternalJsonSchema(gen_cli)) => {
codex_app_server_protocol::generate_internal_json_schema(&gen_cli.out_dir)?;
}
}
}
Some(Subcommand::RemoteControl(remote_control_cli)) => {
let subcommand_name = remote_control_cli.subcommand_name();
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
subcommand_name,
)?;
remote_control_cmd::run(
remote_control_cli,
arg0_paths.clone(),
root_config_overrides,
)
.await?;
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
Some(Subcommand::App(app_cli)) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"app",
)?;
app_cmd::run_app(app_cli).await?;
}
Some(Subcommand::Resume(ResumeCommand {
session_id,
last,
all,
include_non_interactive,
remote,
config_overrides,
})) => {
let SessionTuiCli(config_overrides) = config_overrides;
interactive = finalize_resume_interactive(
interactive,
root_config_overrides.clone(),
session_id,
last,
all,
include_non_interactive,
config_overrides,
);
let exit_info = run_interactive_tui(
interactive,
remote.remote.or(root_remote.clone()),
remote
.remote_auth_token_env
.or(root_remote_auth_token_env.clone()),
arg0_paths.clone(),
)
.await?;
handle_app_exit(exit_info, daemon_cli_executable.as_deref())?;
}
Some(Subcommand::Archive(cmd)) => {
let output = run_session_archive_cli_command(
codex_tui::SessionArchiveAction::Archive,
cmd,
interactive,
root_config_overrides.clone(),
root_remote.clone(),
root_remote_auth_token_env.clone(),
arg0_paths.clone(),
)
.await?;
println!("{output}");
}
Some(Subcommand::Queue(cmd)) => {
let output = queue_cmd::run_queue_command(
cmd,
interactive,
root_config_overrides.clone(),
root_remote.clone(),
root_remote_auth_token_env.clone(),
arg0_paths.clone(),
)
.await?;
println!("{output}");
}
Some(Subcommand::Delete(DeleteCommand { session, force })) => {
let action = delete_action(&session.target, force)?;
let output = run_session_archive_cli_command(
action,
session,
interactive,
root_config_overrides.clone(),
root_remote.clone(),
root_remote_auth_token_env.clone(),
arg0_paths.clone(),
)
.await?;
println!("{output}");
}
Some(Subcommand::MigrateRollouts(command)) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"migrate-rollouts",
)?;
migrate_rollouts::run(command, root_config_overrides).await?;
}
Some(Subcommand::Unarchive(cmd)) => {
let output = run_session_archive_cli_command(
codex_tui::SessionArchiveAction::Unarchive,
cmd,
interactive,
root_config_overrides.clone(),
root_remote.clone(),
root_remote_auth_token_env.clone(),
arg0_paths.clone(),
)
.await?;
println!("{output}");
}
Some(Subcommand::Fork(ForkCommand {
session_id,
last,
all,
remote,
config_overrides,
})) => {
let SessionTuiCli(config_overrides) = config_overrides;
interactive = finalize_fork_interactive(
interactive,
root_config_overrides.clone(),
session_id,
last,
all,
config_overrides,
);
let exit_info = run_interactive_tui(
interactive,
remote.remote.or(root_remote.clone()),
remote
.remote_auth_token_env
.or(root_remote_auth_token_env.clone()),
arg0_paths.clone(),
)
.await?;
handle_app_exit(exit_info, daemon_cli_executable.as_deref())?;
}
Some(Subcommand::Login(mut login_cli)) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"login",
)?;
prepend_config_flags(
&mut login_cli.config_overrides,
root_config_overrides.clone(),
);
match login_cli.action {
Some(LoginSubcommand::Status) => {
run_login_status(login_cli.config_overrides).await;
}
None => {
if login_cli.with_api_key && login_cli.with_access_token {
eprintln!(
"Choose one login credential source: --with-api-key or --with-access-token."
);
std::process::exit(1);
} else if login_cli.use_device_code {
run_login_with_device_code(
login_cli.config_overrides,
login_cli.issuer_base_url,
login_cli.client_id,
)
.await;
} else if login_cli.api_key.is_some() {
eprintln!(
"The --api-key flag is no longer supported. Pipe the key instead, e.g. `printenv OPENAI_API_KEY | codex login --with-api-key`."
);
std::process::exit(1);
} else if login_cli.with_api_key {
let api_key = read_api_key_from_stdin();
run_login_with_api_key(login_cli.config_overrides, api_key).await;
} else if login_cli.with_access_token {
let access_token = read_access_token_from_stdin();
run_login_with_access_token(login_cli.config_overrides, access_token).await;
} else {
run_login_with_chatgpt(login_cli.config_overrides).await;
}
}
}
}
Some(Subcommand::Logout(mut logout_cli)) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"logout",
)?;
prepend_config_flags(
&mut logout_cli.config_overrides,
root_config_overrides.clone(),
);
run_logout(logout_cli.config_overrides).await;
}
Some(Subcommand::Completion(completion_cli)) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"completion",
)?;
print_completion(completion_cli);
}
Some(Subcommand::Update) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"update",
)?;
run_update_command()?;
}
Some(Subcommand::Doctor(doctor_cli)) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"doctor",
)?;
doctor::run_doctor(
doctor_cli,
root_config_overrides.clone(),
&interactive,
&arg0_paths,
)
.await?;
}
Some(Subcommand::Cloud(mut cloud_cli)) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"cloud",
)?;
prepend_config_flags(
&mut cloud_cli.config_overrides,
root_config_overrides.clone(),
);
codex_cloud_tasks::run_main(cloud_cli, arg0_paths.codex_linux_sandbox_exe.clone())
.await?;
}
Some(Subcommand::Sandbox(mut sandbox_cli)) => {
let config_profile = sandbox_cli
.config_profile
.as_ref()
.or(interactive.config_profile_v2.as_ref());
prepend_config_flags(
&mut sandbox_cli.config_overrides,
root_config_overrides.clone(),
);
#[cfg(target_os = "windows")]
if let Some(setup_cli) = sandbox_setup::parse_setup_command(&sandbox_cli.command)? {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"sandbox setup",
)?;
let cli_overrides = sandbox_cli
.config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
sandbox_setup::run(setup_cli, config_profile.cloned(), cli_overrides).await?;
return Ok(());
}
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"sandbox",
)?;
let loader_overrides = loader_overrides_for_profile(config_profile)?;
#[cfg(target_os = "macos")]
codex_cli::run_command_under_seatbelt(
sandbox_cli,
arg0_paths.codex_linux_sandbox_exe.clone(),
loader_overrides,
)
.await?;
#[cfg(target_os = "linux")]
codex_cli::run_command_under_landlock(
sandbox_cli,
arg0_paths.codex_linux_sandbox_exe.clone(),
loader_overrides,
)
.await?;
#[cfg(target_os = "windows")]
codex_cli::run_command_under_windows_sandbox(
sandbox_cli,
arg0_paths.codex_linux_sandbox_exe.clone(),
loader_overrides,
)
.await?;
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
let _ = loader_overrides;
anyhow::bail!("`codex sandbox` is not supported on this operating system");
}
}
Some(Subcommand::Debug(DebugCommand { subcommand })) => match subcommand {
DebugSubcommand::Models(cmd) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"debug models",
)?;
run_debug_models_command(cmd, root_config_overrides).await?;
}
DebugSubcommand::AppServer(cmd) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"debug app-server",
)?;
run_debug_app_server_command(cmd).await?;
}
DebugSubcommand::PromptInput(cmd) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"debug prompt-input",
)?;
run_debug_prompt_input_command(
cmd,
root_config_overrides,
interactive,
arg0_paths.clone(),
)
.await?;
}
DebugSubcommand::TraceReduce(cmd) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"debug trace-reduce",
)?;
run_debug_trace_reduce_command(cmd).await?;
}
DebugSubcommand::ClearMemories => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"debug clear-memories",
)?;
run_debug_clear_memories_command(&root_config_overrides).await?;
}
},
Some(Subcommand::Execpolicy(ExecpolicyCommand { sub })) => match sub {
ExecpolicySubcommand::Check(cmd) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"execpolicy check",
)?;
run_execpolicycheck(cmd)?
}
},
Some(Subcommand::Apply(mut apply_cli)) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"apply",
)?;
prepend_config_flags(
&mut apply_cli.config_overrides,
root_config_overrides.clone(),
);
run_apply_command(apply_cli, /*cwd*/ None).await?;
}
Some(Subcommand::ResponsesApiProxy(args)) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"responses-api-proxy",
)?;
tokio::task::spawn_blocking(move || codex_responses_api_proxy::run_main(args))
.await??;
}
Some(Subcommand::StdioToUds(cmd)) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"stdio-to-uds",
)?;
let socket_path = cmd.socket_path;
codex_stdio_to_uds::run(socket_path.as_path()).await?;
}
Some(Subcommand::ExecServer(cmd)) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"exec-server",
)?;
let strict_config = cmd.strict_config || root_strict_config;
run_exec_server_command(cmd, &arg0_paths, &root_config_overrides, strict_config)
.await?;
}
Some(Subcommand::Features(FeaturesCli { sub })) => match sub {
FeaturesSubcommand::List => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"features list",
)?;
// Honor `--search` via the canonical web_search mode.
if interactive.web_search {
root_config_overrides
.raw_overrides
.push("web_search=\"live\"".to_string());
}
let config =
cloud_config::load_config(&root_config_overrides, LoaderOverrides::default())
.await?;
let mut rows = Vec::with_capacity(FEATURES.len());
let mut name_width = 0;
let mut stage_width = 0;
for def in FEATURES {
let name = def.key;
let stage = stage_str(def.stage);
let enabled = config.features.enabled(def.id);
name_width = name_width.max(name.len());
stage_width = stage_width.max(stage.len());
rows.push((name, stage, enabled));
}
rows.sort_unstable_by_key(|(name, _, _)| *name);
for (name, stage, enabled) in rows {
println!("{name:<name_width$} {stage:<stage_width$} {enabled}");
}
}
FeaturesSubcommand::Enable(FeatureSetArgs { feature }) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"features enable",
)?;
enable_feature_in_config(&feature).await?;
}
FeaturesSubcommand::Disable(FeatureSetArgs { feature }) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"features disable",
)?;
disable_feature_in_config(&feature).await?;
}
},
}
Ok(())
}
fn profile_v2_for_subcommand<'a>(
interactive: &'a TuiCli,
subcommand: &Subcommand,
) -> anyhow::Result<Option<&'a ProfileV2Name>> {
let Some(profile_v2) = interactive.config_profile_v2.as_ref() else {
return Ok(None);
};
match subcommand {
Subcommand::Agents(_)
| Subcommand::Exec(_)
| Subcommand::Review(_)
| Subcommand::Resume(_)
| Subcommand::Queue(_)
| Subcommand::Archive(_)
| Subcommand::Delete(_)
| Subcommand::Unarchive(_)
| Subcommand::Fork(_)
| Subcommand::Mcp(_)
| Subcommand::Sandbox(_)
| Subcommand::Debug(DebugCommand {
subcommand: DebugSubcommand::PromptInput(_),
}) => Ok(Some(profile_v2)),
_ => anyhow::bail!(
"--profile only applies to runtime commands and `codex mcp`: `codex`, `codex exec`, `codex review`, `codex resume`, `codex queue`, `codex archive`, `codex delete`, `codex unarchive`, `codex fork`, `codex mcp`, `codex sandbox`, and `codex debug prompt-input`."
),
}
}
async fn run_exec_server_command(
mut cmd: ExecServerCommand,
arg0_paths: &Arg0DispatchPaths,
root_config_overrides: &CliConfigOverrides,
strict_config: bool,
) -> anyhow::Result<()> {
cmd.validate_remote_transport()?;
let codex_self_exe = arg0_paths
.codex_self_exe
.clone()
.ok_or_else(|| anyhow::anyhow!("Codex executable path is not configured"))?;
let runtime_paths =
ExecServerRuntimePaths::new(codex_self_exe, arg0_paths.codex_linux_sandbox_exe.clone())?;
if let Some(base_url) = cmd.remote.take() {
let environment_id = cmd
.environment_id
.take()
.ok_or_else(|| anyhow::anyhow!("--environment-id is required when --remote is set"))?;
let config = load_exec_server_config(
root_config_overrides,
strict_config,
/*enable_workload_identity*/ true,
)
.await?;
let direct_transport = cmd.remote_transport == ExecServerRemoteTransport::Direct;
let (_otel, telemetry) = exec_server_telemetry::init(Some(&config));
let auth_provider = if cmd.aws_sigv4 {
exec_server_auth::aws_sigv4_auth_provider(codex_aws_auth::AwsAuthConfig {
profile: cmd.aws_profile,
region: cmd.aws_region,
service: cmd.aws_service,
})
.await?
} else {
load_exec_server_remote_auth_provider(&config, &base_url, cmd.use_agent_identity_auth)
.await?
};
let mut remote_config = codex_exec_server::RemoteEnvironmentConfig::new_with_transport(
base_url,
environment_id,
if direct_transport {
codex_exec_server::RemoteEnvironmentTransport::Direct
} else {
codex_exec_server::RemoteEnvironmentTransport::Noise
},
auth_provider,
config.http_client_factory(),
)?;
if let Some(name) = cmd.name {
remote_config.name = name;
}
remote_config.request_dispatch_mode = cmd.request_dispatch_mode;
let remote_config = remote_config.with_telemetry(telemetry);
let parent_lifetime = if cmd.exit_on_stdin_close {
exec_server_telemetry::ParentLifetime::StdinPipe
} else {
exec_server_telemetry::ParentLifetime::Independent
};
let (shutdown_sender, shutdown_receiver) = tokio::sync::oneshot::channel();
#[cfg(target_os = "macos")]
let runtime_paths = runtime_paths.with_allowed_symlinked_codex_home(
codex_config::allowed_symlinked_codex_home(
&config.config_layer_stack,
&config.codex_home,
),
);
exec_server_telemetry::run_until_shutdown(
async move {
let shutdown = async move {
let _ = shutdown_receiver.await;
};
match cmd.command {
Some(ExecServerSubcommand::Forward { connect }) => {
codex_exec_server::run_remote_environment_forward_until_shutdown(
remote_config,
connect,
shutdown,
)
.await
}
None => {
codex_exec_server::run_remote_environment_until_shutdown(
remote_config,
runtime_paths,
shutdown,
)
.await
}
}
.map_err(anyhow::Error::new)
},
parent_lifetime,
exec_server_telemetry::ShutdownBehavior::Graceful(shutdown_sender),
)
.await
} else {
let config_result = load_exec_server_config(
root_config_overrides,
strict_config,
/*enable_workload_identity*/ false,
)
.await;
let config = if strict_config {
Some(config_result?)
} else {
config_result.ok()
};
let (_otel, telemetry) = exec_server_telemetry::init(config.as_ref());
#[cfg(target_os = "macos")]
let runtime_paths =
runtime_paths.with_allowed_symlinked_codex_home(config.as_ref().and_then(|config| {
codex_config::allowed_symlinked_codex_home(
&config.config_layer_stack,
&config.codex_home,
)
}));
let http_client_factory = config
.as_ref()
.map(Config::http_client_factory)
.unwrap_or_else(|| HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault));
let listen_url = cmd
.listen
.unwrap_or_else(|| codex_exec_server::DEFAULT_LISTEN_URL.to_string());
let run = exec_server_telemetry::run_until_shutdown(
codex_exec_server::run_main_with_telemetry(
&listen_url,
runtime_paths,
telemetry,
http_client_factory,
cmd.request_dispatch_mode,
),
exec_server_telemetry::ParentLifetime::Independent,
exec_server_telemetry::ShutdownBehavior::Immediate,
);
run.await.map_err(anyhow::Error::from_boxed)
}
}
async fn load_exec_server_remote_auth_provider(
config: &codex_core::config::Config,
base_url: &str,
use_agent_identity_auth: bool,
) -> anyhow::Result<codex_api::SharedAuthProvider> {
if use_agent_identity_auth {
read_codex_access_token_from_env().ok_or_else(|| {
anyhow::anyhow!("CODEX_ACCESS_TOKEN is required when --use-agent-identity-auth is set")
})?;
let auth = AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false)
.await?
.auth()
.await
.ok_or_else(|| anyhow::anyhow!("Agent Identity authentication is unavailable"))?;
if !matches!(auth, CodexAuth::AgentIdentity(_)) {
anyhow::bail!(
"CODEX_ACCESS_TOKEN did not provide permitted Agent Identity authentication"
);
}
return Ok(codex_model_provider::auth_provider_from_auth(&auth));
}
let (auth_manager, auth) = load_exec_server_remote_auth(
config,
"remote exec-server registration requires ChatGPT authentication or API key authentication; run `codex login` or set CODEX_API_KEY",
)
.await?;
if !is_supported_exec_server_remote_auth(&auth) {
anyhow::bail!(
"remote exec-server registration requires ChatGPT authentication or API key authentication; Agent Identity auth requires --use-agent-identity-auth"
);
}
if auth.is_api_key_auth() {
validate_api_key_remote_host(base_url)?;
}
if auth_manager.is_workload_identity_selected() {
Ok(codex_model_provider::auth_provider_from_auth_manager(
auth_manager,
&auth,
))
} else {
Ok(codex_model_provider::auth_provider_from_auth(&auth))
}
}
fn is_supported_exec_server_remote_auth(auth: &CodexAuth) -> bool {
auth.is_chatgpt_auth() || auth.is_api_key_auth()
}
fn validate_api_key_remote_host(base_url: &str) -> anyhow::Result<()> {
let url = url::Url::parse(base_url)
.map_err(|err| anyhow::anyhow!("invalid remote exec-server registration URL: {err}"))?;
let host = url.host().ok_or_else(|| {
anyhow::anyhow!("remote exec-server registration URL must include a host")
})?;
let is_loopback = match &host {
url::Host::Domain(host) => host.eq_ignore_ascii_case("localhost"),
url::Host::Ipv4(ip) => ip.is_loopback(),
url::Host::Ipv6(ip) => ip.is_loopback(),
};
let is_openai_host = match &host {
url::Host::Domain(host) => ["openai.com", "openai.org"].into_iter().any(|domain| {
host.eq_ignore_ascii_case(domain)
|| host.to_ascii_lowercase().ends_with(&format!(".{domain}"))
}),
_ => false,
};
let is_allowed = match url.scheme() {
"https" => is_loopback || is_openai_host,
"http" => is_loopback,
_ => false,
};
if !is_allowed {
anyhow::bail!(
"remote exec-server API-key authentication is restricted to HTTPS openai.com and openai.org hosts and subdomains or loopback hosts"
);
}
Ok(())
}
async fn load_exec_server_config(
root_config_overrides: &CliConfigOverrides,
strict_config: bool,
enable_workload_identity: bool,
) -> anyhow::Result<codex_core::config::Config> {
let cli_kv_overrides = root_config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
let bootstrap_cli_overrides = cli_kv_overrides.clone();
let mut builder = ConfigBuilder::default()
.cli_overrides(cli_kv_overrides)
.strict_config(strict_config);
if enable_workload_identity && is_workload_identity_selected() {
let codex_home = find_codex_home()?;
let bootstrap_cwd = AbsolutePathBuf::current_dir()?;
let bootstrap_config = load_config_toml_with_layer_stack(
&codex_home,
Some(&bootstrap_cwd),
bootstrap_cli_overrides,
ConfigLoadOptions {
loader_overrides: LoaderOverrides::default(),
strict_config,
cloud_config_bundle: Default::default(),
},
)
.await?;
let bootstrap_auth_config = bootstrap_auth_config(&codex_home, &bootstrap_config)?;
let cloud_config_bundle = cloud_config_bundle_loader_for_storage(
bootstrap_auth_config,
/*enable_codex_api_key_env*/ false,
)
.await?;
builder = builder.cloud_config_bundle(cloud_config_bundle);
}
Ok(builder.build().await?)
}
async fn load_exec_server_remote_auth(
config: &codex_core::config::Config,
missing_auth_error: &'static str,
) -> anyhow::Result<(Arc<AuthManager>, codex_login::CodexAuth)> {
let auth_manager =
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ true).await?;
let auth = match auth_manager.auth().await {
Some(auth) => auth,
None => {
auth_manager.reload().await;
auth_manager
.auth()
.await
.ok_or_else(|| anyhow::anyhow!(missing_auth_error))?
}
};
Ok((auth_manager, auth))
}
async fn enable_feature_in_config(feature: &str) -> anyhow::Result<()> {
FeatureToggles::validate_feature(feature)?;
let codex_home = find_codex_home()?;
ConfigEditsBuilder::new(&codex_home)
.set_feature_enabled(feature, /*enabled*/ true)
.apply()
.await?;
println!("Enabled feature `{feature}` in config.toml.");
maybe_print_under_development_feature_warning(&codex_home, feature);
Ok(())
}
async fn disable_feature_in_config(feature: &str) -> anyhow::Result<()> {
FeatureToggles::validate_feature(feature)?;
let codex_home = find_codex_home()?;
ConfigEditsBuilder::new(&codex_home)
.set_feature_enabled(feature, /*enabled*/ false)
.apply()
.await?;
println!("Disabled feature `{feature}` in config.toml.");
Ok(())
}
fn loader_overrides_for_profile(
profile_v2: Option<&ProfileV2Name>,
) -> anyhow::Result<LoaderOverrides> {
match profile_v2 {
Some(profile_v2) => {
let codex_home = find_codex_home()?;
Ok(loader_overrides_for_profile_at_codex_home(
Some(profile_v2),
&codex_home,
))
}
None => Ok(LoaderOverrides::default()),
}
}
fn loader_overrides_for_profile_at_codex_home(
profile_v2: Option<&ProfileV2Name>,
codex_home: &std::path::Path,
) -> LoaderOverrides {
match profile_v2 {
Some(profile_v2) => LoaderOverrides {
user_config_path: Some(resolve_profile_v2_config_path(codex_home, profile_v2)),
user_config_profile: Some(profile_v2.clone()),
..Default::default()
},
None => LoaderOverrides::default(),
}
}
fn maybe_print_under_development_feature_warning(codex_home: &std::path::Path, feature: &str) {
let Some(spec) = FEATURES.iter().find(|spec| spec.key == feature) else {
return;
};
if !matches!(spec.stage, Stage::UnderDevelopment) {
return;
}
let config_path = codex_home.join(codex_config::CONFIG_TOML_FILE);
eprintln!(
"Under-development features enabled: {feature}. Under-development features are incomplete and may behave unpredictably. To suppress this warning, set `suppress_unstable_features_warning = true` in {}.",
config_path.display()
);
}
async fn run_debug_trace_reduce_command(cmd: DebugTraceReduceCommand) -> anyhow::Result<()> {
let output = cmd
.output
.unwrap_or_else(|| cmd.trace_bundle.join(REDUCED_STATE_FILE_NAME));
let trace = replay_bundle(&cmd.trace_bundle)?;
let reduced_json = serde_json::to_vec_pretty(&trace)?;
tokio::fs::write(&output, reduced_json).await?;
println!("{}", output.display());
Ok(())
}
async fn run_debug_prompt_input_command(
cmd: DebugPromptInputCommand,
root_config_overrides: CliConfigOverrides,
interactive: TuiCli,
arg0_paths: Arg0DispatchPaths,
) -> anyhow::Result<()> {
let loader_overrides = loader_overrides_for_profile(interactive.config_profile_v2.as_ref())?;
let shared = interactive.shared.into_inner();
let mut cli_kv_overrides = root_config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
if interactive.web_search {
cli_kv_overrides.push((
"web_search".to_string(),
toml::Value::String("live".to_string()),
));
}
let approval_policy = if shared.dangerously_bypass_approvals_and_sandbox {
Some(AskForApproval::Never)
} else {
interactive.approval_policy.map(Into::into)
};
let sandbox_mode = if shared.dangerously_bypass_approvals_and_sandbox {
Some(codex_protocol::config_types::SandboxMode::DangerFullAccess)
} else {
shared.sandbox_mode.map(Into::into)
};
let overrides = ConfigOverrides {
model: shared.model,
approval_policy,
sandbox_mode,
cwd: shared.cwd,
codex_self_exe: arg0_paths.codex_self_exe,
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe,
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe,
show_raw_agent_reasoning: shared.oss.then_some(true),
ephemeral: Some(true),
bypass_hook_trust: shared.bypass_hook_trust.then_some(true),
additional_writable_roots: shared.add_dir,
..Default::default()
};
let config = ConfigBuilder::default()
.cli_overrides(cli_kv_overrides)
.harness_overrides(overrides)
.loader_overrides(loader_overrides)
.build()
.await?;
let mut input = shared
.images
.into_iter()
.chain(cmd.images)
.map(|path| UserInput::LocalImage { path, detail: None })
.collect::<Vec<_>>();
if let Some(prompt) = cmd.prompt.or(interactive.prompt) {
input.push(UserInput::Text {
text: prompt.replace("\r\n", "\n").replace('\r', "\n"),
text_elements: Vec::new(),
});
}
let user_instructions_provider = Arc::new(CodexHomeUserInstructionsProvider::new(
config.codex_home.clone(),
));
let auth_manager =
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await?;
let mut extensions = codex_extension_api::ExtensionRegistryBuilder::new();
codex_git_attribution::install(
&mut extensions,
auth_manager,
config.chatgpt_base_url.clone(),
config.http_client_factory(),
);
codex_skills_extension::install(&mut extensions, |config: &Config| {
codex_skills_extension::SkillsExtensionConfig {
include_instructions: config.include_skill_instructions,
max_context_tokens: config.skill_max_context_tokens,
bundled_skills_enabled: config.bundled_skills_enabled(),
orchestrator_skills_enabled: config.orchestrator_skills_enabled,
shadow_selection_enabled: config
.features
.enabled(codex_features::Feature::SkillSearch),
}
});
let prompt_input = codex_core::build_prompt_input(
config,
input,
/*state_db*/ None,
Arc::new(extensions.build()),
user_instructions_provider,
)
.await?;
println!("{}", serde_json::to_string_pretty(&prompt_input)?);
Ok(())
}
async fn run_debug_models_command(
cmd: DebugModelsCommand,
root_config_overrides: CliConfigOverrides,
) -> anyhow::Result<()> {
let catalog = if cmd.bundled {
bundled_models_response()?
} else {
let cli_overrides = root_config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
let config = ConfigBuilder::default()
.cli_overrides(cli_overrides)
.build()
.await?;
let auth_manager =
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ true).await?;
let models_manager = build_models_manager(&config, auth_manager);
models_manager
.raw_model_catalog(
RefreshStrategy::OnlineIfUncached,
config.http_client_factory(),
)
.await
};
serde_json::to_writer(std::io::stdout(), &catalog)?;
println!();
Ok(())
}
async fn run_debug_clear_memories_command(
root_config_overrides: &CliConfigOverrides,
) -> anyhow::Result<()> {
let cli_kv_overrides = root_config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
let config = ConfigBuilder::default()
.cli_overrides(cli_kv_overrides)
.build()
.await?;
let memories_path = config.sqlite_config().memories_db_path();
let cleared_memories_db =
StateRuntime::clear_memory_data_in_sqlite_home(config.sqlite_config()).await?;
clear_memory_roots_contents(&config.codex_home).await?;
let mut message = if cleared_memories_db {
format!("Cleared memory state from {}.", memories_path.display())
} else {
format!("No memories db found at {}.", memories_path.display())
};
message.push_str(&format!(
" Cleared memory directories under {}.",
config.codex_home.display()
));
println!("{message}");
Ok(())
}
/// Prepend root-level overrides so they have lower precedence than
/// CLI-specific ones specified after the subcommand (if any).
fn prepend_config_flags(
subcommand_config_overrides: &mut CliConfigOverrides,
cli_config_overrides: CliConfigOverrides,
) {
subcommand_config_overrides.prepend_root_overrides(cli_config_overrides);
}
fn reject_remote_mode_for_subcommand(
remote: Option<&str>,
remote_auth_token_env: Option<&str>,
subcommand: &str,
) -> anyhow::Result<()> {
if let Some(remote) = remote {
anyhow::bail!(
"`--remote {remote}` is only supported for interactive TUI commands, not `codex {subcommand}`"
);
}
if remote_auth_token_env.is_some() {
anyhow::bail!(
"`--remote-auth-token-env` is only supported for interactive TUI commands, not `codex {subcommand}`"
);
}
Ok(())
}
fn reject_unsupported_worktree_for_subcommand(
root_worktree: bool,
subcommand: &Option<Subcommand>,
) -> anyhow::Result<()> {
let subcommand_worktree = match subcommand {
Some(Subcommand::Exec(command)) => command.shared.worktree,
Some(Subcommand::Resume(command)) => command.config_overrides.0.shared.worktree,
Some(Subcommand::Fork(command)) => command.config_overrides.0.shared.worktree,
Some(Subcommand::Archive(command)) | Some(Subcommand::Unarchive(command)) => {
command.config_overrides.shared.worktree
}
Some(Subcommand::Delete(command)) => command.session.config_overrides.shared.worktree,
Some(Subcommand::Queue(command)) => command.config_overrides.shared.worktree,
_ => false,
};
if !root_worktree && !subcommand_worktree {
return Ok(());
}
match subcommand {
None => Ok(()),
Some(Subcommand::Fork(command)) if command.session_id.is_some() && !command.last => Ok(()),
Some(Subcommand::Fork(_)) => {
anyhow::bail!("`codex fork --worktree` requires an explicit session ID")
}
Some(Subcommand::Exec(command)) => match &command.command {
None | Some(ExecCommand::Fork(_)) => Ok(()),
Some(ExecCommand::Resume(_)) => anyhow::bail!(
"`--worktree` cannot resume an existing session; use `codex exec fork --worktree`"
),
Some(ExecCommand::Review(_)) => {
anyhow::bail!("`--worktree` is not supported for code review")
}
},
_ => {
anyhow::bail!(
"`--worktree` supports new interactive sessions, `codex fork`, `codex exec`, and `codex exec fork`"
)
}
}
}
fn reject_root_strict_config_for_subcommand(
strict_config: bool,
subcommand: &Option<Subcommand>,
) -> anyhow::Result<()> {
if !strict_config {
return Ok(());
}
match unsupported_subcommand_name_for_strict_config(subcommand) {
Some(subcommand_name) => {
reject_strict_config_for_unsupported_subcommand(strict_config, subcommand_name)
}
None => Ok(()),
}
}
/// Return the selected subcommand name when a root-level `--strict-config`
/// flag should be rejected after parsing.
///
/// `--strict-config` is parsed on the root interactive CLI so commands like
/// `codex --strict-config` continue to work for the TUI and for wrappers that
/// forward root options into another command shape. Clap will still accept that
/// root flag before the dispatcher knows which subcommand the user selected, so
/// unsupported subcommands need an explicit post-parse reject path.
///
/// `Some(...)` returns the user-facing command name fragment to embed in the
/// rejection error, such as `cloud` or `app-server proxy`. `None` means the
/// selected command is allowed to inherit root `--strict-config`.
fn unsupported_subcommand_name_for_strict_config(
subcommand: &Option<Subcommand>,
) -> Option<&'static str> {
match subcommand {
None
| Some(Subcommand::Agents(_))
| Some(Subcommand::Exec(_))
| Some(Subcommand::Review(_))
| Some(Subcommand::ExecServer(_))
| Some(Subcommand::Resume(_))
| Some(Subcommand::Queue(_))
| Some(Subcommand::Archive(_))
| Some(Subcommand::Delete(_))
| Some(Subcommand::Unarchive(_))
| Some(Subcommand::Fork(_))
| Some(Subcommand::Doctor(_)) => None,
Some(Subcommand::AppServer(app_server)) if app_server.subcommand.is_none() => None,
Some(Subcommand::AppServer(app_server)) => {
Some(app_server_subcommand_name(app_server.subcommand.as_ref()))
}
Some(Subcommand::RemoteControl(remote_control)) => Some(remote_control.subcommand_name()),
Some(Subcommand::Mcp(_)) => Some("mcp"),
Some(Subcommand::Plugin(_)) => Some("plugin"),
Some(Subcommand::MigrateRollouts(_)) => Some("migrate-rollouts"),
#[cfg(any(target_os = "macos", target_os = "windows"))]
Some(Subcommand::App(_)) => Some("app"),
Some(Subcommand::Login(_)) => Some("login"),
Some(Subcommand::Logout(_)) => Some("logout"),
Some(Subcommand::Completion(_)) => Some("completion"),
Some(Subcommand::Update) => Some("update"),
Some(Subcommand::Cloud(_)) => Some("cloud"),
Some(Subcommand::Sandbox(_)) => Some("sandbox"),
Some(Subcommand::Debug(_)) => Some("debug"),
Some(Subcommand::Execpolicy(_)) => Some("execpolicy"),
Some(Subcommand::Apply(_)) => Some("apply"),
Some(Subcommand::ResponsesApiProxy(_)) => Some("responses-api-proxy"),
Some(Subcommand::StdioToUds(_)) => Some("stdio-to-uds"),
Some(Subcommand::Features(_)) => Some("features"),
Some(Subcommand::TcpTunnel(_)) => Some("tcp-tunnel"),
}
}
fn reject_strict_config_for_app_server_subcommand(
strict_config: bool,
subcommand: Option<&AppServerSubcommand>,
) -> anyhow::Result<()> {
if subcommand.is_none() {
return Ok(());
}
reject_strict_config_for_unsupported_subcommand(
strict_config,
app_server_subcommand_name(subcommand),
)
}
fn reject_strict_config_for_unsupported_subcommand(
strict_config: bool,
subcommand: &str,
) -> anyhow::Result<()> {
if strict_config {
anyhow::bail!("`--strict-config` is not supported for `codex {subcommand}`");
}
Ok(())
}
fn reject_remote_mode_for_app_server_subcommand(
remote: Option<&str>,
remote_auth_token_env: Option<&str>,
subcommand: Option<&AppServerSubcommand>,
) -> anyhow::Result<()> {
let subcommand_name = app_server_subcommand_name(subcommand);
reject_remote_mode_for_subcommand(remote, remote_auth_token_env, subcommand_name)
}
fn app_server_subcommand_name(subcommand: Option<&AppServerSubcommand>) -> &'static str {
match subcommand {
None => "app-server",
Some(AppServerSubcommand::Daemon(daemon)) => match daemon.subcommand {
AppServerDaemonSubcommand::Bootstrap(_) => "app-server daemon bootstrap",
AppServerDaemonSubcommand::Start => "app-server daemon start",
AppServerDaemonSubcommand::Restart => "app-server daemon restart",
AppServerDaemonSubcommand::Update { .. } => "app-server daemon update",
AppServerDaemonSubcommand::EnableRemoteControl => {
"app-server daemon enable-remote-control"
}
AppServerDaemonSubcommand::DisableRemoteControl => {
"app-server daemon disable-remote-control"
}
AppServerDaemonSubcommand::Stop => "app-server daemon stop",
AppServerDaemonSubcommand::Version => "app-server daemon version",
AppServerDaemonSubcommand::PidUpdateLoop { .. } => "app-server daemon pid-update-loop",
},
Some(AppServerSubcommand::Proxy(_)) => "app-server proxy",
Some(AppServerSubcommand::GenerateTs(_)) => "app-server generate-ts",
Some(AppServerSubcommand::GenerateJsonSchema(_)) => "app-server generate-json-schema",
Some(AppServerSubcommand::GenerateInternalJsonSchema(_)) => {
"app-server generate-internal-json-schema"
}
}
}
async fn print_app_server_daemon_output(command: AppServerLifecycleCommand) -> anyhow::Result<()> {
let output = codex_app_server_daemon::run(command).await?;
println!("{}", serde_json::to_string(&output)?);
Ok(())
}
fn updater_http_client_factory(
config: anyhow::Result<codex_core::config::Config>,
) -> codex_http_client::HttpClientFactory {
match config {
Ok(config) => config.http_client_factory(),
Err(error) => {
eprintln!("warning: failed to load updater network configuration: {error}");
codex_http_client::HttpClientFactory::new(
codex_http_client::OutboundProxyPolicy::ReqwestDefault,
)
}
}
}
async fn print_app_server_remote_control_output(
mode: AppServerRemoteControlMode,
) -> anyhow::Result<()> {
let output = codex_app_server_daemon::set_remote_control(mode).await?;
println!("{}", serde_json::to_string(&output)?);
Ok(())
}
fn read_remote_auth_token_from_env_var_with<F>(
env_var_name: &str,
get_var: F,
) -> anyhow::Result<String>
where
F: FnOnce(&str) -> Result<String, std::env::VarError>,
{
let auth_token = get_var(env_var_name)
.map_err(|_| anyhow::anyhow!("environment variable `{env_var_name}` is not set"))?;
let auth_token = auth_token.trim().to_string();
if auth_token.is_empty() {
anyhow::bail!("environment variable `{env_var_name}` is empty");
}
Ok(auth_token)
}
fn read_remote_auth_token_from_env_var(env_var_name: &str) -> anyhow::Result<String> {
read_remote_auth_token_from_env_var_with(env_var_name, |name| std::env::var(name))
}
async fn run_interactive_tui(
mut interactive: TuiCli,
remote: Option<String>,
remote_auth_token_env: Option<String>,
arg0_paths: Arg0DispatchPaths,
) -> std::io::Result<AppExitInfo> {
if let Some(prompt) = interactive.prompt.take() {
// Normalize CRLF/CR to LF so CLI-provided text can't leak `\r` into TUI state.
interactive.prompt = Some(prompt.replace("\r\n", "\n").replace('\r', "\n"));
}
let terminal_info = codex_terminal_detection::terminal_info();
if terminal_info.name == TerminalName::Dumb {
if !(std::io::stdin().is_terminal() && std::io::stderr().is_terminal()) {
return Ok(AppExitInfo::fatal(
"TERM is set to \"dumb\". Refusing to start the interactive TUI because no terminal is available for a confirmation prompt (stdin/stderr is not a TTY). Run in a supported terminal or unset TERM.",
));
}
eprintln!(
"WARNING: TERM is set to \"dumb\". Codex's interactive TUI may not work in this terminal."
);
if !confirm("Continue anyway? [y/N]: ")? {
return Ok(AppExitInfo::fatal(
"Refusing to start the interactive TUI because TERM is set to \"dumb\". Run in a supported terminal or unset TERM.",
));
}
}
#[cfg(any(unix, windows))]
if interactive.agents_overview && remote.is_none() {
if !std::io::stdin().is_terminal() {
return Ok(AppExitInfo::fatal("stdin is not a terminal"));
}
if !std::io::stdout().is_terminal() {
return Ok(AppExitInfo::fatal("stdout is not a terminal"));
}
cloud_config::load_config(&interactive.config_overrides, LoaderOverrides::default())
.await
.map_err(std::io::Error::other)?;
codex_app_server_daemon::run(AppServerLifecycleCommand::Start)
.await
.map_err(std::io::Error::other)?;
}
let remote_endpoint = match resolve_remote_endpoint(remote, remote_auth_token_env.clone()) {
Ok(remote_endpoint) => remote_endpoint,
Err(err) if is_remote_auth_usage_error(&err) => {
return Ok(AppExitInfo::fatal(err.to_string()));
}
Err(err) => return Err(err),
};
let start_tui = || {
codex_tui::run_main(
interactive.clone(),
arg0_paths.clone(),
codex_config::LoaderOverrides::default(),
remote_endpoint.clone(),
)
};
run_tui_with_recovery(start_tui, remote_auth_token_env.as_deref()).await
}
async fn run_tui_with_recovery<F, Fut>(
mut start_tui: F,
remote_auth_token_env: Option<&str>,
) -> std::io::Result<AppExitInfo>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = std::io::Result<AppExitInfo>>,
{
let mut attempted_backups = HashSet::new();
loop {
// Keep the large TUI future out of the CLI dispatcher's stack frame.
let err = match Box::pin(start_tui()).await {
Ok(mut exit_info) => {
if let Some(disconnect) = &mut exit_info.disconnect_info
&& let Some(env_var) = remote_auth_token_env
{
disconnect
.command
.extend(["--remote-auth-token-env".to_string(), env_var.to_string()]);
}
return Ok(exit_info);
}
Err(err) => err,
};
let Some(startup_error) = local_state_db::startup_error(&err) else {
return Err(err);
};
if local_state_db::is_locked(startup_error.detail()) {
local_state_db::print_locked_guidance(startup_error);
return Ok(AppExitInfo::fatal(startup_error.to_string()));
}
if !local_state_db::is_auto_backup_recoverable(startup_error) {
local_state_db::print_diagnostic_guidance(startup_error);
return Ok(AppExitInfo::fatal(startup_error.to_string()));
}
if !attempted_backups.insert(startup_error.database_path().to_path_buf()) {
local_state_db::print_diagnostic_guidance(startup_error);
return Ok(AppExitInfo::fatal(startup_error.to_string()));
}
local_state_db::print_auto_backup_start(startup_error);
match local_state_db::backup_files_for_fresh_start(startup_error).await {
Ok(backups) => local_state_db::confirm_fresh_start_rebuild(startup_error, &backups)?,
Err(backup_err) => {
local_state_db::print_diagnostic_guidance(startup_error);
return Ok(AppExitInfo::fatal(format!(
"failed to move damaged Codex local database files into a backup folder automatically: {backup_err}"
)));
}
}
}
}
fn resolve_remote_endpoint(
remote: Option<String>,
remote_auth_token_env: Option<String>,
) -> std::io::Result<Option<codex_tui::RemoteAppServerEndpoint>> {
let mut remote_endpoint = remote
.as_deref()
.map(codex_tui::resolve_remote_addr)
.transpose()
.map_err(std::io::Error::other)?;
if let Some(remote_auth_token_env) = remote_auth_token_env {
let Some(endpoint) = remote_endpoint.as_mut() else {
return Err(std::io::Error::other(
"`--remote-auth-token-env` requires `--remote`.",
));
};
if !codex_tui::remote_addr_supports_auth_token(endpoint) {
return Err(std::io::Error::other(
"`--remote-auth-token-env` requires a `wss://` or loopback `ws://` remote.",
));
}
let auth_token = read_remote_auth_token_from_env_var(&remote_auth_token_env)
.map_err(std::io::Error::other)?;
let codex_tui::RemoteAppServerEndpoint::WebSocket {
auth_token: slot, ..
} = endpoint
else {
return Err(std::io::Error::other(
"`--remote-auth-token-env` requires a `wss://` or loopback `ws://` remote.",
));
};
*slot = Some(auth_token);
}
Ok(remote_endpoint)
}
fn is_remote_auth_usage_error(err: &std::io::Error) -> bool {
err.to_string()
.starts_with("`--remote-auth-token-env` requires")
}
fn confirm(prompt: &str) -> std::io::Result<bool> {
eprintln!("{prompt}");
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let answer = input.trim();
Ok(answer.eq_ignore_ascii_case("y") || answer.eq_ignore_ascii_case("yes"))
}
/// Build the final `TuiCli` for a `codex resume` invocation.
fn finalize_resume_interactive(
mut interactive: TuiCli,
root_config_overrides: CliConfigOverrides,
session_id: Option<String>,
last: bool,
show_all: bool,
include_non_interactive: bool,
mut resume_cli: TuiCli,
) -> TuiCli {
// Start with the parsed interactive CLI so resume shares the same
// configuration surface area as `codex` without additional flags.
// Clap assigns the first positional to `session_id`. With `--last`, reinterpret it as the
// prompt when no second positional prompt was provided.
let resume_session_id = if last && resume_cli.prompt.is_none() {
resume_cli.prompt = session_id;
None
} else {
session_id
};
interactive.resume_picker = resume_session_id.is_none() && !last;
interactive.resume_last = last;
interactive.resume_session_id = resume_session_id;
interactive.resume_show_all = show_all;
interactive.resume_include_non_interactive = include_non_interactive;
// Merge resume-scoped flags and overrides with highest precedence.
merge_interactive_cli_flags(&mut interactive, resume_cli);
// Propagate any root-level config overrides (e.g. `-c key=value`).
prepend_config_flags(&mut interactive.config_overrides, root_config_overrides);
interactive
}
/// Build the final `TuiCli` for a `codex fork` invocation.
fn finalize_fork_interactive(
mut interactive: TuiCli,
root_config_overrides: CliConfigOverrides,
session_id: Option<String>,
last: bool,
show_all: bool,
mut fork_cli: TuiCli,
) -> TuiCli {
// Start with the parsed interactive CLI so fork shares the same
// configuration surface area as `codex` without additional flags.
// Clap assigns the first positional to `session_id`. With `--last`, reinterpret it as the
// prompt when no second positional prompt was provided.
let fork_session_id = if last && fork_cli.prompt.is_none() {
fork_cli.prompt = session_id;
None
} else {
session_id
};
interactive.fork_picker = fork_session_id.is_none() && !last;
interactive.fork_last = last;
interactive.fork_session_id = fork_session_id;
interactive.fork_show_all = show_all;
// Merge fork-scoped flags and overrides with highest precedence.
merge_interactive_cli_flags(&mut interactive, fork_cli);
// Propagate any root-level config overrides (e.g. `-c key=value`).
prepend_config_flags(&mut interactive.config_overrides, root_config_overrides);
interactive
}
fn finalize_session_archive_interactive(
mut interactive: TuiCli,
root_config_overrides: CliConfigOverrides,
archive_cli: SessionArchiveConfigOverrides,
) -> TuiCli {
let SessionArchiveConfigOverrides {
shared,
strict_config,
config_overrides,
} = archive_cli;
interactive.shared.apply_subcommand_overrides(shared);
if strict_config {
interactive.strict_config = true;
}
interactive
.config_overrides
.raw_overrides
.extend(config_overrides.raw_overrides);
prepend_config_flags(&mut interactive.config_overrides, root_config_overrides);
interactive
}
/// Merge flags provided to runtime wrapper commands so they take precedence over any root-level
/// flags. Only overrides fields explicitly set on the subcommand-scoped CLI. Also appends
/// `-c key=value` overrides with highest precedence.
fn merge_interactive_cli_flags(interactive: &mut TuiCli, subcommand_cli: TuiCli) {
let TuiCli {
shared,
strict_config,
approval_policy,
web_search,
no_alt_screen,
prompt,
mut config_overrides,
..
} = subcommand_cli;
let subcommand_auto_review = shared.auto_review;
interactive
.shared
.apply_subcommand_overrides(shared.into_inner());
interactive
.shared
.take_auto_review_config_overrides(&mut config_overrides);
if subcommand_auto_review {
interactive.approval_policy = None;
} else if let Some(approval) = approval_policy {
interactive.approval_policy = Some(approval);
}
if web_search {
interactive.web_search = true;
}
interactive.no_alt_screen |= no_alt_screen;
if strict_config {
interactive.strict_config = true;
}
if let Some(prompt) = prompt {
// Normalize CRLF/CR to LF so CLI-provided text can't leak `\r` into TUI state.
interactive.prompt = Some(prompt.replace("\r\n", "\n").replace('\r', "\n"));
}
interactive
.config_overrides
.raw_overrides
.extend(config_overrides.raw_overrides);
}
fn print_completion(cmd: CompletionCommand) {
let mut app = MultitoolCli::command();
let name = "codex";
generate(cmd.shell, &mut app, name, &mut std::io::stdout());
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use codex_protocol::ThreadId;
use codex_tui::TokenUsage;
use pretty_assertions::assert_eq;
#[test]
fn interactive_tui_future_stays_bounded() {
let future = run_interactive_tui(
TuiCli::parse_from(["codex"]),
/*remote*/ None,
/*remote_auth_token_env*/ None,
Arg0DispatchPaths::default(),
);
let size = std::mem::size_of_val(&future);
assert!(size < 64 * 1024, "interactive TUI future is {size} bytes");
}
#[cfg(windows)]
#[test]
fn windows_update_command_resolution_ignores_relative_path_entries() {
let cwd = std::env::current_dir().expect("current directory");
let decoy_dir = tempfile::tempdir_in(&cwd).expect("relative decoy directory");
let trusted_dir = tempfile::tempdir().expect("trusted PATH directory");
let relative_decoy_dir = decoy_dir
.path()
.strip_prefix(&cwd)
.expect("decoy directory should be relative to cwd");
for command in ["npm.cmd", "pnpm.cmd", "bun.exe"] {
std::fs::write(decoy_dir.path().join(command), "decoy")
.expect("write cwd-relative decoy");
std::fs::write(trusted_dir.path().join(command), "trusted")
.expect("write trusted PATH command");
let path_env = std::env::join_paths([relative_decoy_dir, trusted_dir.path()])
.expect("join synthetic PATH");
let resolved = resolve_windows_update_command_from_path(command, &path_env)
.expect("resolve update command");
assert_eq!(resolved, trusted_dir.path().join(command));
}
let cwd_decoy = tempfile::Builder::new()
.suffix(".cmd")
.tempfile_in(&cwd)
.expect("cwd-local decoy");
let command = cwd_decoy
.path()
.file_name()
.and_then(|name| name.to_str())
.expect("decoy filename");
let relative_only_path_env = std::env::join_paths(["."]).expect("join relative-only PATH");
let err = resolve_windows_update_command_from_path(command, &relative_only_path_env)
.expect_err("relative-only PATH should not resolve a cwd command");
assert_eq!(
err.to_string(),
format!(
"Could not find an absolute update command `{command}` on PATH. Please update manually: https://developers.openai.com/codex/cli/"
)
);
}
#[tokio::test]
async fn updater_http_client_factory_honors_respect_system_proxy() {
let codex_home = tempfile::tempdir().expect("temporary Codex home");
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.cli_overrides(vec![(
"features.respect_system_proxy".to_string(),
toml::Value::Boolean(true),
)])
.build()
.await
.expect("config should load");
assert_eq!(
updater_http_client_factory(Ok(config)).outbound_proxy_policy(),
codex_http_client::OutboundProxyPolicy::RespectSystemProxy
);
}
#[test]
fn updater_http_client_factory_falls_back_when_config_load_fails() {
assert_eq!(
updater_http_client_factory(Err(anyhow::anyhow!("invalid config")))
.outbound_proxy_policy(),
codex_http_client::OutboundProxyPolicy::ReqwestDefault
);
}
#[test]
fn exec_server_remote_auth_accepts_api_key_auth() {
let auth = CodexAuth::from_api_key("sk-test");
assert!(is_supported_exec_server_remote_auth(&auth));
}
#[test]
fn exec_server_remote_api_key_auth_accepts_https_openai_domains() {
for base_url in [
"https://openai.com/api",
"https://service.openai.com/api",
"https://openai.org/api",
"https://service.openai.org/api",
] {
assert!(validate_api_key_remote_host(base_url).is_ok());
}
}
#[test]
fn exec_server_remote_api_key_auth_accepts_http_loopback() {
for base_url in [
"http://localhost:8098/api",
"http://127.0.0.1:8098/api",
"http://[::1]:8098/api",
] {
assert!(validate_api_key_remote_host(base_url).is_ok());
}
}
#[test]
fn exec_server_remote_api_key_auth_rejects_http_openai_domain() {
for base_url in [
"http://service.openai.com/api",
"http://service.openai.org/api",
] {
let error = validate_api_key_remote_host(base_url)
.expect_err("reject plaintext OpenAI destination");
assert_eq!(
error.to_string(),
"remote exec-server API-key authentication is restricted to HTTPS openai.com and openai.org hosts and subdomains or loopback hosts"
);
}
}
#[test]
fn exec_server_remote_api_key_auth_rejects_suffix_spoof() {
let error = validate_api_key_remote_host("https://service.openai.org.evil.example/api")
.expect_err("reject suffix spoof");
assert_eq!(
error.to_string(),
"remote exec-server API-key authentication is restricted to HTTPS openai.com and openai.org hosts and subdomains or loopback hosts"
);
}
fn finalize_resume_from_args(args: &[&str]) -> TuiCli {
let cli = MultitoolCli::try_parse_from(args).expect("parse");
let MultitoolCli {
mut interactive,
config_overrides: mut root_overrides,
subcommand,
feature_toggles: _,
remote: _,
} = cli;
interactive
.shared
.take_auto_review_config_overrides(&mut root_overrides);
let Subcommand::Resume(ResumeCommand {
session_id,
last,
all,
include_non_interactive,
remote: _,
config_overrides: resume_cli,
}) = subcommand.expect("resume present")
else {
unreachable!()
};
let SessionTuiCli(resume_cli) = resume_cli;
finalize_resume_interactive(
interactive,
root_overrides,
session_id,
last,
all,
include_non_interactive,
resume_cli,
)
}
fn finalize_fork_from_args(args: &[&str]) -> TuiCli {
let cli = MultitoolCli::try_parse_from(args).expect("parse");
let MultitoolCli {
mut interactive,
config_overrides: mut root_overrides,
subcommand,
feature_toggles: _,
remote: _,
} = cli;
interactive
.shared
.take_auto_review_config_overrides(&mut root_overrides);
let Subcommand::Fork(ForkCommand {
session_id,
last,
all,
remote: _,
config_overrides: fork_cli,
}) = subcommand.expect("fork present")
else {
unreachable!()
};
let SessionTuiCli(fork_cli) = fork_cli;
finalize_fork_interactive(interactive, root_overrides, session_id, last, all, fork_cli)
}
fn finalize_exec_from_args(args: &[&str]) -> ExecCli {
let mut cli = MultitoolCli::try_parse_from(args).expect("parse");
cli.interactive
.shared
.take_auto_review_config_overrides(&mut cli.config_overrides);
let Some(Subcommand::Exec(mut exec)) = cli.subcommand else {
panic!("expected exec subcommand");
};
exec.shared
.inherit_exec_root_options(&cli.interactive.shared);
prepend_config_flags(&mut exec.config_overrides, cli.config_overrides);
exec.shared
.take_auto_review_config_overrides(&mut exec.config_overrides);
exec
}
fn finalize_archive_from_args(args: &[&str]) -> (String, TuiCli, InteractiveRemoteOptions) {
let cli = MultitoolCli::try_parse_from(args).expect("parse");
let MultitoolCli {
interactive,
config_overrides: root_overrides,
subcommand,
feature_toggles: _,
remote: _,
} = cli;
let Subcommand::Archive(SessionArchiveCommand {
target,
remote,
config_overrides: archive_cli,
}) = subcommand.expect("archive present")
else {
unreachable!()
};
(
target,
finalize_session_archive_interactive(interactive, root_overrides, archive_cli),
remote,
)
}
fn profile_v2_for_args(args: &[&str]) -> anyhow::Result<Option<String>> {
let cli = MultitoolCli::try_parse_from(args).expect("parse");
let Some(subcommand) = cli.subcommand.as_ref() else {
return Ok(cli
.interactive
.config_profile_v2
.as_ref()
.map(std::string::ToString::to_string));
};
Ok(profile_v2_for_subcommand(&cli.interactive, subcommand)?.map(ToString::to_string))
}
#[test]
fn profile_loader_overrides_use_explicit_codex_home() -> anyhow::Result<()> {
let codex_home = tempfile::tempdir()?;
let profile: ProfileV2Name = "work".parse()?;
let overrides =
loader_overrides_for_profile_at_codex_home(Some(&profile), codex_home.path());
assert_eq!(
overrides.user_config_path,
Some(resolve_profile_v2_config_path(codex_home.path(), &profile))
);
assert_eq!(overrides.user_config_profile, Some(profile));
Ok(())
}
#[test]
fn profile_v2_is_rejected_for_config_management_subcommands() {
assert!(profile_v2_for_args(&["codex", "--profile", "work", "features", "list"]).is_err());
}
#[test]
fn profile_v2_is_allowed_for_runtime_subcommands() {
assert_eq!(
profile_v2_for_args(&["codex", "--profile", "work", "resume"])
.expect("resume supports profile-v2")
.as_deref(),
Some("work")
);
assert_eq!(
profile_v2_for_args(&["codex", "--profile", "work", "debug", "prompt-input"])
.expect("debug prompt-input supports profile-v2")
.as_deref(),
Some("work")
);
assert_eq!(
profile_v2_for_args(&["codex", "--profile", "work", "mcp", "list"])
.expect("mcp supports profile-v2")
.as_deref(),
Some("work")
);
assert_eq!(
profile_v2_for_args(&["codex", "--profile", "work", "sandbox"])
.expect("sandbox supports config profile")
.as_deref(),
Some("work")
);
}
#[test]
fn import_remains_an_interactive_prompt() {
let cli = MultitoolCli::try_parse_from(["codex", "import"]).expect("parse");
assert!(cli.subcommand.is_none());
assert_eq!(cli.interactive.prompt.as_deref(), Some("import"));
}
#[test]
fn profile_v2_rejects_non_plain_names_at_parse_time() {
assert!(
MultitoolCli::try_parse_from(["codex", "--profile", "nested/work", "resume"]).is_err()
);
}
#[test]
fn worktree_flag_supports_interactive_exec_and_explicit_fork_positions() {
let arguments = [
vec!["codex", "--worktree"],
vec!["codex", "--worktree", "hello"],
vec!["codex", "--worktree", "exec", "hello"],
vec!["codex", "exec", "--worktree", "hello"],
vec![
"codex",
"fork",
"--worktree",
"019f1234-5678-7000-8000-000000000001",
],
vec![
"codex",
"exec",
"fork",
"--worktree",
"019f1234-5678-7000-8000-000000000001",
],
];
for arguments in arguments {
let cli = MultitoolCli::try_parse_from(&arguments).expect("parse worktree command");
assert!(
reject_unsupported_worktree_for_subcommand(
cli.interactive.shared.worktree,
&cli.subcommand,
)
.is_ok(),
"supported worktree command should be accepted: {arguments:?}",
);
}
}
#[test]
fn worktree_flag_rejects_unsupported_session_and_management_commands() {
let arguments = [
vec!["codex", "--worktree", "login"],
vec!["codex", "fork", "--worktree"],
vec!["codex", "fork", "--worktree", "--last"],
vec!["codex", "exec", "resume", "--worktree", "session"],
vec!["codex", "exec", "review", "--worktree"],
vec!["codex", "resume", "--worktree", "session"],
vec!["codex", "archive", "session", "--worktree"],
vec![
"codex",
"queue",
"--thread",
"session",
"--message",
"hi",
"--worktree",
],
];
let mut errors = Vec::new();
for arguments in arguments {
let cli = MultitoolCli::try_parse_from(&arguments).expect("parse shared worktree flag");
let error = reject_unsupported_worktree_for_subcommand(
cli.interactive.shared.worktree,
&cli.subcommand,
)
.expect_err("unsupported worktree command");
errors.push(format!("{}: {error}", arguments.join(" ")));
}
insta::assert_snapshot!("unsupported_worktree_commands", errors.join("\n"));
}
#[test]
fn exec_resume_last_accepts_prompt_positional() {
let cli =
MultitoolCli::try_parse_from(["codex", "exec", "--json", "resume", "--last", "2+2"])
.expect("parse should succeed");
let Some(Subcommand::Exec(exec)) = cli.subcommand else {
panic!("expected exec subcommand");
};
let Some(codex_exec::Command::Resume(args)) = exec.command else {
panic!("expected exec resume");
};
assert!(args.last);
assert_eq!(args.session_id, None);
assert_eq!(args.prompt.as_deref(), Some("2+2"));
}
#[test]
fn exec_resume_accepts_output_flags_after_subcommand() {
let cli = MultitoolCli::try_parse_from([
"codex",
"exec",
"resume",
"session-123",
"-o",
"/tmp/resume-output.md",
"--output-schema",
"/tmp/schema.json",
"re-review",
])
.expect("parse should succeed");
let Some(Subcommand::Exec(exec)) = cli.subcommand else {
panic!("expected exec subcommand");
};
let Some(codex_exec::Command::Resume(args)) = exec.command else {
panic!("expected exec resume");
};
assert_eq!(
exec.last_message_file,
Some(std::path::PathBuf::from("/tmp/resume-output.md"))
);
assert_eq!(
exec.output_schema,
Some(std::path::PathBuf::from("/tmp/schema.json"))
);
assert_eq!(args.session_id.as_deref(), Some("session-123"));
assert_eq!(args.prompt.as_deref(), Some("re-review"));
}
#[test]
fn dangerous_bypass_conflicts_with_approval_policy() {
let err = MultitoolCli::try_parse_from([
"codex",
"--dangerously-bypass-approvals-and-sandbox",
"--ask-for-approval",
"on-request",
])
.expect_err("conflicting permission flags should be rejected");
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
#[test]
fn approve_for_me_configures_interactive_mode() {
for flag in ["--approve-for-me", "--not-so-yolo"] {
let mut cli = MultitoolCli::try_parse_from(["codex", flag]).expect("parse flag");
assert!(cli.interactive.auto_review);
cli.interactive
.shared
.take_auto_review_config_overrides(&mut cli.interactive.config_overrides);
assert_eq!(
cli.interactive.config_overrides.raw_overrides,
vec![
r#"approvals_reviewer="auto_review""#.to_string(),
r#"approval_policy="on-request""#.to_string(),
r#"sandbox_mode="workspace-write""#.to_string(),
]
);
assert!(!cli.interactive.auto_review);
}
}
#[test]
fn not_so_yolo_alias_is_hidden_from_help() {
for args in [&["codex", "--help"][..], &["codex", "exec", "--help"][..]] {
let help = help_from_args(args);
assert!(!help.contains("--not-so-yolo"), "{help}");
}
}
#[test]
fn approve_for_me_defaults_propagate_from_root_to_exec() {
let exec = finalize_exec_from_args(&["codex", "--approve-for-me", "exec", "summarize"]);
assert_eq!(
exec.config_overrides.raw_overrides,
vec![
r#"approvals_reviewer="auto_review""#.to_string(),
r#"approval_policy="on-request""#.to_string(),
r#"sandbox_mode="workspace-write""#.to_string(),
]
);
assert!(exec.sandbox_mode.is_none());
}
#[test]
fn later_exec_sandbox_partially_overrides_approve_for_me() {
let exec = finalize_exec_from_args(&[
"codex",
"--approve-for-me",
"exec",
"--sandbox",
"read-only",
]);
assert_matches!(
exec.sandbox_mode,
Some(codex_utils_cli::SandboxModeCliArg::ReadOnly)
);
assert_eq!(
exec.config_overrides.raw_overrides,
vec![
r#"approvals_reviewer="auto_review""#.to_string(),
r#"approval_policy="on-request""#.to_string(),
r#"sandbox_mode="workspace-write""#.to_string(),
]
);
}
#[test]
fn later_approve_for_me_overrides_root_exec_sandbox() {
let exec = finalize_exec_from_args(&[
"codex",
"--sandbox",
"read-only",
"exec",
"--approve-for-me",
]);
assert!(exec.sandbox_mode.is_none());
assert_eq!(
exec.config_overrides.raw_overrides,
vec![
r#"approvals_reviewer="auto_review""#.to_string(),
r#"approval_policy="on-request""#.to_string(),
r#"sandbox_mode="workspace-write""#.to_string(),
]
);
}
#[test]
fn later_resume_approval_policy_partially_overrides_approve_for_me() {
let interactive = finalize_resume_from_args(&[
"codex",
"--approve-for-me",
"resume",
"--ask-for-approval",
"never",
]);
assert_matches!(
interactive.approval_policy,
Some(codex_utils_cli::ApprovalModeCliArg::Never)
);
assert_eq!(
interactive.config_overrides.raw_overrides,
vec![
r#"approvals_reviewer="auto_review""#.to_string(),
r#"approval_policy="on-request""#.to_string(),
r#"sandbox_mode="workspace-write""#.to_string(),
]
);
}
#[test]
fn later_approve_for_me_overrides_root_tui_approval_policy() {
let interactive = finalize_resume_from_args(&[
"codex",
"--ask-for-approval",
"never",
"resume",
"--approve-for-me",
]);
assert!(interactive.approval_policy.is_none());
assert_eq!(
interactive.config_overrides.raw_overrides,
vec![
r#"approvals_reviewer="auto_review""#.to_string(),
r#"approval_policy="on-request""#.to_string(),
r#"sandbox_mode="workspace-write""#.to_string(),
]
);
}
#[test]
fn approve_for_me_conflicts_with_explicit_interactive_permissions() {
for conflicting_args in [
vec!["--sandbox", "read-only"],
vec!["--ask-for-approval", "on-request"],
vec!["--dangerously-bypass-approvals-and-sandbox"],
] {
let mut args = vec!["codex", "--approve-for-me"];
args.extend(conflicting_args);
let error =
MultitoolCli::try_parse_from(args).expect_err("permission flags should conflict");
assert_eq!(error.kind(), clap::error::ErrorKind::ArgumentConflict);
}
}
fn app_server_from_args(args: &[&str]) -> AppServerCommand {
let cli = MultitoolCli::try_parse_from(args).expect("parse");
let Subcommand::AppServer(app_server) = cli.subcommand.expect("app-server present") else {
unreachable!()
};
app_server
}
fn default_app_server_socket_path() -> AbsolutePathBuf {
let codex_home = find_codex_home().expect("codex home");
codex_app_server::app_server_control_socket_path(&codex_home)
.expect("default app-server socket path")
}
#[test]
fn debug_prompt_input_parses_prompt_and_images() {
let cli = MultitoolCli::try_parse_from([
"codex",
"debug",
"prompt-input",
"hello",
"--image",
"/tmp/a.png,/tmp/b.png",
])
.expect("parse");
let Some(Subcommand::Debug(DebugCommand {
subcommand: DebugSubcommand::PromptInput(cmd),
})) = cli.subcommand
else {
panic!("expected debug prompt-input subcommand");
};
assert_eq!(cmd.prompt.as_deref(), Some("hello"));
assert_eq!(
cmd.images,
vec![PathBuf::from("/tmp/a.png"), PathBuf::from("/tmp/b.png")]
);
}
#[test]
fn debug_models_parses_bundled_flag() {
let cli =
MultitoolCli::try_parse_from(["codex", "debug", "models", "--bundled"]).expect("parse");
let Some(Subcommand::Debug(DebugCommand {
subcommand: DebugSubcommand::Models(cmd),
})) = cli.subcommand
else {
panic!("expected debug models subcommand");
};
assert!(cmd.bundled);
}
#[test]
fn responses_subcommand_is_not_registered() {
let command = MultitoolCli::command();
assert!(
command
.get_subcommands()
.all(|subcommand| subcommand.get_name() != "responses")
);
}
fn help_from_args(args: &[&str]) -> String {
let err = MultitoolCli::try_parse_from(args).expect_err("help should short-circuit");
assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
err.to_string()
}
#[test]
fn tcp_tunnel_is_hidden_and_accepts_its_control_input_contract() {
let root_help = help_from_args(&["codex", "--help"]);
assert!(!root_help.contains("tcp-tunnel"), "{root_help}");
let help = help_from_args(&["codex", "tcp-tunnel", "--help"]);
for option in [
"--target",
"--auth-token-stdin",
"--auth-token-updates-stdin",
"--connect-headers-stdin",
] {
assert!(help.contains(option), "{help}");
}
let cli = MultitoolCli::try_parse_from([
"codex",
"tcp-tunnel",
"--proxy-url",
"https://proxy.example.org",
"--proxy-origins-file",
"/tmp/approved-origins",
"--target",
"[::1]:22",
"--auth-token-stdin",
"--auth-token-updates-stdin",
"--connect-headers-stdin",
])
.expect("generic tunnel should parse");
assert!(matches!(cli.subcommand, Some(Subcommand::TcpTunnel(_))));
}
#[test]
fn plugin_marketplace_help_uses_plugin_namespace() {
let help = help_from_args(&["codex", "plugin", "marketplace", "--help"]);
assert!(
help.contains("Usage: codex plugin marketplace [OPTIONS] <COMMAND>"),
"{help}"
);
for (subcommand, usage) in [
("add", "Usage: codex plugin marketplace add"),
("list", "Usage: codex plugin marketplace list"),
("upgrade", "Usage: codex plugin marketplace upgrade"),
("remove", "Usage: codex plugin marketplace remove"),
] {
let help = help_from_args(&["codex", "plugin", "marketplace", subcommand, "--help"]);
assert!(help.contains(usage), "{help}");
}
}
#[test]
fn plugin_marketplace_add_parses_under_plugin() {
let cli =
MultitoolCli::try_parse_from(["codex", "plugin", "marketplace", "add", "owner/repo"])
.expect("parse");
assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_))));
}
#[test]
fn plugin_marketplace_upgrade_parses_under_plugin() {
let cli =
MultitoolCli::try_parse_from(["codex", "plugin", "marketplace", "upgrade", "debug"])
.expect("parse");
assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_))));
}
#[test]
fn plugin_add_parses_under_plugin() {
let cli = MultitoolCli::try_parse_from([
"codex",
"plugin",
"add",
"sample",
"--marketplace",
"debug",
])
.expect("parse");
assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_))));
}
#[test]
fn plugin_list_parses_under_plugin() {
let cli =
MultitoolCli::try_parse_from(["codex", "plugin", "list", "--marketplace", "debug"])
.expect("parse");
assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_))));
}
#[test]
fn plugin_remove_parses_under_plugin() {
let cli = MultitoolCli::try_parse_from([
"codex",
"plugin",
"remove",
"sample",
"--marketplace",
"debug",
])
.expect("parse");
assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_))));
}
#[test]
fn update_parses_as_update_subcommand() {
let cli = MultitoolCli::try_parse_from(["codex", "update"]).expect("parse");
assert!(matches!(cli.subcommand, Some(Subcommand::Update)));
}
#[test]
fn archive_merges_scoped_tui_flags() {
let (target, interactive, remote) = finalize_archive_from_args(
[
"codex",
"-C",
"/root",
"archive",
"--remote",
"unix://archive.sock",
"--strict-config",
"--dangerously-bypass-hook-trust",
"-m",
"gpt-5.1-test",
"-p",
"work",
"-C",
"/archive",
"my-thread",
]
.as_ref(),
);
assert_eq!(target, "my-thread");
assert_eq!(remote.remote.as_deref(), Some("unix://archive.sock"));
assert_eq!(interactive.model.as_deref(), Some("gpt-5.1-test"));
assert_eq!(interactive.config_profile_v2.as_deref(), Some("work"));
assert_eq!(
interactive.cwd.as_deref(),
Some(std::path::Path::new("/archive"))
);
assert!(interactive.strict_config);
assert!(interactive.bypass_hook_trust);
}
#[test]
fn delete_force_requires_uuid() {
assert!(delete_action("123e4567-e89b-12d3-a456-426614174000", /*force*/ true).is_ok());
let err =
delete_action("my-thread", /*force*/ true).expect_err("name should require prompt");
assert_eq!(
err.to_string(),
"--force requires a session UUID; names must be confirmed interactively"
);
}
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
#[test]
fn sandbox_parses_permission_profile() {
let cli = MultitoolCli::try_parse_from([
"codex",
"sandbox",
"--permission-profile",
":workspace",
"--",
"echo",
])
.expect("parse");
let Some(Subcommand::Sandbox(command)) = cli.subcommand else {
panic!("expected sandbox command");
};
assert_eq!(command.permissions_profile.as_deref(), Some(":workspace"));
assert_eq!(command.command, vec!["echo"]);
}
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
#[test]
fn sandbox_parses_legacy_permissions_profile_alias() {
let cli = MultitoolCli::try_parse_from([
"codex",
"sandbox",
"--permissions-profile",
":workspace",
"--",
"echo",
])
.expect("parse");
let Some(Subcommand::Sandbox(command)) = cli.subcommand else {
panic!("expected sandbox command");
};
assert_eq!(command.permissions_profile.as_deref(), Some(":workspace"));
assert_eq!(command.command, vec!["echo"]);
}
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
#[test]
fn sandbox_help_only_shows_singular_permission_profile() {
let help = help_from_args(&["codex", "sandbox", "--help"]);
assert!(help.contains("--permission-profile"), "{help}");
assert!(!help.contains("--permissions-profile"), "{help}");
}
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
#[test]
fn sandbox_parses_permissions_profile_short_alias() {
let cli =
MultitoolCli::try_parse_from(["codex", "sandbox", "-P", ":workspace", "--", "echo"])
.expect("parse");
let Some(Subcommand::Sandbox(command)) = cli.subcommand else {
panic!("expected sandbox command");
};
assert_eq!(command.permissions_profile.as_deref(), Some(":workspace"));
assert_eq!(command.command, vec!["echo"]);
}
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
#[test]
fn sandbox_parses_config_profile() {
let cli =
MultitoolCli::try_parse_from(["codex", "sandbox", "--profile", "work", "--", "echo"])
.expect("parse");
let Some(Subcommand::Sandbox(command)) = cli.subcommand else {
panic!("expected sandbox command");
};
assert_eq!(command.config_profile.as_deref(), Some("work"));
assert_eq!(command.command, vec!["echo"]);
}
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
#[test]
fn sandbox_rejects_explicit_profile_controls_without_profile() {
let err = MultitoolCli::try_parse_from(["codex", "sandbox", "-C", "/tmp"])
.expect_err("parse should fail");
assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
}
#[test]
fn plugin_marketplace_remove_parses_under_plugin() {
let cli =
MultitoolCli::try_parse_from(["codex", "plugin", "marketplace", "remove", "debug"])
.expect("parse");
assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_))));
}
#[test]
fn marketplace_no_longer_parses_at_top_level() {
let add_result =
MultitoolCli::try_parse_from(["codex", "marketplace", "add", "owner/repo"]);
assert!(add_result.is_err());
let upgrade_result =
MultitoolCli::try_parse_from(["codex", "marketplace", "upgrade", "debug"]);
assert!(upgrade_result.is_err());
let remove_result =
MultitoolCli::try_parse_from(["codex", "marketplace", "remove", "debug"]);
assert!(remove_result.is_err());
}
fn sample_exit_info(conversation_id: Option<&str>, thread_name: Option<&str>) -> AppExitInfo {
let token_usage = TokenUsage {
output_tokens: 2,
total_tokens: 2,
..Default::default()
};
let thread_id = conversation_id
.map(ThreadId::from_string)
.map(Result::unwrap);
AppExitInfo {
token_usage,
thread_id,
resume_hint: thread_id.map(|thread_id| codex_tui::ResumableThread {
thread_id,
thread_name: thread_name.map(str::to_string),
}),
disconnect_info: None,
update_action: None,
exit_reason: ExitReason::UserRequested,
}
}
#[test]
fn format_exit_messages_skips_zero_usage() {
let exit_info = AppExitInfo {
token_usage: TokenUsage::default(),
thread_id: None,
resume_hint: None,
disconnect_info: None,
update_action: None,
exit_reason: ExitReason::UserRequested,
};
let lines = exit_info.format_exit_messages(/*color_enabled*/ false);
assert!(lines.is_empty());
}
#[tokio::test]
async fn format_exit_messages_preserves_auth_env_through_tui_runner() {
let exit_info = run_tui_with_recovery(
|| async {
let mut exit_info = sample_exit_info(
Some("123e4567-e89b-12d3-a456-426614174000"),
/*thread_name*/ None,
);
exit_info.disconnect_info = Some(codex_tui::DisconnectInfo {
command: vec![
"codex".to_string(),
"--remote".to_string(),
"wss://example.com:443/".to_string(),
],
stop_hint: "press ctrl + x".to_string(),
});
Ok(exit_info)
},
Some("CODEX_REMOTE_TOKEN"),
)
.await
.unwrap();
assert_eq!(
exit_info.format_exit_messages(/*color_enabled*/ false),
vec![
"Disconnected from this task. Any running work continues.",
"Reconnect: codex --remote wss://example.com:443/ --remote-auth-token-env CODEX_REMOTE_TOKEN resume 123e4567-e89b-12d3-a456-426614174000",
"Stop the current turn: run codex --remote wss://example.com:443/ --remote-auth-token-env CODEX_REMOTE_TOKEN agents, select this task, and press ctrl + x.",
"Token usage so far: total=2 input=0 output=2",
]
);
}
#[test]
fn format_exit_messages_includes_session_id_without_resume_hint() {
let mut exit_info = sample_exit_info(
Some("123e4567-e89b-12d3-a456-426614174000"),
/*thread_name*/ None,
);
exit_info.token_usage = TokenUsage::default();
exit_info.resume_hint = None;
let lines = exit_info.format_exit_messages(/*color_enabled*/ false);
insta::assert_snapshot!(lines.join("\n"), @"Session ID: 123e4567-e89b-12d3-a456-426614174000");
}
#[test]
fn format_exit_messages_confirms_archive() {
let mut exit_info = sample_exit_info(
Some("123e4567-e89b-12d3-a456-426614174000"),
/*thread_name*/ None,
);
exit_info.exit_reason = ExitReason::Archived(exit_info.thread_id.unwrap());
let lines = exit_info.format_exit_messages(/*color_enabled*/ false);
insta::assert_snapshot!(lines.join("\n"), @"
Token usage: total=2 input=0 output=2
Session archived: 123e4567-e89b-12d3-a456-426614174000
");
}
#[test]
fn format_exit_messages_includes_session_id_for_fatal_exit_without_resume_hint() {
let exit_info = AppExitInfo {
token_usage: TokenUsage::default(),
thread_id: Some(ThreadId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap()),
resume_hint: None,
disconnect_info: None,
update_action: None,
exit_reason: ExitReason::Fatal("boom".to_string()),
};
let lines = exit_info.format_exit_messages(/*color_enabled*/ false);
assert_eq!(
lines,
vec!["Session ID: 123e4567-e89b-12d3-a456-426614174000".to_string()]
);
}
#[test]
fn format_exit_messages_includes_resume_hint_for_fatal_exit() {
let mut exit_info = sample_exit_info(
Some("123e4567-e89b-12d3-a456-426614174000"),
/*thread_name*/ None,
);
exit_info.exit_reason = ExitReason::Fatal("boom".to_string());
let lines = exit_info.format_exit_messages(/*color_enabled*/ false);
assert_eq!(
lines,
vec![
"Token usage: total=2 input=0 output=2".to_string(),
"To continue this session, run:".to_string(),
" codex resume 123e4567-e89b-12d3-a456-426614174000".to_string(),
]
);
}
#[test]
fn format_exit_messages_includes_resume_hint_without_color() {
insta::allow_duplicates! {
for thread_name in [None, Some("")] {
let exit_info =
sample_exit_info(Some("123e4567-e89b-12d3-a456-426614174000"), thread_name);
let lines = exit_info.format_exit_messages(/*color_enabled*/ false);
insta::assert_snapshot!(lines.join("\n"), @"
Token usage: total=2 input=0 output=2
To continue this session, run:
codex resume 123e4567-e89b-12d3-a456-426614174000
");
}
}
}
#[test]
fn format_exit_messages_applies_color_when_enabled() {
let exit_info = sample_exit_info(
Some("123e4567-e89b-12d3-a456-426614174000"),
/*thread_name*/ None,
);
let lines = exit_info.format_exit_messages(/*color_enabled*/ true);
assert_eq!(
lines,
vec![
"Token usage: total=2 input=0 output=2",
"To continue this session, run:",
" \u{1b}[36mcodex resume 123e4567-e89b-12d3-a456-426614174000\u{1b}[39m",
]
);
}
#[test]
fn format_exit_messages_names_picker_item_when_thread_has_name() {
let exit_info = sample_exit_info(
Some("123e4567-e89b-12d3-a456-426614174000"),
Some("my-thread"),
);
let lines = exit_info.format_exit_messages(/*color_enabled*/ false);
insta::assert_snapshot!(lines.join("\n"), @"
Token usage: total=2 input=0 output=2
To continue this session, run:
codex resume 123e4567-e89b-12d3-a456-426614174000
Or run codex resume and select my-thread.
");
}
#[test]
fn format_exit_messages_colors_commands_and_thread_name() {
let exit_info = sample_exit_info(
Some("123e4567-e89b-12d3-a456-426614174000"),
Some("my-thread"),
);
let lines = exit_info.format_exit_messages(/*color_enabled*/ true);
assert_eq!(
lines,
vec![
"Token usage: total=2 input=0 output=2",
"To continue this session, run:",
" \u{1b}[36mcodex resume 123e4567-e89b-12d3-a456-426614174000\u{1b}[39m",
"Or run \u{1b}[36mcodex resume\u{1b}[39m and select \u{1b}[36mmy-thread\u{1b}[39m.",
]
);
}
#[test]
fn resume_model_flag_applies_when_no_root_flags() {
let interactive =
finalize_resume_from_args(["codex", "resume", "-m", "gpt-5.1-test"].as_ref());
assert_eq!(interactive.model.as_deref(), Some("gpt-5.1-test"));
assert!(interactive.resume_picker);
assert!(!interactive.resume_last);
assert_eq!(interactive.resume_session_id, None);
}
#[test]
fn resume_and_fork_preserve_no_alt_screen() {
for (command, finalize) in [
("resume", finalize_resume_from_args as fn(&[&str]) -> TuiCli),
("fork", finalize_fork_from_args as fn(&[&str]) -> TuiCli),
] {
assert!(finalize(&["codex", command, "--no-alt-screen"]).no_alt_screen);
assert!(finalize(&["codex", "--no-alt-screen", command]).no_alt_screen);
assert!(!finalize(&["codex", command]).no_alt_screen);
}
}
#[test]
fn resume_picker_logic_none_and_not_last() {
let interactive = finalize_resume_from_args(["codex", "resume"].as_ref());
assert!(interactive.resume_picker);
assert!(!interactive.resume_last);
assert_eq!(interactive.resume_session_id, None);
assert!(!interactive.resume_show_all);
}
#[test]
fn resume_picker_logic_last() {
let interactive = finalize_resume_from_args(["codex", "resume", "--last"].as_ref());
assert!(!interactive.resume_picker);
assert!(interactive.resume_last);
assert_eq!(interactive.resume_session_id, None);
assert!(!interactive.resume_show_all);
}
#[test]
fn resume_last_accepts_prompt_positional() {
let interactive = finalize_resume_from_args(
["codex", "resume", "--last", "/compact focus on auth"].as_ref(),
);
assert!(!interactive.resume_picker);
assert!(interactive.resume_last);
assert_eq!(interactive.resume_session_id, None);
assert_eq!(
interactive.prompt.as_deref(),
Some("/compact focus on auth")
);
}
#[test]
fn resume_last_rejects_explicit_session_and_prompt() {
let err =
MultitoolCli::try_parse_from(["codex", "resume", "--last", "1234", "continue here"])
.expect_err("--last with an explicit session and prompt should be rejected");
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
#[test]
fn resume_picker_logic_with_session_id() {
let interactive = finalize_resume_from_args(["codex", "resume", "1234"].as_ref());
assert!(!interactive.resume_picker);
assert!(!interactive.resume_last);
assert_eq!(interactive.resume_session_id.as_deref(), Some("1234"));
assert!(!interactive.resume_show_all);
}
#[test]
fn resume_with_session_id_accepts_prompt_positional() {
let interactive =
finalize_resume_from_args(["codex", "resume", "1234", "continue here"].as_ref());
assert!(!interactive.resume_picker);
assert!(!interactive.resume_last);
assert_eq!(interactive.resume_session_id.as_deref(), Some("1234"));
assert_eq!(interactive.prompt.as_deref(), Some("continue here"));
}
#[test]
fn resume_all_flag_sets_show_all() {
let interactive = finalize_resume_from_args(["codex", "resume", "--all"].as_ref());
assert!(interactive.resume_picker);
assert!(interactive.resume_show_all);
}
#[test]
fn resume_include_non_interactive_flag_sets_source_filter_override() {
let interactive =
finalize_resume_from_args(["codex", "resume", "--include-non-interactive"].as_ref());
assert!(interactive.resume_picker);
assert!(interactive.resume_include_non_interactive);
}
#[test]
fn resume_merges_option_flags() {
let interactive = finalize_resume_from_args(
[
"codex",
"resume",
"sid",
"--oss",
"--search",
"--sandbox",
"workspace-write",
"--ask-for-approval",
"on-request",
"-m",
"gpt-5.1-test",
"-p",
"my-config",
"-C",
"/tmp",
"--strict-config",
"-i",
"/tmp/a.png,/tmp/b.png",
]
.as_ref(),
);
assert_eq!(interactive.model.as_deref(), Some("gpt-5.1-test"));
assert!(interactive.oss);
assert_eq!(interactive.config_profile_v2.as_deref(), Some("my-config"));
assert_matches!(
interactive.sandbox_mode,
Some(codex_utils_cli::SandboxModeCliArg::WorkspaceWrite)
);
assert_matches!(
interactive.approval_policy,
Some(codex_utils_cli::ApprovalModeCliArg::OnRequest)
);
assert_eq!(
interactive.cwd.as_deref(),
Some(std::path::Path::new("/tmp"))
);
assert!(interactive.web_search);
assert!(interactive.strict_config);
let has_a = interactive
.images
.iter()
.any(|p| p == std::path::Path::new("/tmp/a.png"));
let has_b = interactive
.images
.iter()
.any(|p| p == std::path::Path::new("/tmp/b.png"));
assert!(has_a && has_b);
assert!(!interactive.resume_picker);
assert!(!interactive.resume_last);
assert_eq!(interactive.resume_session_id.as_deref(), Some("sid"));
}
#[test]
fn resume_merges_dangerously_bypass_flag() {
let interactive = finalize_resume_from_args(
[
"codex",
"resume",
"--dangerously-bypass-approvals-and-sandbox",
]
.as_ref(),
);
assert!(interactive.dangerously_bypass_approvals_and_sandbox);
assert!(interactive.resume_picker);
assert!(!interactive.resume_last);
assert_eq!(interactive.resume_session_id, None);
}
#[test]
fn resume_merges_bypass_hook_trust_flag() {
let interactive = finalize_resume_from_args(
["codex", "resume", "--dangerously-bypass-hook-trust"].as_ref(),
);
assert!(interactive.bypass_hook_trust);
assert!(interactive.resume_picker);
assert!(!interactive.resume_last);
assert_eq!(interactive.resume_session_id, None);
}
#[test]
fn fork_picker_logic_none_and_not_last() {
let interactive = finalize_fork_from_args(["codex", "fork"].as_ref());
assert!(interactive.fork_picker);
assert!(!interactive.fork_last);
assert_eq!(interactive.fork_session_id, None);
assert!(!interactive.fork_show_all);
}
#[test]
fn fork_picker_logic_last() {
let interactive = finalize_fork_from_args(["codex", "fork", "--last"].as_ref());
assert!(!interactive.fork_picker);
assert!(interactive.fork_last);
assert_eq!(interactive.fork_session_id, None);
assert!(!interactive.fork_show_all);
}
#[test]
fn fork_last_accepts_prompt_positional() {
let interactive =
finalize_fork_from_args(["codex", "fork", "--last", "/compact focus on auth"].as_ref());
assert!(!interactive.fork_picker);
assert!(interactive.fork_last);
assert_eq!(interactive.fork_session_id, None);
assert_eq!(
interactive.prompt.as_deref(),
Some("/compact focus on auth")
);
}
#[test]
fn fork_last_rejects_explicit_session_and_prompt() {
let err =
MultitoolCli::try_parse_from(["codex", "fork", "--last", "1234", "continue here"])
.expect_err("--last with an explicit session and prompt should be rejected");
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
#[test]
fn fork_picker_logic_with_session_id() {
let interactive = finalize_fork_from_args(["codex", "fork", "1234"].as_ref());
assert!(!interactive.fork_picker);
assert!(!interactive.fork_last);
assert_eq!(interactive.fork_session_id.as_deref(), Some("1234"));
assert!(!interactive.fork_show_all);
}
#[test]
fn fork_with_session_id_accepts_prompt_positional() {
let interactive =
finalize_fork_from_args(["codex", "fork", "1234", "continue here"].as_ref());
assert!(!interactive.fork_picker);
assert!(!interactive.fork_last);
assert_eq!(interactive.fork_session_id.as_deref(), Some("1234"));
assert_eq!(interactive.prompt.as_deref(), Some("continue here"));
}
#[test]
fn fork_all_flag_sets_show_all() {
let interactive = finalize_fork_from_args(["codex", "fork", "--all"].as_ref());
assert!(interactive.fork_picker);
assert!(interactive.fork_show_all);
}
#[test]
fn app_server_analytics_default_disabled_without_flag() {
let app_server = app_server_from_args(["codex", "app-server"].as_ref());
assert!(!app_server.analytics_default_enabled);
assert!(!app_server.remote_control);
assert_eq!(
app_server.listen,
codex_app_server::AppServerTransport::Stdio
);
}
#[test]
fn app_server_remote_control_startup_flag_enables_remote_control() {
let enabled = app_server_from_args(["codex", "app-server", "--remote-control"].as_ref());
assert!(enabled.remote_control);
}
#[test]
fn app_server_analytics_default_enabled_with_flag() {
let app_server =
app_server_from_args(["codex", "app-server", "--analytics-default-enabled"].as_ref());
assert!(app_server.analytics_default_enabled);
}
#[test]
fn strict_config_parses_for_supported_commands() {
let cli = MultitoolCli::try_parse_from(["codex", "--strict-config"]).expect("parse");
assert!(cli.interactive.strict_config);
let cli =
MultitoolCli::try_parse_from(["codex", "review", "--strict-config", "--uncommitted"])
.expect("parse");
assert_matches!(
cli.subcommand,
Some(Subcommand::Review(ReviewCommand {
strict_config: true,
..
}))
);
let cli = MultitoolCli::try_parse_from(["codex", "exec-server", "--strict-config"])
.expect("parse");
assert_matches!(
cli.subcommand,
Some(Subcommand::ExecServer(ExecServerCommand {
strict_config: true,
..
}))
);
}
#[test]
fn exec_server_forward_parses_shared_remote_options() {
let cli = MultitoolCli::try_parse_from([
"codex",
"exec-server",
"forward",
"--connect",
"ws://127.0.0.1:8765",
"--remote",
"https://example.openai.com",
"--environment-id",
"env-1",
"--name",
"forwarded",
"--strict-config",
"--use-agent-identity-auth",
])
.expect("parse forward");
assert!(cli.remote.remote.is_none());
assert!(!cli.interactive.strict_config);
assert_matches!(cli.subcommand, Some(Subcommand::ExecServer(ExecServerCommand {
command: Some(ExecServerSubcommand::Forward { connect }),
remote: Some(remote),
environment_id: Some(environment_id),
name: Some(name),
strict_config: true,
use_agent_identity_auth: true,
..
})) if connect == "ws://127.0.0.1:8765" && remote == "https://example.openai.com" && environment_id == "env-1" && name == "forwarded");
}
#[test]
fn exec_server_forward_requires_registration_and_destination() {
for args in [
vec!["forward", "--connect", "ws://127.0.0.1:8765"],
vec![
"forward",
"--remote",
"https://example.openai.com",
"--environment-id",
"env-1",
],
vec![
"forward",
"--connect",
"ws://127.0.0.1:8765",
"--remote",
"https://example.openai.com",
],
] {
assert!(
MultitoolCli::try_parse_from(["codex", "exec-server"].into_iter().chain(args))
.is_err()
);
}
}
#[test]
fn root_strict_config_is_supported_for_exec_server() {
let cli = MultitoolCli::try_parse_from(["codex", "--strict-config", "exec-server"])
.expect("parse");
reject_root_strict_config_for_subcommand(cli.interactive.strict_config, &cli.subcommand)
.expect("exec-server should support root --strict-config");
}
#[test]
fn root_strict_config_is_rejected_for_unsupported_subcommands() {
let cli = MultitoolCli::try_parse_from(["codex", "--strict-config", "mcp", "list"])
.expect("parse");
let err = reject_root_strict_config_for_subcommand(
cli.interactive.strict_config,
&cli.subcommand,
)
.expect_err("mcp should not support root --strict-config");
assert_eq!(
err.to_string(),
"`--strict-config` is not supported for `codex mcp`"
);
let cli = MultitoolCli::try_parse_from(["codex", "--strict-config", "remote-control"])
.expect("parse");
let err = reject_root_strict_config_for_subcommand(
cli.interactive.strict_config,
&cli.subcommand,
)
.expect_err("remote-control should not support root --strict-config");
assert_eq!(
err.to_string(),
"`--strict-config` is not supported for `codex remote-control`"
);
}
#[test]
fn app_server_subcommands_reject_strict_config() {
let app_server =
app_server_from_args(["codex", "app-server", "--strict-config", "proxy"].as_ref());
let err = reject_strict_config_for_app_server_subcommand(
app_server.strict_config,
app_server.subcommand.as_ref(),
)
.expect_err("app-server proxy should not support --strict-config");
assert_eq!(
err.to_string(),
"`--strict-config` is not supported for `codex app-server proxy`"
);
}
#[test]
fn reject_remote_flag_for_remote_control() {
let cli = MultitoolCli::try_parse_from(["codex", "--remote", "unix://", "remote-control"])
.expect("parse");
let Some(Subcommand::RemoteControl(remote_control)) = &cli.subcommand else {
panic!("expected remote-control subcommand");
};
assert_eq!(remote_control.subcommand_name(), "remote-control");
let err = reject_remote_mode_for_subcommand(
cli.remote.remote.as_deref(),
cli.remote.remote_auth_token_env.as_deref(),
"remote-control",
)
.expect_err("remote-control should reject root --remote");
assert!(err.to_string().contains("remote-control"));
}
#[test]
fn remote_control_pair_parses() {
let cli = MultitoolCli::try_parse_from(["codex", "remote-control", "pair"]).expect("parse");
let Some(Subcommand::RemoteControl(remote_control)) = &cli.subcommand else {
panic!("expected remote-control subcommand");
};
assert_eq!(remote_control.subcommand_name(), "remote-control pair");
}
#[test]
fn remote_flag_parses_for_interactive_root() {
let cli = MultitoolCli::try_parse_from(["codex", "--remote", "unix://codex.sock"])
.expect("parse");
assert_eq!(cli.remote.remote.as_deref(), Some("unix://codex.sock"));
}
#[test]
fn remote_auth_token_env_flag_parses_for_interactive_root() {
let cli = MultitoolCli::try_parse_from([
"codex",
"--remote-auth-token-env",
"CODEX_REMOTE_AUTH_TOKEN",
"--remote",
"ws://127.0.0.1:4500",
])
.expect("parse");
assert_eq!(
cli.remote.remote_auth_token_env.as_deref(),
Some("CODEX_REMOTE_AUTH_TOKEN")
);
}
#[test]
fn remote_flag_parses_for_resume_subcommand() {
let cli =
MultitoolCli::try_parse_from(["codex", "resume", "--remote", "unix://codex.sock"])
.expect("parse");
let Subcommand::Resume(ResumeCommand { remote, .. }) =
cli.subcommand.expect("resume present")
else {
panic!("expected resume subcommand");
};
assert_eq!(remote.remote.as_deref(), Some("unix://codex.sock"));
}
#[test]
fn agents_subcommand_accepts_remote_session_options() {
let cli = MultitoolCli::try_parse_from([
"codex",
"agents",
"--remote",
"ws://127.0.0.1:4500",
"--remote-auth-token-env",
"CODEX_REMOTE_AUTH_TOKEN",
"--cd",
"/workspace",
"--no-alt-screen",
])
.expect("parse");
let Some(Subcommand::Agents(options)) = cli.subcommand else {
panic!("expected agents subcommand");
};
assert_eq!(
options.remote.remote.as_deref(),
Some("ws://127.0.0.1:4500")
);
assert_eq!(
options.remote.remote_auth_token_env.as_deref(),
Some("CODEX_REMOTE_AUTH_TOKEN")
);
assert_eq!(
options.cwd.as_deref(),
Some(std::path::Path::new("/workspace"))
);
assert!(options.no_alt_screen);
}
#[test]
fn reject_remote_mode_for_non_interactive_subcommands() {
let err = reject_remote_mode_for_subcommand(
Some("127.0.0.1:4500"),
/*remote_auth_token_env*/ None,
"exec",
)
.expect_err("non-interactive subcommands should reject --remote");
assert!(
err.to_string()
.contains("only supported for interactive TUI commands")
);
}
#[test]
fn reject_remote_auth_token_env_for_non_interactive_subcommands() {
let err = reject_remote_mode_for_subcommand(
/*remote*/ None,
Some("CODEX_REMOTE_AUTH_TOKEN"),
"exec",
)
.expect_err("non-interactive subcommands should reject --remote-auth-token-env");
assert!(
err.to_string()
.contains("only supported for interactive TUI commands")
);
}
#[test]
fn reject_remote_auth_token_env_for_app_server_generate_internal_json_schema() {
let subcommand =
AppServerSubcommand::GenerateInternalJsonSchema(GenerateInternalJsonSchemaCommand {
out_dir: PathBuf::from("/tmp/out"),
});
let err = reject_remote_mode_for_app_server_subcommand(
/*remote*/ None,
Some("CODEX_REMOTE_AUTH_TOKEN"),
Some(&subcommand),
)
.expect_err("non-interactive app-server subcommands should reject --remote-auth-token-env");
assert!(err.to_string().contains("generate-internal-json-schema"));
}
#[test]
fn read_remote_auth_token_from_env_var_reports_missing_values() {
let err = read_remote_auth_token_from_env_var_with("CODEX_REMOTE_AUTH_TOKEN", |_| {
Err(std::env::VarError::NotPresent)
})
.expect_err("missing env vars should be rejected");
assert!(err.to_string().contains("is not set"));
}
#[test]
fn read_remote_auth_token_from_env_var_trims_values() {
let auth_token =
read_remote_auth_token_from_env_var_with("CODEX_REMOTE_AUTH_TOKEN", |_| {
Ok(" bearer-token ".to_string())
})
.expect("env var should parse");
assert_eq!(auth_token, "bearer-token");
}
#[test]
fn read_remote_auth_token_from_env_var_rejects_empty_values() {
let err = read_remote_auth_token_from_env_var_with("CODEX_REMOTE_AUTH_TOKEN", |_| {
Ok(" \n\t ".to_string())
})
.expect_err("empty env vars should be rejected");
assert!(err.to_string().contains("is empty"));
}
#[test]
fn app_server_grpc_code_mode_host_url_parses_independently_of_listen_transport() {
let app_server = app_server_from_args(
[
"codex",
"app-server",
"--code-mode-host",
"https://example.test",
"--listen",
"ws://127.0.0.1:4500",
]
.as_ref(),
);
assert_eq!(
app_server.code_mode_host.code_mode_host,
Some(url::Url::parse("https://example.test").expect("test endpoint should parse"))
);
}
#[test]
fn app_server_rejects_invalid_code_mode_host_urls() {
for endpoint in [
"ftp://127.0.0.1:8765",
"ws://",
"ws://127.0.0.1:8765",
"wss://example.test/code-mode",
"ws://alice:secret@example.test/code-mode",
"wss://alice:secret@example.test/code-mode",
"wss://example.test/code-mode#fragment",
"http://",
"https://example.test/#fragment",
"https://example.test/code-mode",
"http://alice:secret@example.test",
"https://alice:secret@example.test",
"http://example.test/?token=secret",
] {
let error =
MultitoolCli::try_parse_from(["codex", "app-server", "--code-mode-host", endpoint])
.expect_err("invalid code-mode host endpoint should fail argument parsing");
assert_eq!(error.kind(), clap::error::ErrorKind::ValueValidation);
let rendered_error = error.to_string();
assert!(!rendered_error.contains("alice"));
assert!(!rendered_error.contains("secret"));
}
}
#[test]
fn app_server_listen_websocket_url_parses() {
let app_server = app_server_from_args(
["codex", "app-server", "--listen", "ws://127.0.0.1:4500"].as_ref(),
);
assert_eq!(
app_server.listen,
codex_app_server::AppServerTransport::WebSocket {
bind_address: "127.0.0.1:4500".parse().expect("valid socket address"),
}
);
}
#[test]
fn app_server_listen_stdio_url_parses() {
let app_server =
app_server_from_args(["codex", "app-server", "--listen", "stdio://"].as_ref());
assert_eq!(
app_server.listen,
codex_app_server::AppServerTransport::Stdio
);
}
#[test]
fn app_server_stdio_flag_parses() {
let app_server = app_server_from_args(["codex", "app-server", "--stdio"].as_ref());
assert!(app_server.stdio);
}
#[test]
fn app_server_stdio_flag_conflicts_with_listen() {
let err = MultitoolCli::try_parse_from([
"codex",
"app-server",
"--stdio",
"--listen",
"stdio://",
])
.expect_err("--stdio and --listen should be rejected together");
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
#[test]
fn app_server_listen_unix_socket_url_parses() {
let app_server =
app_server_from_args(["codex", "app-server", "--listen", "unix://"].as_ref());
assert_eq!(
app_server.listen,
codex_app_server::AppServerTransport::UnixSocket {
socket_path: default_app_server_socket_path()
}
);
}
#[test]
fn app_server_listen_unix_socket_path_parses() {
let app_server = app_server_from_args(
["codex", "app-server", "--listen", "unix:///tmp/codex.sock"].as_ref(),
);
assert_eq!(
app_server.listen,
codex_app_server::AppServerTransport::UnixSocket {
socket_path: AbsolutePathBuf::from_absolute_path("/tmp/codex.sock")
.expect("absolute path should parse")
}
);
}
#[test]
fn app_server_listen_off_parses() {
let app_server = app_server_from_args(["codex", "app-server", "--listen", "off"].as_ref());
assert_eq!(app_server.listen, codex_app_server::AppServerTransport::Off);
}
#[test]
fn app_server_listen_invalid_url_fails_to_parse() {
let parse_result =
MultitoolCli::try_parse_from(["codex", "app-server", "--listen", "http://foo"]);
assert!(parse_result.is_err());
}
#[test]
fn app_server_proxy_subcommand_parses() {
let app_server = app_server_from_args(["codex", "app-server", "proxy"].as_ref());
assert!(matches!(
app_server.subcommand,
Some(AppServerSubcommand::Proxy(AppServerProxyCommand {
socket_path: None
}))
));
}
#[test]
fn app_server_daemon_subcommands_parse() {
assert!(matches!(
app_server_from_args(
[
"codex",
"app-server",
"daemon",
"bootstrap",
"--remote-control"
]
.as_ref()
)
.subcommand,
Some(AppServerSubcommand::Daemon(AppServerDaemonCommand {
subcommand: AppServerDaemonSubcommand::Bootstrap(AppServerBootstrapCommand {
remote_control: true
})
}))
));
assert!(matches!(
app_server_from_args(["codex", "app-server", "daemon", "start"].as_ref()).subcommand,
Some(AppServerSubcommand::Daemon(AppServerDaemonCommand {
subcommand: AppServerDaemonSubcommand::Start
}))
));
assert!(matches!(
app_server_from_args(["codex", "app-server", "daemon", "restart"].as_ref()).subcommand,
Some(AppServerSubcommand::Daemon(AppServerDaemonCommand {
subcommand: AppServerDaemonSubcommand::Restart
}))
));
assert!(matches!(
app_server_from_args(
["codex", "app-server", "daemon", "enable-remote-control"].as_ref()
)
.subcommand,
Some(AppServerSubcommand::Daemon(AppServerDaemonCommand {
subcommand: AppServerDaemonSubcommand::EnableRemoteControl
}))
));
assert!(matches!(
app_server_from_args(
["codex", "app-server", "daemon", "disable-remote-control"].as_ref()
)
.subcommand,
Some(AppServerSubcommand::Daemon(AppServerDaemonCommand {
subcommand: AppServerDaemonSubcommand::DisableRemoteControl
}))
));
assert!(matches!(
app_server_from_args(["codex", "app-server", "daemon", "stop"].as_ref()).subcommand,
Some(AppServerSubcommand::Daemon(AppServerDaemonCommand {
subcommand: AppServerDaemonSubcommand::Stop
}))
));
assert!(matches!(
app_server_from_args(["codex", "app-server", "daemon", "version"].as_ref()).subcommand,
Some(AppServerSubcommand::Daemon(AppServerDaemonCommand {
subcommand: AppServerDaemonSubcommand::Version
}))
));
}
#[test]
fn app_server_proxy_sock_path_parses() {
let app_server =
app_server_from_args(["codex", "app-server", "proxy", "--sock", "codex.sock"].as_ref());
let Some(AppServerSubcommand::Proxy(proxy)) = app_server.subcommand else {
panic!("expected proxy subcommand");
};
assert_eq!(
proxy.socket_path,
Some(
AbsolutePathBuf::relative_to_current_dir("codex.sock")
.expect("relative path should resolve")
)
);
}
#[test]
fn reject_remote_auth_token_env_for_app_server_proxy() {
let subcommand = AppServerSubcommand::Proxy(AppServerProxyCommand { socket_path: None });
let err = reject_remote_mode_for_app_server_subcommand(
/*remote*/ None,
Some("CODEX_REMOTE_AUTH_TOKEN"),
Some(&subcommand),
)
.expect_err("app-server proxy should reject --remote-auth-token-env");
assert!(err.to_string().contains("app-server proxy"));
}
#[test]
fn reject_remote_auth_token_env_for_app_server_version() {
let subcommand = AppServerSubcommand::Daemon(AppServerDaemonCommand {
subcommand: AppServerDaemonSubcommand::Version,
});
let err = reject_remote_mode_for_app_server_subcommand(
/*remote*/ None,
Some("CODEX_REMOTE_AUTH_TOKEN"),
Some(&subcommand),
)
.expect_err("app-server daemon version should reject --remote-auth-token-env");
assert!(err.to_string().contains("app-server daemon version"));
}
#[test]
fn app_server_capability_token_flags_parse() {
let app_server = app_server_from_args(
[
"codex",
"app-server",
"--ws-auth",
"capability-token",
"--ws-token-file",
"/tmp/codex-token",
]
.as_ref(),
);
assert_eq!(
app_server.auth.ws_auth,
Some(codex_app_server::WebsocketAuthCliMode::CapabilityToken)
);
assert_eq!(
app_server.auth.ws_token_file,
Some(PathBuf::from("/tmp/codex-token"))
);
}
#[test]
fn app_server_signed_bearer_flags_parse() {
let app_server = app_server_from_args(
[
"codex",
"app-server",
"--ws-auth",
"signed-bearer-token",
"--ws-shared-secret-file",
"/tmp/codex-secret",
"--ws-issuer",
"issuer",
"--ws-audience",
"audience",
"--ws-max-clock-skew-seconds",
"9",
]
.as_ref(),
);
assert_eq!(
app_server.auth.ws_auth,
Some(codex_app_server::WebsocketAuthCliMode::SignedBearerToken)
);
assert_eq!(
app_server.auth.ws_shared_secret_file,
Some(PathBuf::from("/tmp/codex-secret"))
);
assert_eq!(app_server.auth.ws_issuer.as_deref(), Some("issuer"));
assert_eq!(app_server.auth.ws_audience.as_deref(), Some("audience"));
assert_eq!(app_server.auth.ws_max_clock_skew_seconds, Some(9));
}
#[test]
fn app_server_rejects_removed_insecure_non_loopback_flag() {
let parse_result = MultitoolCli::try_parse_from([
"codex",
"app-server",
"--allow-unauthenticated-non-loopback-ws",
]);
assert!(parse_result.is_err());
}
#[test]
fn features_enable_parses_feature_name() {
let cli = MultitoolCli::try_parse_from(["codex", "features", "enable", "unified_exec"])
.expect("parse should succeed");
let Some(Subcommand::Features(FeaturesCli { sub })) = cli.subcommand else {
panic!("expected features subcommand");
};
let FeaturesSubcommand::Enable(FeatureSetArgs { feature }) = sub else {
panic!("expected features enable");
};
assert_eq!(feature, "unified_exec");
}
#[test]
fn features_disable_parses_feature_name() {
let cli = MultitoolCli::try_parse_from(["codex", "features", "disable", "shell_tool"])
.expect("parse should succeed");
let Some(Subcommand::Features(FeaturesCli { sub })) = cli.subcommand else {
panic!("expected features subcommand");
};
let FeaturesSubcommand::Disable(FeatureSetArgs { feature }) = sub else {
panic!("expected features disable");
};
assert_eq!(feature, "shell_tool");
}
#[test]
fn feature_toggles_known_features_generate_overrides() {
let toggles = FeatureToggles {
enable: vec!["web_search_request".to_string()],
disable: vec!["unified_exec".to_string()],
};
let overrides = toggles.to_overrides().expect("valid features");
assert_eq!(
overrides,
vec![
"features.web_search_request=true".to_string(),
"features.unified_exec=false".to_string(),
]
);
}
#[test]
fn feature_toggles_accept_legacy_linux_sandbox_flag() {
let toggles = FeatureToggles {
enable: vec!["use_linux_sandbox_bwrap".to_string()],
disable: Vec::new(),
};
let overrides = toggles.to_overrides().expect("valid features");
assert_eq!(
overrides,
vec!["features.use_linux_sandbox_bwrap=true".to_string(),]
);
}
#[test]
fn feature_toggles_accept_removed_image_detail_original_flag() {
let toggles = FeatureToggles {
enable: vec!["image_detail_original".to_string()],
disable: Vec::new(),
};
let overrides = toggles.to_overrides().expect("valid features");
assert_eq!(
overrides,
vec!["features.image_detail_original=true".to_string(),]
);
}
#[test]
fn feature_toggles_accept_removed_enable_fanout_flag() {
let toggles = FeatureToggles {
enable: vec!["enable_fanout".to_string()],
disable: Vec::new(),
};
let overrides = toggles.to_overrides().expect("valid features");
assert_eq!(overrides, vec!["features.enable_fanout=true".to_string(),]);
}
#[test]
fn feature_toggles_accept_removed_item_ids_flag() {
let toggles = FeatureToggles {
enable: vec!["item_ids".to_string()],
disable: Vec::new(),
};
let overrides = toggles.to_overrides().expect("valid features");
assert_eq!(overrides, vec!["features.item_ids=true".to_string()]);
}
#[test]
fn feature_toggles_unknown_feature_errors() {
let toggles = FeatureToggles {
enable: vec!["does_not_exist".to_string()],
disable: Vec::new(),
};
let err = toggles
.to_overrides()
.expect_err("feature should be rejected");
assert_eq!(err.to_string(), "Unknown feature flag: does_not_exist");
}
#[test]
fn strict_config_with_unknown_enable_errors() {
let err = strict_config_feature_toggle_error(["--enable", "does_not_exist"].as_ref());
assert_eq!(err.to_string(), "Unknown feature flag: does_not_exist");
}
#[test]
fn strict_config_with_unknown_disable_errors() {
let err = strict_config_feature_toggle_error(["--disable", "does_not_exist"].as_ref());
assert_eq!(err.to_string(), "Unknown feature flag: does_not_exist");
}
#[test]
fn strict_config_with_compound_enable_errors() {
let err = strict_config_feature_toggle_error(
["--enable", "multi_agent_v2.subagent_usage_hint_text"].as_ref(),
);
assert_eq!(
err.to_string(),
"Unknown feature flag: multi_agent_v2.subagent_usage_hint_text"
);
}
fn strict_config_feature_toggle_error(args: &[&str]) -> anyhow::Error {
let cli_args = std::iter::once("codex")
.chain(std::iter::once("--strict-config"))
.chain(args.iter().copied());
let cli = MultitoolCli::try_parse_from(cli_args).expect("parse should succeed");
assert!(cli.interactive.strict_config);
cli.feature_toggles
.to_overrides()
.expect_err("feature should be rejected")
}
}
#[cfg(all(test, unix))]
#[path = "daemon_update_tests.rs"]
mod daemon_update_tests;