diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 4e9e63d30273..6af19f89e293 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -2737,7 +2737,7 @@ "type": "string" }, "RemoteControlStatusChangedNotification": { - "description": "Current remote-control connection status and environment id exposed to clients.", + "description": "Current remote-control connection status and remote identity exposed to clients.", "properties": { "environmentId": { "type": [ @@ -2745,11 +2745,15 @@ "null" ] }, + "installationId": { + "type": "string" + }, "status": { "$ref": "#/definitions/RemoteControlConnectionStatus" } }, "required": [ + "installationId", "status" ], "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 156f6ddc4ab1..1747274799a3 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -13306,7 +13306,7 @@ }, "RemoteControlStatusChangedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Current remote-control connection status and environment id exposed to clients.", + "description": "Current remote-control connection status and remote identity exposed to clients.", "properties": { "environmentId": { "type": [ @@ -13314,11 +13314,15 @@ "null" ] }, + "installationId": { + "type": "string" + }, "status": { "$ref": "#/definitions/v2/RemoteControlConnectionStatus" } }, "required": [ + "installationId", "status" ], "title": "RemoteControlStatusChangedNotification", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 3c5eb030c5c7..05b916561092 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -9899,7 +9899,7 @@ }, "RemoteControlStatusChangedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Current remote-control connection status and environment id exposed to clients.", + "description": "Current remote-control connection status and remote identity exposed to clients.", "properties": { "environmentId": { "type": [ @@ -9907,11 +9907,15 @@ "null" ] }, + "installationId": { + "type": "string" + }, "status": { "$ref": "#/definitions/RemoteControlConnectionStatus" } }, "required": [ + "installationId", "status" ], "title": "RemoteControlStatusChangedNotification", diff --git a/codex-rs/app-server-protocol/schema/json/v2/RemoteControlStatusChangedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/RemoteControlStatusChangedNotification.json index 8286815ff46e..85be3316d7b1 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/RemoteControlStatusChangedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/RemoteControlStatusChangedNotification.json @@ -11,7 +11,7 @@ "type": "string" } }, - "description": "Current remote-control connection status and environment id exposed to clients.", + "description": "Current remote-control connection status and remote identity exposed to clients.", "properties": { "environmentId": { "type": [ @@ -19,11 +19,15 @@ "null" ] }, + "installationId": { + "type": "string" + }, "status": { "$ref": "#/definitions/RemoteControlConnectionStatus" } }, "required": [ + "installationId", "status" ], "title": "RemoteControlStatusChangedNotification", diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RemoteControlStatusChangedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RemoteControlStatusChangedNotification.ts index 16a9138556d4..8c63ab902996 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/RemoteControlStatusChangedNotification.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/RemoteControlStatusChangedNotification.ts @@ -4,6 +4,6 @@ import type { RemoteControlConnectionStatus } from "./RemoteControlConnectionStatus"; /** - * Current remote-control connection status and environment id exposed to clients. + * Current remote-control connection status and remote identity exposed to clients. */ -export type RemoteControlStatusChangedNotification = { status: RemoteControlConnectionStatus, environmentId: string | null, }; +export type RemoteControlStatusChangedNotification = { status: RemoteControlConnectionStatus, installationId: string, environmentId: string | null, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/remote_control.rs b/codex-rs/app-server-protocol/src/protocol/v2/remote_control.rs index 7d6383f46800..7dab24c3d443 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/remote_control.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/remote_control.rs @@ -3,12 +3,13 @@ use serde::Deserialize; use serde::Serialize; use ts_rs::TS; -/// Current remote-control connection status and environment id exposed to clients. +/// Current remote-control connection status and remote identity exposed to clients. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct RemoteControlStatusChangedNotification { pub status: RemoteControlConnectionStatus, + pub installation_id: String, pub environment_id: Option, } diff --git a/codex-rs/app-server-transport/src/lib.rs b/codex-rs/app-server-transport/src/lib.rs index 0a5c080acc7e..e3f39f60ce57 100644 --- a/codex-rs/app-server-transport/src/lib.rs +++ b/codex-rs/app-server-transport/src/lib.rs @@ -11,6 +11,7 @@ pub use transport::AppServerTransportParseError; pub use transport::CHANNEL_CAPACITY; pub use transport::ConnectionOrigin; pub use transport::RemoteControlHandle; +pub use transport::RemoteControlStartConfig; pub use transport::TransportEvent; pub use transport::app_server_control_socket_path; pub use transport::auth; diff --git a/codex-rs/app-server-transport/src/transport/mod.rs b/codex-rs/app-server-transport/src/transport/mod.rs index c63a79a0c14c..3e76069f435f 100644 --- a/codex-rs/app-server-transport/src/transport/mod.rs +++ b/codex-rs/app-server-transport/src/transport/mod.rs @@ -31,6 +31,7 @@ mod unix_socket_tests; mod websocket; pub use remote_control::RemoteControlHandle; +pub use remote_control::RemoteControlStartConfig; pub use remote_control::start_remote_control; pub use stdio::start_stdio_connection; pub use unix_socket::start_control_socket_acceptor; diff --git a/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs b/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs index fb7f727b8307..60dfc3d845e4 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs @@ -19,6 +19,7 @@ const REQUEST_ID_HEADER: &str = "x-request-id"; const OAI_REQUEST_ID_HEADER: &str = "x-oai-request-id"; const CF_RAY_HEADER: &str = "cf-ray"; pub(super) const REMOTE_CONTROL_ACCOUNT_ID_HEADER: &str = "chatgpt-account-id"; +pub(super) const REMOTE_CONTROL_INSTALLATION_ID_HEADER: &str = "x-codex-installation-id"; #[derive(Debug, Clone, PartialEq, Eq)] pub(super) struct RemoteControlEnrollment { @@ -193,6 +194,7 @@ pub(crate) fn format_headers(headers: &HeaderMap) -> String { pub(super) async fn enroll_remote_control_server( remote_control_target: &RemoteControlTarget, auth: &RemoteControlConnectionAuth, + installation_id: &str, ) -> io::Result { let enroll_url = &remote_control_target.enroll_url; let server_name = gethostname().to_string_lossy().trim().to_string(); @@ -201,6 +203,7 @@ pub(super) async fn enroll_remote_control_server( os: std::env::consts::OS, arch: std::env::consts::ARCH, app_server_version: env!("CARGO_PKG_VERSION"), + installation_id: installation_id.to_string(), }; let client = build_reqwest_client(); let mut auth_headers = HeaderMap::new(); @@ -210,6 +213,7 @@ pub(super) async fn enroll_remote_control_server( .timeout(REMOTE_CONTROL_ENROLL_TIMEOUT) .headers(auth_headers) .header(REMOTE_CONTROL_ACCOUNT_ID_HEADER, &auth.account_id) + .header(REMOTE_CONTROL_INSTALLATION_ID_HEADER, installation_id) .json(&request); let response = http_request.send().await.map_err(|err| { @@ -459,6 +463,7 @@ mod tests { auth_provider: codex_model_provider::unauthenticated_auth_provider(), account_id: "account_id".to_string(), }, + "11111111-1111-4111-8111-111111111111", ) .await .expect_err("invalid response should fail to parse"); 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 87405efa4f81..9ffe8b60bb9c 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 @@ -28,6 +28,11 @@ use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; use tracing::warn; +pub struct RemoteControlStartConfig { + pub remote_control_url: String, + pub installation_id: String, +} + pub(super) struct QueuedServerEnvelope { pub(super) event: ServerEvent, pub(super) client_id: ClientId, @@ -62,7 +67,7 @@ impl RemoteControlHandle { } pub async fn start_remote_control( - remote_control_url: String, + config: RemoteControlStartConfig, state_db: Option>, auth_manager: Arc, transport_event_tx: mpsc::Sender, @@ -77,7 +82,7 @@ pub async fn start_remote_control( warn!("remote control disabled because sqlite state db is unavailable"); } let remote_control_target = if initial_enabled { - Some(normalize_remote_control_url(&remote_control_url)?) + Some(normalize_remote_control_url(&config.remote_control_url)?) } else { None }; @@ -89,14 +94,18 @@ pub async fn start_remote_control( } else { RemoteControlConnectionStatus::Disabled }, + installation_id: config.installation_id.clone(), environment_id: None, }; let (status_tx, _status_rx) = watch::channel(initial_status); let status_publisher = RemoteControlStatusPublisher::new(status_tx.clone()); let join_handle = tokio::spawn(async move { RemoteControlWebsocket::new( - remote_control_url, - remote_control_target, + websocket::RemoteControlWebsocketConfig { + remote_control_url: config.remote_control_url, + installation_id: config.installation_id, + remote_control_target, + }, state_db, auth_manager, RemoteControlChannels { diff --git a/codex-rs/app-server-transport/src/transport/remote_control/protocol.rs b/codex-rs/app-server-transport/src/transport/remote_control/protocol.rs index dea5404ab199..cee646b0d936 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/protocol.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/protocol.rs @@ -19,6 +19,7 @@ pub(super) struct EnrollRemoteServerRequest { pub(super) os: &'static str, pub(super) arch: &'static str, pub(super) app_server_version: &'static str, + pub(super) installation_id: String, } #[derive(Debug, Deserialize)] 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 5fd3caa401b8..88b7798827a1 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 @@ -1,4 +1,5 @@ use super::enroll::REMOTE_CONTROL_ACCOUNT_ID_HEADER; +use super::enroll::REMOTE_CONTROL_INSTALLATION_ID_HEADER; use super::enroll::RemoteControlEnrollment; use super::enroll::load_persisted_remote_control_enrollment; use super::enroll::update_persisted_remote_control_enrollment; @@ -56,6 +57,8 @@ use tokio_tungstenite::accept_hdr_async; use tokio_tungstenite::tungstenite; use tokio_util::sync::CancellationToken; +const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111"; + fn remote_control_auth_manager() -> Arc { auth_manager_from_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) } @@ -131,6 +134,7 @@ async fn expect_remote_control_status( if let Some(expected_status) = expected_status { assert_eq!(status.status, expected_status); } + assert_eq!(status.installation_id, TEST_INSTALLATION_ID); assert_eq!(status.environment_id.as_deref(), expected_environment_id); } @@ -173,7 +177,10 @@ async fn remote_control_transport_manages_virtual_clients_and_routes_messages() mpsc::channel::(CHANNEL_CAPACITY); let shutdown_token = CancellationToken::new(); let (remote_task, remote_handle) = start_remote_control( - remote_control_url, + RemoteControlStartConfig { + remote_control_url, + installation_id: TEST_INSTALLATION_ID.to_string(), + }, Some(remote_control_state_runtime(&codex_home).await), remote_control_auth_manager(), transport_event_tx, @@ -449,7 +456,10 @@ async fn remote_control_transport_reconnects_after_disconnect() { mpsc::channel::(CHANNEL_CAPACITY); let shutdown_token = CancellationToken::new(); let (remote_task, remote_handle) = start_remote_control( - remote_control_url, + RemoteControlStartConfig { + remote_control_url, + installation_id: TEST_INSTALLATION_ID.to_string(), + }, Some(remote_control_state_runtime(&codex_home).await), remote_control_auth_manager(), transport_event_tx, @@ -528,7 +538,10 @@ async fn remote_control_start_allows_remote_control_invalid_url_when_disabled() mpsc::channel::(CHANNEL_CAPACITY); let shutdown_token = CancellationToken::new(); let (remote_task, _remote_handle) = start_remote_control( - "https://internal.example.com/backend-api/".to_string(), + RemoteControlStartConfig { + remote_control_url: "https://internal.example.com/backend-api/".to_string(), + installation_id: TEST_INSTALLATION_ID.to_string(), + }, /*state_db*/ None, remote_control_auth_manager(), transport_event_tx, @@ -564,7 +577,10 @@ async fn remote_control_start_allows_missing_auth_when_enabled() { mpsc::channel::(CHANNEL_CAPACITY); let shutdown_token = CancellationToken::new(); let (remote_task, _remote_handle) = start_remote_control( - remote_control_url, + RemoteControlStartConfig { + remote_control_url, + installation_id: TEST_INSTALLATION_ID.to_string(), + }, Some(remote_control_state_runtime(&codex_home).await), auth_manager, transport_event_tx, @@ -596,7 +612,10 @@ async fn remote_control_start_reports_missing_state_db_as_disabled_when_enabled( mpsc::channel::(CHANNEL_CAPACITY); let shutdown_token = CancellationToken::new(); let (remote_task, remote_handle) = start_remote_control( - remote_control_url, + RemoteControlStartConfig { + remote_control_url, + installation_id: TEST_INSTALLATION_ID.to_string(), + }, /*state_db*/ None, remote_control_auth_manager(), transport_event_tx, @@ -611,6 +630,7 @@ async fn remote_control_start_reports_missing_state_db_as_disabled_when_enabled( status_rx.borrow().clone(), RemoteControlStatusChangedNotification { status: RemoteControlConnectionStatus::Disabled, + installation_id: TEST_INSTALLATION_ID.to_string(), environment_id: None, } ); @@ -645,7 +665,10 @@ async fn remote_control_handle_set_enabled_stops_and_restarts_connections() { mpsc::channel::(CHANNEL_CAPACITY); let shutdown_token = CancellationToken::new(); let (remote_task, remote_handle) = start_remote_control( - remote_control_url, + RemoteControlStartConfig { + remote_control_url, + installation_id: TEST_INSTALLATION_ID.to_string(), + }, Some(remote_control_state_runtime(&codex_home).await), remote_control_auth_manager(), transport_event_tx, @@ -672,6 +695,7 @@ async fn remote_control_handle_set_enabled_stops_and_restarts_connections() { &mut status_rx, RemoteControlStatusChangedNotification { status: RemoteControlConnectionStatus::Connected, + installation_id: TEST_INSTALLATION_ID.to_string(), environment_id: Some("env_test".to_string()), }, ) @@ -682,6 +706,7 @@ async fn remote_control_handle_set_enabled_stops_and_restarts_connections() { &mut status_rx, RemoteControlStatusChangedNotification { status: RemoteControlConnectionStatus::Disabled, + installation_id: TEST_INSTALLATION_ID.to_string(), environment_id: None, }, ) @@ -698,6 +723,7 @@ async fn remote_control_handle_set_enabled_stops_and_restarts_connections() { &mut status_rx, RemoteControlStatusChangedNotification { status: RemoteControlConnectionStatus::Connecting, + installation_id: TEST_INSTALLATION_ID.to_string(), environment_id: Some("env_test".to_string()), }, ) @@ -729,7 +755,10 @@ async fn remote_control_transport_clears_outgoing_buffer_when_backend_acks() { mpsc::channel::(CHANNEL_CAPACITY); let shutdown_token = CancellationToken::new(); let (remote_task, remote_handle) = start_remote_control( - remote_control_url, + RemoteControlStartConfig { + remote_control_url, + installation_id: TEST_INSTALLATION_ID.to_string(), + }, Some(remote_control_state_runtime(&codex_home).await), remote_control_auth_manager(), transport_event_tx, @@ -904,7 +933,10 @@ async fn remote_control_http_mode_enrolls_before_connecting() { let expected_server_name = gethostname().to_string_lossy().trim().to_string(); let shutdown_token = CancellationToken::new(); let (remote_task, remote_handle) = start_remote_control( - remote_control_url, + RemoteControlStartConfig { + remote_control_url, + installation_id: TEST_INSTALLATION_ID.to_string(), + }, Some(remote_control_state_runtime(&codex_home).await), remote_control_auth_manager(), transport_event_tx, @@ -929,6 +961,12 @@ async fn remote_control_http_mode_enrolls_before_connecting() { enroll_request.headers.get(REMOTE_CONTROL_ACCOUNT_ID_HEADER), Some(&"account_id".to_string()) ); + assert_eq!( + enroll_request + .headers + .get(REMOTE_CONTROL_INSTALLATION_ID_HEADER), + Some(&TEST_INSTALLATION_ID.to_string()) + ); assert_eq!( serde_json::from_str::(&enroll_request.body) .expect("enroll body should deserialize"), @@ -937,6 +975,7 @@ async fn remote_control_http_mode_enrolls_before_connecting() { "os": std::env::consts::OS, "arch": std::env::consts::ARCH, "app_server_version": env!("CARGO_PKG_VERSION"), + "installation_id": TEST_INSTALLATION_ID, }) ); respond_with_json( @@ -967,6 +1006,12 @@ async fn remote_control_http_mode_enrolls_before_connecting() { .get(REMOTE_CONTROL_ACCOUNT_ID_HEADER), Some(&"account_id".to_string()) ); + assert_eq!( + handshake_request + .headers + .get(REMOTE_CONTROL_INSTALLATION_ID_HEADER), + Some(&TEST_INSTALLATION_ID.to_string()) + ); assert_eq!( handshake_request.headers.get("x-codex-server-id"), Some(&"srv_e_test".to_string()) @@ -1128,7 +1173,10 @@ async fn remote_control_http_mode_reuses_persisted_enrollment_before_reenrolling mpsc::channel::(CHANNEL_CAPACITY); let shutdown_token = CancellationToken::new(); let (remote_task, _remote_handle) = start_remote_control( - remote_control_url, + RemoteControlStartConfig { + remote_control_url, + installation_id: TEST_INSTALLATION_ID.to_string(), + }, Some(state_db.clone()), remote_control_auth_manager_with_home(&codex_home), transport_event_tx, @@ -1196,7 +1244,10 @@ async fn remote_control_stdio_mode_waits_for_client_name_before_connecting() { let (app_server_client_name_tx, app_server_client_name_rx) = oneshot::channel::(); let shutdown_token = CancellationToken::new(); let (remote_task, _remote_handle) = start_remote_control( - remote_control_url, + RemoteControlStartConfig { + remote_control_url, + installation_id: TEST_INSTALLATION_ID.to_string(), + }, Some(state_db.clone()), remote_control_auth_manager_with_home(&codex_home), transport_event_tx, @@ -1255,7 +1306,10 @@ async fn remote_control_waits_for_account_id_before_enrolling() { mpsc::channel::(CHANNEL_CAPACITY); let shutdown_token = CancellationToken::new(); let (remote_task, _remote_handle) = start_remote_control( - remote_control_url, + RemoteControlStartConfig { + remote_control_url, + installation_id: TEST_INSTALLATION_ID.to_string(), + }, Some(state_db.clone()), auth_manager, transport_event_tx, @@ -1338,7 +1392,10 @@ async fn remote_control_http_mode_clears_stale_persisted_enrollment_after_404() mpsc::channel::(CHANNEL_CAPACITY); let shutdown_token = CancellationToken::new(); let (remote_task, remote_handle) = start_remote_control( - remote_control_url, + RemoteControlStartConfig { + remote_control_url, + installation_id: TEST_INSTALLATION_ID.to_string(), + }, Some(state_db.clone()), remote_control_auth_manager_with_home(&codex_home), transport_event_tx, 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 f7b49b72ec3d..472639bc68fa 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 @@ -55,6 +55,7 @@ use tracing::warn; pub(super) const REMOTE_CONTROL_PROTOCOL_VERSION: &str = "3"; pub(super) const REMOTE_CONTROL_ACCOUNT_ID_HEADER: &str = "chatgpt-account-id"; +pub(super) const REMOTE_CONTROL_INSTALLATION_ID_HEADER: &str = "x-codex-installation-id"; const REMOTE_CONTROL_SUBSCRIBE_CURSOR_HEADER: &str = "x-codex-subscribe-cursor"; const REMOTE_CONTROL_WEBSOCKET_PING_INTERVAL: std::time::Duration = std::time::Duration::from_secs(10); @@ -214,6 +215,7 @@ impl WebsocketState { pub(crate) struct RemoteControlWebsocket { remote_control_url: String, + installation_id: String, remote_control_target: Option, state_db: Option>, auth_manager: Arc, @@ -229,6 +231,12 @@ pub(crate) struct RemoteControlWebsocket { enabled_rx: watch::Receiver, } +pub(crate) struct RemoteControlWebsocketConfig { + pub(crate) remote_control_url: String, + pub(crate) installation_id: String, + pub(crate) remote_control_target: Option, +} + enum ConnectOutcome { Connected(Box>>), Disabled, @@ -254,6 +262,7 @@ impl RemoteControlStatusPublisher { self.tx.send_if_modified(|status| { let next_status = RemoteControlStatusChangedNotification { status: connection_status, + installation_id: status.installation_id.clone(), environment_id: if connection_status == RemoteControlConnectionStatus::Disabled { None } else { @@ -276,6 +285,7 @@ impl RemoteControlStatusPublisher { } let next_status = RemoteControlStatusChangedNotification { status: status.status, + installation_id: status.installation_id.clone(), environment_id, }; if *status == next_status { @@ -290,14 +300,14 @@ impl RemoteControlStatusPublisher { #[derive(Clone, Copy)] pub(super) struct RemoteControlConnectOptions<'a> { + installation_id: &'a str, subscribe_cursor: Option<&'a str>, app_server_client_name: Option<&'a str>, } impl RemoteControlWebsocket { pub(crate) fn new( - remote_control_url: String, - remote_control_target: Option, + config: RemoteControlWebsocketConfig, state_db: Option>, auth_manager: Arc, channels: RemoteControlChannels, @@ -315,8 +325,9 @@ impl RemoteControlWebsocket { let auth_recovery = auth_manager.unauthorized_recovery(); Self { - remote_control_url, - remote_control_target, + remote_control_url: config.remote_control_url, + installation_id: config.installation_id, + remote_control_target: config.remote_control_target, state_db, auth_manager, status_publisher: channels.status_publisher, @@ -442,6 +453,7 @@ impl RemoteControlWebsocket { loop { let subscribe_cursor = self.state.lock().await.subscribe_cursor.clone(); let connect_options = RemoteControlConnectOptions { + installation_id: &self.installation_id, subscribe_cursor: subscribe_cursor.as_deref(), app_server_client_name, }; @@ -918,6 +930,7 @@ fn build_remote_control_websocket_request( websocket_url: &str, enrollment: &RemoteControlEnrollment, auth: &RemoteControlConnectionAuth, + installation_id: &str, subscribe_cursor: Option<&str>, ) -> io::Result> { let mut request = websocket_url.into_client_request().map_err(|err| { @@ -942,6 +955,11 @@ fn build_remote_control_websocket_request( auth.auth_provider.add_auth_headers(&mut auth_headers); headers.extend(auth_headers); set_remote_control_header(headers, REMOTE_CONTROL_ACCOUNT_ID_HEADER, &auth.account_id)?; + set_remote_control_header( + headers, + REMOTE_CONTROL_INSTALLATION_ID_HEADER, + installation_id, + )?; if let Some(subscribe_cursor) = subscribe_cursor { set_remote_control_header( headers, @@ -1066,7 +1084,12 @@ pub(super) async fn connect_remote_control_websocket( "creating new remote control enrollment: websocket_url={}, enroll_url={}, account_id={}", remote_control_target.websocket_url, remote_control_target.enroll_url, auth.account_id ); - let new_enrollment = match enroll_remote_control_server(remote_control_target, &auth).await + let new_enrollment = match enroll_remote_control_server( + remote_control_target, + &auth, + connect_options.installation_id, + ) + .await { Ok(new_enrollment) => new_enrollment, Err(err) @@ -1110,6 +1133,7 @@ pub(super) async fn connect_remote_control_websocket( &remote_control_target.websocket_url, enrollment_ref, &auth, + connect_options.installation_id, connect_options.subscribe_cursor, )?; @@ -1247,6 +1271,7 @@ mod tests { const TEST_HTTP_ACCEPT_TIMEOUT: Duration = Duration::from_secs(30); #[cfg(not(windows))] const TEST_HTTP_ACCEPT_TIMEOUT: Duration = Duration::from_secs(5); + const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111"; fn remote_control_status_channel() -> ( RemoteControlStatusPublisher, @@ -1254,6 +1279,7 @@ mod tests { ) { let (status_tx, status_rx) = watch::channel(RemoteControlStatusChangedNotification { status: RemoteControlConnectionStatus::Connecting, + installation_id: TEST_INSTALLATION_ID.to_string(), environment_id: None, }); (RemoteControlStatusPublisher::new(status_tx), status_rx) @@ -1359,6 +1385,7 @@ mod tests { &mut auth_recovery, &mut enrollment, RemoteControlConnectOptions { + installation_id: TEST_INSTALLATION_ID, subscribe_cursor: None, app_server_client_name: None, }, @@ -1376,6 +1403,7 @@ mod tests { status_rx.borrow().clone(), RemoteControlStatusChangedNotification { status: RemoteControlConnectionStatus::Connecting, + installation_id: TEST_INSTALLATION_ID.to_string(), environment_id: Some("env_test".to_string()), } ); @@ -1435,6 +1463,7 @@ mod tests { &mut auth_recovery, &mut enrollment, RemoteControlConnectOptions { + installation_id: TEST_INSTALLATION_ID, subscribe_cursor: None, app_server_client_name: None, }, @@ -1448,6 +1477,7 @@ mod tests { status_rx.borrow().clone(), RemoteControlStatusChangedNotification { status: RemoteControlConnectionStatus::Connecting, + installation_id: TEST_INSTALLATION_ID.to_string(), environment_id: Some("env_test".to_string()), } ); @@ -1515,6 +1545,7 @@ mod tests { &mut auth_recovery, &mut enrollment, RemoteControlConnectOptions { + installation_id: TEST_INSTALLATION_ID, subscribe_cursor: None, app_server_client_name: None, }, @@ -1567,6 +1598,7 @@ mod tests { &mut auth_recovery, &mut enrollment, RemoteControlConnectOptions { + installation_id: TEST_INSTALLATION_ID, subscribe_cursor: None, app_server_client_name: None, }, @@ -1614,6 +1646,7 @@ mod tests { &mut auth_recovery, &mut enrollment, RemoteControlConnectOptions { + installation_id: TEST_INSTALLATION_ID, subscribe_cursor: None, app_server_client_name: None, }, @@ -1632,6 +1665,7 @@ mod tests { status_rx.borrow().clone(), RemoteControlStatusChangedNotification { status: RemoteControlConnectionStatus::Connecting, + installation_id: TEST_INSTALLATION_ID.to_string(), environment_id: None, } ); @@ -1656,8 +1690,11 @@ mod tests { let shutdown_token = shutdown_token.clone(); async move { RemoteControlWebsocket::new( - remote_control_url, - Some(remote_control_target), + RemoteControlWebsocketConfig { + remote_control_url, + installation_id: TEST_INSTALLATION_ID.to_string(), + remote_control_target: Some(remote_control_target), + }, /*state_db*/ None, remote_control_auth_manager(), RemoteControlChannels { @@ -1701,6 +1738,7 @@ mod tests { status_rx.borrow().clone(), RemoteControlStatusChangedNotification { status: RemoteControlConnectionStatus::Connecting, + installation_id: TEST_INSTALLATION_ID.to_string(), environment_id: Some("env_first".to_string()), } ); @@ -1721,6 +1759,7 @@ mod tests { status_rx.borrow().clone(), RemoteControlStatusChangedNotification { status: RemoteControlConnectionStatus::Connected, + installation_id: TEST_INSTALLATION_ID.to_string(), environment_id: Some("env_first".to_string()), } ); @@ -1734,6 +1773,7 @@ mod tests { status_rx.borrow().clone(), RemoteControlStatusChangedNotification { status: RemoteControlConnectionStatus::Connected, + installation_id: TEST_INSTALLATION_ID.to_string(), environment_id: None, } ); @@ -1748,6 +1788,7 @@ mod tests { status_rx.borrow().clone(), RemoteControlStatusChangedNotification { status: RemoteControlConnectionStatus::Disabled, + installation_id: TEST_INSTALLATION_ID.to_string(), environment_id: None, } ); diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index 08aab99f6549..91c7e801a735 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -31,6 +31,7 @@ use crate::outgoing_message::QueuedOutgoingMessage; use crate::transport::CHANNEL_CAPACITY; use crate::transport::ConnectionState; use crate::transport::OutboundConnectionState; +use crate::transport::RemoteControlStartConfig; use crate::transport::TransportEvent; use crate::transport::auth::policy_from_settings; use crate::transport::route_outgoing_envelope; @@ -686,7 +687,10 @@ pub async fn run_main_with_transport_options( } let (remote_control_accept_handle, remote_control_handle) = start_remote_control( - config.chatgpt_base_url.clone(), + RemoteControlStartConfig { + remote_control_url: config.chatgpt_base_url.clone(), + installation_id: installation_id.clone(), + }, state_db.clone(), auth_manager.clone(), transport_event_tx.clone(), @@ -977,6 +981,7 @@ pub async fn run_main_with_transport_options( .send_server_notification(ServerNotification::RemoteControlStatusChanged( RemoteControlStatusChangedNotification { status: status.status, + installation_id: status.installation_id, environment_id: status.environment_id, }, )) diff --git a/codex-rs/app-server/src/transport.rs b/codex-rs/app-server/src/transport.rs index 8d61ac5f56d3..4eae17e4696e 100644 --- a/codex-rs/app-server/src/transport.rs +++ b/codex-rs/app-server/src/transport.rs @@ -19,6 +19,7 @@ pub(crate) use codex_app_server_transport::ConnectionOrigin; pub(crate) use codex_app_server_transport::OutgoingMessage; pub(crate) use codex_app_server_transport::QueuedOutgoingMessage; pub(crate) use codex_app_server_transport::RemoteControlHandle; +pub(crate) use codex_app_server_transport::RemoteControlStartConfig; pub(crate) use codex_app_server_transport::TransportEvent; pub use codex_app_server_transport::app_server_control_socket_path; pub use codex_app_server_transport::auth;