Codex Handbook
app-server-transport/src/transport/remote_control/clients.rs 307 lines
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, &params.environment_id)?;    let response = send_client_management_request(        auth_manager,        ClientManagementRequest::List {            url: &url,            params: &params,        },        "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, &params.environment_id)?;    url.path_segments_mut()        .map_err(|()| {            io::Error::new(                ErrorKind::InvalidInput,                "remote control URL cannot be a base",            )        })?        .push(&params.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) = &params.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()?,        })    }}