use super::auth::RemoteControlConnectionAuth;use super::auth::load_remote_control_auth;use super::auth::recover_remote_control_auth;use super::enroll::REMOTE_CONTROL_ACCOUNT_ID_HEADER;use super::enroll::format_headers;use super::enroll::preview_remote_control_response_body;use super::protocol::normalize_remote_control_base_url;use axum::http::HeaderMap;use codex_app_server_protocol::RemoteControlClient;use codex_app_server_protocol::RemoteControlClientsListOrder;use codex_app_server_protocol::RemoteControlClientsListParams;use codex_app_server_protocol::RemoteControlClientsListResponse;use codex_app_server_protocol::RemoteControlClientsRevokeParams;use codex_app_server_protocol::RemoteControlClientsRevokeResponse;use codex_login::AuthManager;use codex_login::default_client::build_reqwest_client;use serde::Deserialize;use std::io;use std::io::ErrorKind;use std::sync::Arc;use time::OffsetDateTime;use time::format_description::well_known::Rfc3339;use url::Url;const REMOTE_CONTROL_CLIENT_MANAGEMENT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);#[derive(Debug, Deserialize)]struct ListRemoteControlClientsResponse { items: Vec<RemoteControlClientResponse>, #[serde(default)] cursor: Option<String>,}#[derive(Debug, Deserialize)]struct RemoteControlClientResponse { client_id: String, #[serde(default)] display_name: Option<String>, #[serde(default)] device_type: Option<String>, #[serde(default)] platform: Option<String>, #[serde(default)] os_version: Option<String>, #[serde(default)] device_model: Option<String>, #[serde(default)] app_version: Option<String>, #[serde(default)] last_seen_at: Option<String>,}enum ClientManagementRequest<'a> { List { url: &'a Url, params: &'a RemoteControlClientsListParams, }, Revoke { url: &'a Url, },}struct ClientManagementResponse { status: axum::http::StatusCode, headers: HeaderMap, body: Vec<u8>,}pub(super) async fn list_remote_control_clients( remote_control_url: &str, auth_manager: &Arc<AuthManager>, params: RemoteControlClientsListParams,) -> io::Result<RemoteControlClientsListResponse> { if params.environment_id.is_empty() { return Err(io::Error::new( ErrorKind::InvalidInput, "remote control client list requires environmentId", )); } if params .limit .is_some_and(|limit| !(1..=100).contains(&limit)) { return Err(io::Error::new( ErrorKind::InvalidInput, "remote control client list limit must be between 1 and 100", )); } let url = environment_clients_url(remote_control_url, ¶ms.environment_id)?; let response = send_client_management_request( auth_manager, ClientManagementRequest::List { url: &url, params: ¶ms, }, "list remote control clients", ) .await?; let ClientManagementResponse { status, headers, body, } = response; let body_preview = preview_remote_control_response_body(&body); ensure_success_response(status, &headers, &url, &body_preview, "client list")?; let response = serde_json::from_slice::<ListRemoteControlClientsResponse>(&body).map_err( |err| { io::Error::other(format!( "failed to parse remote control client list response from `{url}`: HTTP {status}, {}, body: {body_preview}, decode error: {err}", format_headers(&headers) )) }, )?; Ok(RemoteControlClientsListResponse { data: response .items .into_iter() .map(RemoteControlClient::try_from) .collect::<io::Result<_>>()?, next_cursor: response.cursor, })}pub(super) async fn revoke_remote_control_client( remote_control_url: &str, auth_manager: &Arc<AuthManager>, params: RemoteControlClientsRevokeParams,) -> io::Result<RemoteControlClientsRevokeResponse> { if params.environment_id.is_empty() { return Err(io::Error::new( ErrorKind::InvalidInput, "remote control client revoke requires environmentId", )); } if params.client_id.is_empty() { return Err(io::Error::new( ErrorKind::InvalidInput, "remote control client revoke requires clientId", )); } let mut url = environment_clients_url(remote_control_url, ¶ms.environment_id)?; url.path_segments_mut() .map_err(|()| { io::Error::new( ErrorKind::InvalidInput, "remote control URL cannot be a base", ) })? .push(¶ms.client_id); let response = send_client_management_request( auth_manager, ClientManagementRequest::Revoke { url: &url }, "revoke remote control client", ) .await?; let ClientManagementResponse { status, headers, body, } = response; let body_preview = preview_remote_control_response_body(&body); ensure_success_response(status, &headers, &url, &body_preview, "client revoke")?; Ok(RemoteControlClientsRevokeResponse {})}async fn send_client_management_request( auth_manager: &Arc<AuthManager>, request: ClientManagementRequest<'_>, action: &str,) -> io::Result<ClientManagementResponse> { let mut auth_recovery = auth_manager.unauthorized_recovery(); let mut auth_change_rx = auth_manager.auth_change_receiver(); let auth = load_remote_control_auth(auth_manager).await?; let response = send_client_management_request_once(&auth, &request, action).await?; if response.status.as_u16() != 401 || !recover_remote_control_auth(&mut auth_recovery, &mut auth_change_rx).await { return Ok(response); } let auth = load_remote_control_auth(auth_manager).await?; send_client_management_request_once(&auth, &request, action).await}async fn send_client_management_request_once( auth: &RemoteControlConnectionAuth, request: &ClientManagementRequest<'_>, action: &str,) -> io::Result<ClientManagementResponse> { let client = build_reqwest_client(); let mut auth_headers = HeaderMap::new(); auth.auth_provider.add_auth_headers(&mut auth_headers); let request = match request { ClientManagementRequest::List { url, params } => { let mut query = Vec::new(); if let Some(cursor) = ¶ms.cursor { query.push(("cursor", cursor.clone())); } if let Some(limit) = params.limit { query.push(("limit", limit.to_string())); } if let Some(order) = params.order { query.push(( "order", match order { RemoteControlClientsListOrder::Asc => "asc", RemoteControlClientsListOrder::Desc => "desc", } .to_string(), )); } client.get((*url).clone()).query(&query) } ClientManagementRequest::Revoke { url } => client.delete((*url).clone()), }; let response = request .timeout(REMOTE_CONTROL_CLIENT_MANAGEMENT_TIMEOUT) .headers(auth_headers) .header(REMOTE_CONTROL_ACCOUNT_ID_HEADER, &auth.account_id) .send() .await .map_err(|err| io::Error::other(format!("failed to {action}: {err}")))?; let headers = response.headers().clone(); let status = response.status(); let body = response .bytes() .await .map_err(|err| io::Error::other(format!("failed to read {action} response: {err}")))? .to_vec(); Ok(ClientManagementResponse { status, headers, body, })}fn ensure_success_response( status: axum::http::StatusCode, headers: &HeaderMap, url: &Url, body_preview: &str, response_kind: &str,) -> io::Result<()> { if status.is_success() { return Ok(()); } let error_kind = match status.as_u16() { 400 => ErrorKind::InvalidInput, 401 | 403 => ErrorKind::PermissionDenied, 404 => ErrorKind::NotFound, _ => ErrorKind::Other, }; Err(io::Error::new( error_kind, format!( "remote control {response_kind} failed at `{url}`: HTTP {status}, {}, body: {body_preview}", format_headers(headers) ), ))}fn environment_clients_url(remote_control_url: &str, environment_id: &str) -> io::Result<Url> { let mut url = normalize_remote_control_base_url(remote_control_url)? .join("wham/remote/control/environments") .map_err(io::Error::other)?; url.path_segments_mut() .map_err(|()| { io::Error::new( ErrorKind::InvalidInput, "remote control URL cannot be a base", ) })? .push(environment_id) .push("clients"); Ok(url)}impl TryFrom<RemoteControlClientResponse> for RemoteControlClient { type Error = io::Error; fn try_from(client: RemoteControlClientResponse) -> Result<Self, Self::Error> { Ok(Self { client_id: client.client_id, display_name: client.display_name, device_type: client.device_type, platform: client.platform, os_version: client.os_version, device_model: client.device_model, app_version: client.app_version, last_seen_at: client .last_seen_at .map(|last_seen_at| { OffsetDateTime::parse(&last_seen_at, &Rfc3339) .map(OffsetDateTime::unix_timestamp) .map_err(|err| { io::Error::new( ErrorKind::InvalidData, format!( "failed to parse remote control client last_seen_at `{last_seen_at}`: {err}" ), ) }) }) .transpose()?, }) }}