Codex Handbook
core-plugins/src/remote_legacy.rs 235 lines
use crate::remote::RemotePluginServiceConfig;use codex_login::CodexAuth;use codex_login::default_client::build_reqwest_client;use codex_protocol::protocol::Product;use serde::Deserialize;use std::time::Duration;use url::Url;const REMOTE_FEATURED_PLUGIN_FETCH_TIMEOUT: Duration = Duration::from_secs(10);const REMOTE_PLUGIN_MUTATION_TIMEOUT: Duration = Duration::from_secs(30);#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]#[serde(rename_all = "camelCase")]struct RemotePluginMutationResponse {    pub id: String,    pub enabled: bool,}#[derive(Debug, thiserror::Error)]pub enum RemotePluginMutationError {    #[error("chatgpt authentication required for remote plugin mutation")]    AuthRequired,    #[error(        "chatgpt authentication required for remote plugin mutation; api key auth is not supported"    )]    UnsupportedAuthMode,    #[error("failed to read auth token for remote plugin mutation: {0}")]    AuthToken(#[source] std::io::Error),    #[error("invalid chatgpt base url for remote plugin mutation: {0}")]    InvalidBaseUrl(#[source] url::ParseError),    #[error("chatgpt base url cannot be used for plugin mutation")]    InvalidBaseUrlPath,    #[error("failed to send remote plugin mutation request to {url}: {source}")]    Request {        url: String,        #[source]        source: reqwest::Error,    },    #[error("remote plugin mutation failed with status {status} from {url}: {body}")]    UnexpectedStatus {        url: String,        status: reqwest::StatusCode,        body: String,    },    #[error("failed to parse remote plugin mutation response from {url}: {source}")]    Decode {        url: String,        #[source]        source: serde_json::Error,    },    #[error(        "remote plugin mutation returned unexpected plugin id: expected `{expected}`, got `{actual}`"    )]    UnexpectedPluginId { expected: String, actual: String },    #[error(        "remote plugin mutation returned unexpected enabled state for `{plugin_id}`: expected {expected_enabled}, got {actual_enabled}"    )]    UnexpectedEnabledState {        plugin_id: String,        expected_enabled: bool,        actual_enabled: bool,    },}#[derive(Debug, thiserror::Error)]pub enum RemotePluginFetchError {    #[error("failed to send remote featured plugin request to {url}: {source}")]    Request {        url: String,        #[source]        source: reqwest::Error,    },    #[error("remote featured plugin request to {url} failed with status {status}: {body}")]    UnexpectedStatus {        url: String,        status: reqwest::StatusCode,        body: String,    },    #[error("failed to parse remote featured plugin response from {url}: {source}")]    Decode {        url: String,        #[source]        source: serde_json::Error,    },}pub async fn fetch_remote_featured_plugin_ids(    config: &RemotePluginServiceConfig,    auth: Option<&CodexAuth>,    product: Option<Product>,) -> Result<Vec<String>, RemotePluginFetchError> {    let base_url = config.chatgpt_base_url.trim_end_matches('/');    let url = format!("{base_url}/plugins/featured");    let client = build_reqwest_client();    let mut request = client        .get(&url)        .query(&[(            "platform",            product.unwrap_or(Product::Codex).to_app_platform(),        )])        .timeout(REMOTE_FEATURED_PLUGIN_FETCH_TIMEOUT);    if let Some(auth) = auth.filter(|auth| auth.uses_codex_backend()) {        request =            request.headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers());    }    let response = request        .send()        .await        .map_err(|source| RemotePluginFetchError::Request {            url: url.clone(),            source,        })?;    let status = response.status();    let body = response.text().await.unwrap_or_default();    if !status.is_success() {        return Err(RemotePluginFetchError::UnexpectedStatus { url, status, body });    }    serde_json::from_str(&body).map_err(|source| RemotePluginFetchError::Decode {        url: url.clone(),        source,    })}pub async fn enable_remote_plugin(    config: &RemotePluginServiceConfig,    auth: Option<&CodexAuth>,    plugin_id: &str,) -> Result<(), RemotePluginMutationError> {    post_remote_plugin_mutation(config, auth, plugin_id, "enable").await?;    Ok(())}pub async fn uninstall_remote_plugin(    config: &RemotePluginServiceConfig,    auth: Option<&CodexAuth>,    plugin_id: &str,) -> Result<(), RemotePluginMutationError> {    post_remote_plugin_mutation(config, auth, plugin_id, "uninstall").await?;    Ok(())}fn ensure_codex_backend_auth(    auth: Option<&CodexAuth>,) -> Result<&CodexAuth, RemotePluginMutationError> {    let Some(auth) = auth else {        return Err(RemotePluginMutationError::AuthRequired);    };    if !auth.uses_codex_backend() {        return Err(RemotePluginMutationError::UnsupportedAuthMode);    }    Ok(auth)}async fn post_remote_plugin_mutation(    config: &RemotePluginServiceConfig,    auth: Option<&CodexAuth>,    plugin_id: &str,    action: &str,) -> Result<RemotePluginMutationResponse, RemotePluginMutationError> {    let auth = ensure_codex_backend_auth(auth)?;    let url = remote_plugin_mutation_url(config, plugin_id, action)?;    let client = build_reqwest_client();    let request = client        .post(url.clone())        .timeout(REMOTE_PLUGIN_MUTATION_TIMEOUT)        .headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers());    let response = request        .send()        .await        .map_err(|source| RemotePluginMutationError::Request {            url: url.clone(),            source,        })?;    let status = response.status();    let body = response.text().await.unwrap_or_default();    if !status.is_success() {        return Err(RemotePluginMutationError::UnexpectedStatus { url, status, body });    }    let parsed: RemotePluginMutationResponse =        serde_json::from_str(&body).map_err(|source| RemotePluginMutationError::Decode {            url: url.clone(),            source,        })?;    let expected_enabled = action == "enable";    if parsed.id != plugin_id {        return Err(RemotePluginMutationError::UnexpectedPluginId {            expected: plugin_id.to_string(),            actual: parsed.id,        });    }    if parsed.enabled != expected_enabled {        return Err(RemotePluginMutationError::UnexpectedEnabledState {            plugin_id: plugin_id.to_string(),            expected_enabled,            actual_enabled: parsed.enabled,        });    }    Ok(parsed)}fn remote_plugin_mutation_url(    config: &RemotePluginServiceConfig,    plugin_id: &str,    action: &str,) -> Result<String, RemotePluginMutationError> {    let mut url = Url::parse(config.chatgpt_base_url.trim_end_matches('/'))        .map_err(RemotePluginMutationError::InvalidBaseUrl)?;    {        let mut segments = url            .path_segments_mut()            .map_err(|()| RemotePluginMutationError::InvalidBaseUrlPath)?;        segments.pop_if_empty();        segments.push("plugins");        segments.push(plugin_id);        segments.push(action);    }    Ok(url.to_string())}