Codex Handbook
cli/src/main.rs 4034 lines
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_tasks::Cli as CloudTasksCli;use codex_exec::Cli as ExecCli;use codex_exec::Command as ExecCommand;use codex_exec::ReviewArgs;use codex_execpolicy::ExecPolicyCheckCommand;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_state::memories_db_path;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 owo_colors::OwoColorize;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(any(target_os = "macos", target_os = "windows"))]mod app_cmd;#[cfg(any(target_os = "macos", target_os = "windows"))]mod desktop_app;mod doctor;mod marketplace_cmd;mod mcp_cmd;mod plugin_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::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::ConfigBuilder;use codex_core::config::ConfigOverrides;use codex_core::config::edit::ConfigEditsBuilder;use codex_core::config::find_codex_home;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::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 {    /// 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),    /// Start Codex as an MCP server (stdio).    McpServer(McpServerCommand),    /// [experimental] Run the app server or related tooling.    AppServer(AppServerCommand),    /// [experimental] Manage the app-server daemon with remote control enabled.    RemoteControl(RemoteControlCommand),    /// Launch the Codex 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),    /// Archive a saved session by id or session name.    Archive(SessionArchiveCommand),    /// Permanently delete a saved session by id or session name.    Delete(DeleteCommand),    /// 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 McpServerCommand {    /// 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,}#[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 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>,    /// 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,    /// 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 {    /// 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: `ws://IP:PORT` (default), `stdio`, `stdio://`.    #[arg(long = "listen", value_name = "URL", conflicts_with = "remote")]    listen: Option<String>,    /// Register this exec-server as a remote environment using the given base URL.    #[arg(long = "remote", value_name = "URL", requires = "environment_id")]    remote: Option<String>,    /// Environment id to attach to when registering remotely.    #[arg(long = "environment-id", value_name = "ID")]    environment_id: Option<String>,    /// Human-readable environment name.    #[arg(long = "name", value_name = "NAME")]    name: Option<String>,    /// Use Agent Identity auth from CODEX_ACCESS_TOKEN for remote registration.    #[arg(long = "use-agent-identity-auth", requires = "remote")]    use_agent_identity_auth: bool,}#[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,    /// 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,}#[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}"))}fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec<String> {    let is_fatal = matches!(&exit_info.exit_reason, ExitReason::Fatal(_));    let AppExitInfo {        token_usage,        thread_id: conversation_id,        resume_hint,        ..    } = exit_info;    let mut lines = Vec::new();    if !token_usage.is_zero() {        lines.push(token_usage.to_string());    }    if let Some(resume_cmd) = resume_hint {        let command = if color_enabled {            resume_cmd.cyan().to_string()        } else {            resume_cmd        };        lines.push(format!("To continue this session, run {command}"));    } else if is_fatal && let Some(conversation_id) = conversation_id {        lines.push(format!("Session ID: {conversation_id}"));    }    lines}/// Handle the app exit and print the results. Optionally run the update action.fn handle_app_exit(exit_info: AppExitInfo) -> anyhow::Result<()> {    let is_fatal = match &exit_info.exit_reason {        ExitReason::Fatal(message) => {            eprintln!("ERROR: {message}");            true        }        ExitReason::UserRequested => false,    };    let update_action = exit_info.update_action;    let color_enabled = supports_color::on(Stream::Stdout).is_some();    for line in format_exit_messages(exit_info, 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)?;    }    Ok(())}/// Run the update action and print the result.fn run_update_action(action: UpdateAction) -> anyhow::Result<()> {    println!();    let cmd_str = action.command_str();    println!("Updating Codex via `{cmd_str}`...");    let status = {        #[cfg(windows)]        {            if action == UpdateAction::StandaloneWindows {                let (cmd, args) = action.command_args();                // Run the standalone PowerShell installer with PowerShell                // itself. Routing this through `cmd.exe /C` would parse                // PowerShell metacharacters like `|` before PowerShell sees                // the installer command.                std::process::Command::new(cmd).args(args).status()?            } else {                // On Windows, run via cmd.exe so .CMD/.BAT are correctly resolved (PATHEXT semantics).                std::process::Command::new("cmd")                    .args(["/C", &cmd_str])                    .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(())}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)    }}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<()> {    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();    // 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 root_remote = remote.remote;    let root_remote_auth_token_env = remote.remote_auth_token_env;    let root_strict_config = interactive.strict_config;    reject_root_strict_config_for_subcommand(root_strict_config, &subcommand)?;    if let Some(subcommand) = subcommand.as_ref() {        profile_v2_for_subcommand(&interactive, subcommand)?;    }    match subcommand {        None => {            prepend_config_flags(                &mut interactive.config_overrides,                root_config_overrides.clone(),            );            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)?;        }        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::McpServer(McpServerCommand { strict_config })) => {            reject_remote_mode_for_subcommand(                root_remote.as_deref(),                root_remote_auth_token_env.as_deref(),                "mcp-server",            )?;            codex_mcp_server::run_main(                arg0_paths.clone(),                root_config_overrides,                strict_config || root_strict_config,            )            .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,                strict_config: app_server_strict_config,                listen,                stdio,                remote_control,                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 {                        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()                    };                    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?;                }                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::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 => {                        codex_app_server_daemon::run_pid_update_loop().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)?;        }        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::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::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)?;        }        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)) => {            #[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",                )?;                sandbox_setup::run(setup_cli).await?;                return Ok(());            }            reject_remote_mode_for_subcommand(                root_remote.as_deref(),                root_remote_auth_token_env.as_deref(),                "sandbox",            )?;            let config_profile = sandbox_cli                .config_profile                .as_ref()                .or(interactive.config_profile_v2.as_ref());            let loader_overrides = loader_overrides_for_profile(config_profile)?;            prepend_config_flags(                &mut sandbox_cli.config_overrides,                root_config_overrides.clone(),            );            #[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",                )?;                let mut cli_kv_overrides = root_config_overrides                    .parse_overrides()                    .map_err(anyhow::Error::msg)?;                // Honor `--search` via the canonical web_search mode.                if interactive.web_search {                    cli_kv_overrides.push((                        "web_search".to_string(),                        toml::Value::String("live".to_string()),                    ));                }                let config = ConfigBuilder::default()                    .cli_overrides(cli_kv_overrides)                    .build()                    .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::Exec(_)        | Subcommand::Review(_)        | Subcommand::Resume(_)        | 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 archive`, `codex delete`, `codex unarchive`, `codex fork`, `codex mcp`, `codex sandbox`, and `codex debug prompt-input`."        ),    }}async fn run_exec_server_command(    cmd: ExecServerCommand,    arg0_paths: &Arg0DispatchPaths,    root_config_overrides: &CliConfigOverrides,    strict_config: bool,) -> anyhow::Result<()> {    let codex_self_exe = arg0_paths        .codex_self_exe        .clone()        .ok_or_else(|| anyhow::anyhow!("Codex executable path is not configured"))?;    let runtime_paths = codex_exec_server::ExecServerRuntimePaths::new(        codex_self_exe,        arg0_paths.codex_linux_sandbox_exe.clone(),    )?;    if let Some(base_url) = cmd.remote {        let environment_id = cmd            .environment_id            .ok_or_else(|| anyhow::anyhow!("--environment-id is required when --remote is set"))?;        let config = load_exec_server_config(root_config_overrides, strict_config).await?;        let auth_provider =            load_exec_server_remote_auth_provider(&config, &base_url, cmd.use_agent_identity_auth)                .await?;        let mut remote_config = codex_exec_server::RemoteEnvironmentConfig::new(            base_url,            environment_id,            auth_provider,        )?;        if let Some(name) = cmd.name {            remote_config.name = name;        }        codex_exec_server::run_remote_environment(remote_config, runtime_paths).await?;        Ok(())    } else {        if strict_config {            // Local exec-server startup does not consume Config, but strict            // mode should still reject unknown fields before opening a listener.            let _validated_config =                load_exec_server_config(root_config_overrides, strict_config).await?;        }        let listen_url = cmd            .listen            .as_deref()            .unwrap_or(codex_exec_server::DEFAULT_LISTEN_URL);        codex_exec_server::run_main(listen_url, runtime_paths)            .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 {        let agent_identity_jwt = 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 =            CodexAuth::from_agent_identity_jwt(&agent_identity_jwt, Some(&config.chatgpt_base_url))                .await?;        return Ok(codex_model_provider::auth_provider_from_auth(&auth));    }    let 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)?;    }    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,) -> anyhow::Result<codex_core::config::Config> {    let cli_kv_overrides = root_config_overrides        .parse_overrides()        .map_err(anyhow::Error::msg)?;    Ok(ConfigBuilder::default()        .cli_overrides(cli_kv_overrides)        .strict_config(strict_config)        .build()        .await?)}async fn load_exec_server_remote_auth(    config: &codex_core::config::Config,    missing_auth_error: &'static str,) -> anyhow::Result<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)}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(LoaderOverrides {                user_config_path: Some(resolve_profile_v2_config_path(&codex_home, profile_v2)),                user_config_profile: Some(profile_v2.clone()),                ..Default::default()            })        }        None => Ok(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 prompt_input = codex_core::build_prompt_input(        config,        input,        /*state_db*/ None,        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)            .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 = memories_db_path(config.sqlite_home.as_path());    let cleared_memories_db =        StateRuntime::clear_memory_data_in_sqlite_home(config.sqlite_home.as_path()).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_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::Exec(_))        | Some(Subcommand::Review(_))        | Some(Subcommand::McpServer(_))        | Some(Subcommand::ExecServer(_))        | Some(Subcommand::Resume(_))        | 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"),        #[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"),    }}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::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(())}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.",            ));        }    }    let remote_endpoint = match resolve_remote_endpoint(remote, remote_auth_token_env) {        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(),        )    };    let mut attempted_backups = HashSet::new();    loop {        let err = match start_tui().await {            Ok(exit_info) => 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,        prompt,        config_overrides,        ..    } = subcommand_cli;    interactive        .shared        .apply_subcommand_overrides(shared.into_inner());    if let Some(approval) = approval_policy {        interactive.approval_policy = Some(approval);    }    if web_search {        interactive.web_search = true;    }    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 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 {            interactive,            config_overrides: root_overrides,            subcommand,            feature_toggles: _,            remote: _,        } = cli;        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 {            interactive,            config_overrides: root_overrides,            subcommand,            feature_toggles: _,            remote: _,        } = cli;        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_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_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 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);    }    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 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", true).is_ok());        let err = delete_action("my-thread", 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_permissions_profile() {        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_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());    }    #[test]    fn full_auto_no_longer_parses_at_top_level() {        let result = MultitoolCli::try_parse_from(["codex", "--full-auto"]);        assert!(result.is_err());    }    #[test]    fn exec_full_auto_reports_migration_path() {        let cli = MultitoolCli::try_parse_from(["codex", "exec", "--full-auto", "summarize"])            .expect("exec should accept removed flag long enough to report a migration path");        let Some(Subcommand::Exec(exec)) = cli.subcommand else {            panic!("expected exec subcommand");        };        assert_eq!(            exec.removed_full_auto_warning(),            Some("warning: `--full-auto` is deprecated; use `--sandbox workspace-write` instead.")        );    }    #[test]    fn sandbox_full_auto_no_longer_parses() {        let result = MultitoolCli::try_parse_from(["codex", "sandbox", "--full-auto", "--"]);        assert!(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: codex_utils_cli::resume_hint(thread_name, thread_id),            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,            update_action: None,            exit_reason: ExitReason::UserRequested,        };        let lines = format_exit_messages(exit_info, /*color_enabled*/ false);        assert!(lines.is_empty());    }    #[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,            update_action: None,            exit_reason: ExitReason::Fatal("boom".to_string()),        };        let lines = format_exit_messages(exit_info, /*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 = format_exit_messages(exit_info, /*color_enabled*/ false);        assert_eq!(            lines,            vec![                "Token usage: total=2 input=0 output=2".to_string(),                "To continue this session, run codex resume 123e4567-e89b-12d3-a456-426614174000"                    .to_string(),            ]        );    }    #[test]    fn format_exit_messages_includes_resume_hint_without_color() {        let exit_info = sample_exit_info(            Some("123e4567-e89b-12d3-a456-426614174000"),            /*thread_name*/ None,        );        let lines = format_exit_messages(exit_info, /*color_enabled*/ false);        assert_eq!(            lines,            vec![                "Token usage: total=2 input=0 output=2".to_string(),                "To continue this session, run codex resume 123e4567-e89b-12d3-a456-426614174000"                    .to_string(),            ]        );    }    #[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 = format_exit_messages(exit_info, /*color_enabled*/ true);        assert_eq!(lines.len(), 2);        assert!(lines[1].contains("\u{1b}[36m"));    }    #[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 = format_exit_messages(exit_info, /*color_enabled*/ false);        assert_eq!(            lines,            vec![                "Token usage: total=2 input=0 output=2".to_string(),                "To continue this session, run codex resume, then select my-thread (123e4567-e89b-12d3-a456-426614174000)".to_string(),            ]        );    }    #[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_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", "mcp-server", "--strict-config"])            .expect("parse");        assert_matches!(            cli.subcommand,            Some(Subcommand::McpServer(McpServerCommand {                strict_config: true,            }))        );        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 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_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 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_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_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")    }}