From 413ae91e53dc5b0864dfbf6e4ea13f6920ff83e0 Mon Sep 17 00:00:00 2001 From: Anton Panasenko Date: Fri, 29 May 2026 03:03:24 -0700 Subject: [PATCH] fix(app-server-transport): clear stale pairing client --- .../src/transport/remote_control/mod.rs | 7 +++ .../src/transport/remote_control/tests.rs | 45 +++++++++++++++++++ .../src/transport/remote_control/websocket.rs | 7 +-- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/codex-rs/app-server-transport/src/transport/remote_control/mod.rs b/codex-rs/app-server-transport/src/transport/remote_control/mod.rs index 065eb735c4..6c4f774c60 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/mod.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/mod.rs @@ -114,6 +114,7 @@ impl RemoteControlHandle { *state = false; changed }); + clear_pairing_client(&self.pairing_client); let status = self.status(); info!( @@ -211,6 +212,12 @@ fn remote_control_status_with_connection_status( } } +fn clear_pairing_client(pairing_client: &Arc>>) { + *pairing_client + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) = None; +} + pub async fn start_remote_control( config: RemoteControlStartConfig, state_db: Option>, diff --git a/codex-rs/app-server-transport/src/transport/remote_control/tests.rs b/codex-rs/app-server-transport/src/transport/remote_control/tests.rs index 85bcf09222..b763907707 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/tests.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/tests.rs @@ -20,6 +20,7 @@ use codex_app_server_protocol::AuthMode; use codex_app_server_protocol::ConfigWarningNotification; use codex_app_server_protocol::JSONRPCMessage; use codex_app_server_protocol::RemoteControlConnectionStatus; +use codex_app_server_protocol::RemoteControlPairingStartParams; use codex_app_server_protocol::RemoteControlStatusChangedNotification; use codex_app_server_protocol::ServerNotification; use codex_config::types::AuthCredentialsStoreMode; @@ -39,7 +40,9 @@ use pretty_assertions::assert_eq; use serde_json::json; use std::collections::BTreeMap; use std::sync::Arc; +use std::sync::Mutex as StdMutex; use tempfile::TempDir; +use time::OffsetDateTime; use tokio::io::AsyncBufReadExt; use tokio::io::AsyncReadExt; use tokio::io::AsyncWriteExt; @@ -895,6 +898,48 @@ async fn remote_control_handle_enable_disable_stops_and_restarts_connections() { let _ = remote_task.await; } +#[tokio::test] +async fn remote_control_handle_disable_clears_stale_pairing_client() { + let (enabled_tx, _enabled_rx) = watch::channel(/*init*/ true); + let (status_tx, _status_rx) = watch::channel(RemoteControlStatusChangedNotification { + status: RemoteControlConnectionStatus::Connected, + server_name: test_server_name(), + installation_id: TEST_INSTALLATION_ID.to_string(), + environment_id: Some("env_test".to_string()), + }); + let pairing_client = Arc::new(StdMutex::new(Some(RemoteControlPairingClient::new( + &normalize_remote_control_url("http://127.0.0.1:1/backend-api/wham/remote/control") + .expect("remote control target should normalize"), + TEST_REMOTE_CONTROL_SERVER_TOKEN.to_string(), + OffsetDateTime::from_unix_timestamp(33_336_362_096).expect("future timestamp should parse"), + )))); + let remote_handle = RemoteControlHandle { + enabled_tx: Arc::new(enabled_tx), + status_tx: Arc::new(status_tx), + state_db_available: true, + pairing_client, + }; + + assert_eq!( + remote_handle.disable(), + RemoteControlStatusChangedNotification { + status: RemoteControlConnectionStatus::Disabled, + server_name: test_server_name(), + installation_id: TEST_INSTALLATION_ID.to_string(), + environment_id: None, + } + ); + remote_handle.enable().expect("enable should succeed"); + assert_eq!( + remote_handle + .start_pairing(RemoteControlPairingStartParams { manual_code: false }) + .await + .expect_err("re-enabled remote control should wait for refreshed pairing auth") + .to_string(), + "remote control pairing is unavailable until enrollment completes" + ); +} + #[tokio::test] async fn remote_control_transport_clears_outgoing_buffer_when_backend_acks() { let listener = TcpListener::bind("127.0.0.1:0") diff --git a/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs b/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs index 6c187e9c96..f647fea817 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs @@ -11,6 +11,7 @@ use crate::transport::remote_control::enroll::refresh_remote_control_server; use crate::transport::remote_control::enroll::update_persisted_remote_control_enrollment; use crate::transport::remote_control::pairing::RemoteControlPairingClient; +use super::clear_pairing_client; use super::protocol::ClientEnvelope; use super::protocol::ClientEvent; use super::protocol::ClientId; @@ -1515,12 +1516,6 @@ fn set_pairing_client( Ok(()) } -fn clear_pairing_client(pairing_client: &Arc>>) { - *pairing_client - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) = None; -} - async fn enroll_remote_control_server_if_missing( remote_control_target: &RemoteControlTarget, state_db: &StateRuntime,