diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 70cd0294a7..dbe6b7605c 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -126,6 +126,9 @@ enum Subcommand { /// [experimental] Run the app server or related tooling. AppServer(AppServerCommand), + /// [experimental] Start a headless app-server with remote control enabled. + RemoteControl, + /// Launch the Codex desktop app (opens the app installer if missing). #[cfg(any(target_os = "macos", target_os = "windows"))] App(app_cmd::AppCommand), @@ -725,6 +728,14 @@ struct FeatureSetArgs { feature: String, } +const REMOTE_CONTROL_FEATURE_OVERRIDE: &str = "features.remote_control=true"; + +fn enable_remote_control_for_invocation(config_overrides: &mut CliConfigOverrides) { + config_overrides + .raw_overrides + .push(REMOTE_CONTROL_FEATURE_OVERRIDE.to_string()); +} + fn stage_str(stage: Stage) -> &'static str { match stage { Stage::UnderDevelopment => "under development", @@ -896,6 +907,24 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { } } } + Some(Subcommand::RemoteControl) => { + reject_remote_mode_for_subcommand( + root_remote.as_deref(), + root_remote_auth_token_env.as_deref(), + "remote-control", + )?; + enable_remote_control_for_invocation(&mut root_config_overrides); + codex_app_server::run_main_with_transport( + arg0_paths.clone(), + root_config_overrides, + codex_config::LoaderOverrides::default(), + /*default_analytics_enabled*/ false, + codex_app_server::AppServerTransport::Off, + codex_protocol::protocol::SessionSource::Cli, + codex_app_server::AppServerWebsocketAuthSettings::default(), + ) + .await?; + } #[cfg(any(target_os = "macos", target_os = "windows"))] Some(Subcommand::App(app_cli)) => { reject_remote_mode_for_subcommand( @@ -2276,6 +2305,45 @@ mod tests { assert!(app_server.analytics_default_enabled); } + #[test] + fn remote_control_override_is_appended_after_root_toggles() { + let mut config_overrides = CliConfigOverrides::default(); + config_overrides + .raw_overrides + .push("features.remote_control=false".to_string()); + + enable_remote_control_for_invocation(&mut config_overrides); + + assert_eq!( + config_overrides.raw_overrides, + vec![ + "features.remote_control=false".to_string(), + REMOTE_CONTROL_FEATURE_OVERRIDE.to_string(), + ] + ); + } + + #[test] + fn reject_remote_flag_for_remote_control() { + let cli = MultitoolCli::try_parse_from([ + "codex", + "--remote", + "ws://127.0.0.1:1234", + "remote-control", + ]) + .expect("parse"); + assert_matches!(cli.subcommand, Some(Subcommand::RemoteControl)); + + 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", "ws://127.0.0.1:4500"])