From 4859d80ffeec76cc59c95fd274157c6b5560b4d2 Mon Sep 17 00:00:00 2001 From: Owen Lin Date: Mon, 11 May 2026 15:38:30 -0700 Subject: [PATCH] Update codex remote-control to start the daemon (#22218) ## Why Update `codex remote-control` to use the new app server daemon commands instead. - if the updater loop is not running, bootstrap the daemon with remote control enabled (`codex app-server daemon bootstrap --remote-control`) - otherwise, enable the persisted remote-control setting and start the daemon normally --- codex-rs/app-server-daemon/README.md | 4 ++ codex-rs/app-server-daemon/src/lib.rs | 86 ++++++++++++++++++++++++++- codex-rs/cli/src/main.rs | 40 +------------ 3 files changed, 92 insertions(+), 38 deletions(-) diff --git a/codex-rs/app-server-daemon/README.md b/codex-rs/app-server-daemon/README.md index 1d81193bb..ac512a803 100644 --- a/codex-rs/app-server-daemon/README.md +++ b/codex-rs/app-server-daemon/README.md @@ -92,6 +92,10 @@ JSON-RPC initialize handshake on the Unix control socket. for future starts. If a managed app-server is already running, they restart it so the new setting takes effect immediately. +Top-level `codex remote-control` bootstraps with `--remote-control` when the +updater loop is not running. Otherwise it enables remote control and starts the +daemon normally. + `stop` sends a graceful termination request first, then sends a second termination signal after the grace window if the process is still alive. diff --git a/codex-rs/app-server-daemon/src/lib.rs b/codex-rs/app-server-daemon/src/lib.rs index b5b16b461..826acbae6 100644 --- a/codex-rs/app-server-daemon/src/lib.rs +++ b/codex-rs/app-server-daemon/src/lib.rs @@ -89,6 +89,13 @@ pub struct BootstrapOutput { pub app_server_version: String, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(untagged)] +pub enum RemoteControlStartOutput { + Bootstrap(BootstrapOutput), + Start(LifecycleOutput), +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RemoteControlMode { Enabled, @@ -165,6 +172,13 @@ pub async fn bootstrap(options: BootstrapOptions) -> Result { Daemon::from_environment()?.bootstrap(options).await } +pub async fn ensure_remote_control_started() -> Result { + ensure_supported_platform()?; + Daemon::from_environment()? + .ensure_remote_control_started() + .await +} + pub async fn set_remote_control(mode: RemoteControlMode) -> Result { ensure_supported_platform()?; Daemon::from_environment()?.set_remote_control(mode).await @@ -397,8 +411,34 @@ impl Daemon { self.bootstrap_locked(options).await } + async fn ensure_remote_control_started(&self) -> Result { + let _operation_lock = self.acquire_operation_lock().await?; + let settings = self.load_settings().await?; + if self.is_bootstrapped(&settings).await? { + let _ = self + .set_remote_control_locked(RemoteControlMode::Enabled) + .await?; + let output = self.start().await?; + return Ok(RemoteControlStartOutput::Start(output)); + } + + let output = self + .bootstrap_locked(BootstrapOptions { + remote_control_enabled: true, + }) + .await?; + Ok(RemoteControlStartOutput::Bootstrap(output)) + } + async fn set_remote_control(&self, mode: RemoteControlMode) -> Result { let _operation_lock = self.acquire_operation_lock().await?; + self.set_remote_control_locked(mode).await + } + + async fn set_remote_control_locked( + &self, + mode: RemoteControlMode, + ) -> Result { let previous_settings = self.load_settings().await?; let mut settings = previous_settings.clone(); let remote_control_enabled = mode.is_enabled(); @@ -517,6 +557,11 @@ impl Daemon { backend.start().await } + async fn is_bootstrapped(&self, settings: &DaemonSettings) -> Result { + let updater = backend::pid_update_loop_backend(self.backend_paths(settings)); + updater.is_starting_or_running().await + } + fn ensure_managed_codex_bin(&self) -> Result<()> { if self.managed_codex_bin.is_file() { return Ok(()); @@ -687,8 +732,12 @@ fn try_lock_file(_file: &tokio::fs::File) -> Result { mod tests { use pretty_assertions::assert_eq; + use super::BackendKind; + use super::BootstrapOutput; use super::BootstrapStatus; + use super::LifecycleOutput; use super::LifecycleStatus; + use super::RemoteControlStartOutput; use super::RemoteControlStatus; use super::RestartDecision; use super::RestartIfRunningOutcome; @@ -776,7 +825,7 @@ mod tests { restart_decision( RestartMode::Always, /*info*/ None, - /*managed_version*/ None + /*managed_version*/ None, ), ], [ @@ -787,4 +836,39 @@ mod tests { ] ); } + + #[test] + fn remote_control_start_output_serializes_inner_output_without_tag() { + let lifecycle_output = LifecycleOutput { + status: LifecycleStatus::AlreadyRunning, + backend: Some(BackendKind::Pid), + pid: None, + socket_path: "codex.sock".into(), + cli_version: Some("1.2.3".to_string()), + app_server_version: Some("1.2.4".to_string()), + }; + let output = RemoteControlStartOutput::Start(lifecycle_output.clone()); + + assert_eq!( + serde_json::to_value(output).expect("serialize"), + serde_json::to_value(lifecycle_output).expect("serialize") + ); + + let bootstrap_output = BootstrapOutput { + status: BootstrapStatus::Bootstrapped, + backend: BackendKind::Pid, + auto_update_enabled: true, + remote_control_enabled: true, + managed_codex_path: "codex".into(), + socket_path: "codex.sock".into(), + cli_version: "1.2.3".to_string(), + app_server_version: "1.2.4".to_string(), + }; + let output = RemoteControlStartOutput::Bootstrap(bootstrap_output.clone()); + + assert_eq!( + serde_json::to_value(output).expect("serialize"), + serde_json::to_value(bootstrap_output).expect("serialize") + ); + } } diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index d04b7092d..fba2c479c 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -129,7 +129,7 @@ enum Subcommand { /// [experimental] Run the app server or related tooling. AppServer(AppServerCommand), - /// [experimental] Start a headless app-server with remote control enabled. + /// [experimental] Ensure the app-server daemon is running with remote control enabled. RemoteControl, /// Launch the Codex desktop app (opens the app installer if missing). @@ -772,14 +772,6 @@ 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", @@ -989,16 +981,8 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { 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, - ) - .await?; + let output = codex_app_server_daemon::ensure_remote_control_started().await?; + println!("{}", serde_json::to_string(&output)?); } #[cfg(any(target_os = "macos", target_os = "windows"))] Some(Subcommand::App(app_cli)) => { @@ -2408,24 +2392,6 @@ 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([