Codex Handbook
core-plugins/src/remote_bundle.rs 976 lines
use crate::plugin_bundle_archive::PluginBundleUnpackError;use crate::plugin_bundle_archive::unpack_plugin_bundle_tar_gz;use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;use crate::store::PluginInstallResult;use crate::store::PluginStore;use crate::store::PluginStoreError;use crate::store::validate_plugin_version_segment;use codex_login::default_client::build_reqwest_client;use codex_plugin::PluginId;use codex_plugin::PluginIdError;use codex_utils_absolute_path::AbsolutePathBuf;use codex_utils_plugins::find_plugin_manifest_path;use reqwest::Response;use reqwest::StatusCode;use serde_json::Value as JsonValue;use std::fs;use std::io;use std::path::Path;use std::path::PathBuf;use std::time::Duration;use url::Host;use url::Url;const REMOTE_PLUGIN_BUNDLE_DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(60);const REMOTE_PLUGIN_BUNDLE_MAX_DOWNLOAD_BYTES: u64 = 50 * 1024 * 1024;const REMOTE_PLUGIN_BUNDLE_ERROR_BODY_MAX_BYTES: u64 = 8 * 1024;const REMOTE_PLUGIN_BUNDLE_MAX_EXTRACTED_BYTES: u64 = 250 * 1024 * 1024;const REMOTE_PLUGIN_INSTALL_STAGING_DIR: &str = "plugins/.remote-plugin-install-staging";#[cfg(debug_assertions)]const TEST_ALLOW_LOOPBACK_HTTP_REMOTE_PLUGIN_BUNDLES_ENV: &str =    "CODEX_TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS";#[derive(Debug, Clone)]pub struct ValidatedRemotePluginBundle {    pub plugin_id: PluginId,    pub plugin_version: String,    app_manifest: Option<JsonValue>,    bundle_download_url: String,}#[derive(Debug, thiserror::Error)]pub enum RemotePluginBundleInstallError {    #[error("backend did not return a release version for remote plugin `{remote_plugin_id}`")]    MissingReleaseVersion { remote_plugin_id: String },    #[error(        "backend returned an invalid release version for remote plugin `{remote_plugin_id}`: {message}"    )]    InvalidReleaseVersion {        remote_plugin_id: String,        message: String,    },    #[error("backend did not return a download URL for remote plugin `{remote_plugin_id}`")]    MissingBundleDownloadUrl { remote_plugin_id: String },    #[error(        "backend returned an invalid download URL for remote plugin `{remote_plugin_id}`: {url}"    )]    InvalidBundleDownloadUrl {        remote_plugin_id: String,        url: String,        #[source]        source: url::ParseError,    },    #[error(        "backend returned an unsupported download URL scheme for remote plugin `{remote_plugin_id}`: {scheme}"    )]    UnsupportedBundleDownloadUrlScheme {        remote_plugin_id: String,        scheme: String,    },    #[error(        "backend returned an invalid local plugin id for remote plugin `{remote_plugin_id}`: {source}"    )]    InvalidPluginId {        remote_plugin_id: String,        #[source]        source: PluginIdError,    },    #[error("failed to send remote plugin bundle download request to {url}: {source}")]    DownloadRequest {        url: String,        #[source]        source: reqwest::Error,    },    #[error("remote plugin bundle download from {url} failed with status {status}: {body}")]    DownloadStatus {        url: String,        status: StatusCode,        body: String,    },    #[error("failed to read remote plugin bundle download response from {url}: {source}")]    DownloadBody {        url: String,        #[source]        source: reqwest::Error,    },    #[error("remote plugin bundle download from {url} exceeded maximum size of {max_bytes} bytes")]    DownloadTooLarge { url: String, max_bytes: u64 },    #[error("remote plugin bundle download from {url} redirected to unsupported URL {final_url}")]    UnsupportedBundleDownloadFinalUrl { url: String, final_url: String },    #[error(        "remote plugin bundle extracted size would be {bytes} bytes, exceeding the maximum total size of {max_bytes} bytes"    )]    ExtractedBundleTooLarge { bytes: u64, max_bytes: u64 },    #[error("{context}: {source}")]    Io {        context: &'static str,        #[source]        source: io::Error,    },    #[error("{0}")]    InvalidBundle(String),    #[error("{0}")]    Store(#[from] PluginStoreError),}impl RemotePluginBundleInstallError {    fn io(context: &'static str, source: io::Error) -> Self {        Self::Io { context, source }    }}pub fn validate_remote_plugin_bundle(    remote_plugin_id: &str,    remote_marketplace_name: &str,    plugin_name: &str,    release_version: Option<&str>,    bundle_download_url: Option<&str>,    app_manifest: Option<JsonValue>,) -> Result<ValidatedRemotePluginBundle, RemotePluginBundleInstallError> {    let plugin_id = PluginId::new(plugin_name.to_string(), remote_marketplace_name.to_string())        .map_err(|source| RemotePluginBundleInstallError::InvalidPluginId {            remote_plugin_id: remote_plugin_id.to_string(),            source,        })?;    let plugin_version = release_version        .map(str::trim)        .filter(|version| !version.is_empty())        .ok_or_else(|| RemotePluginBundleInstallError::MissingReleaseVersion {            remote_plugin_id: remote_plugin_id.to_string(),        })?        .to_string();    validate_plugin_version_segment(&plugin_version).map_err(|message| {        RemotePluginBundleInstallError::InvalidReleaseVersion {            remote_plugin_id: remote_plugin_id.to_string(),            message,        }    })?;    let bundle_download_url = bundle_download_url        .map(str::trim)        .filter(|url| !url.is_empty())        .ok_or_else(            || RemotePluginBundleInstallError::MissingBundleDownloadUrl {                remote_plugin_id: remote_plugin_id.to_string(),            },        )?        .to_string();    let parsed_bundle_url = Url::parse(&bundle_download_url).map_err(|source| {        RemotePluginBundleInstallError::InvalidBundleDownloadUrl {            remote_plugin_id: remote_plugin_id.to_string(),            url: bundle_download_url.clone(),            source,        }    })?;    if !is_allowed_bundle_download_url(        &parsed_bundle_url,        allow_test_loopback_http_bundle_downloads(),    ) {        return Err(            RemotePluginBundleInstallError::UnsupportedBundleDownloadUrlScheme {                remote_plugin_id: remote_plugin_id.to_string(),                scheme: parsed_bundle_url.scheme().to_string(),            },        );    }    Ok(ValidatedRemotePluginBundle {        plugin_id,        plugin_version,        app_manifest,        bundle_download_url,    })}fn allow_test_loopback_http_bundle_downloads() -> bool {    #[cfg(debug_assertions)]    {        if let Ok(value) = std::env::var(TEST_ALLOW_LOOPBACK_HTTP_REMOTE_PLUGIN_BUNDLES_ENV) {            return value == "1";        }    }    false}fn is_allowed_bundle_download_url(url: &Url, allow_loopback_http: bool) -> bool {    match url.scheme() {        "https" => true,        "http" => allow_loopback_http && is_loopback_url(url),        _ => false,    }}fn is_loopback_url(url: &Url) -> bool {    match url.host() {        Some(Host::Ipv4(addr)) => addr.is_loopback(),        Some(Host::Ipv6(addr)) => addr.is_loopback(),        Some(Host::Domain(host)) => host.eq_ignore_ascii_case("localhost"),        None => false,    }}pub async fn download_and_install_remote_plugin_bundle(    codex_home: PathBuf,    bundle: ValidatedRemotePluginBundle,) -> Result<PluginInstallResult, RemotePluginBundleInstallError> {    let bundle_bytes = download_remote_plugin_bundle_with_limit(        &bundle.bundle_download_url,        /*max_bytes*/ REMOTE_PLUGIN_BUNDLE_MAX_DOWNLOAD_BYTES,    )    .await?;    tokio::task::spawn_blocking(move || {        install_remote_plugin_bundle(codex_home, bundle, bundle_bytes)    })    .await    .map_err(|err| {        RemotePluginBundleInstallError::InvalidBundle(format!(            "failed to join remote plugin bundle install task: {err}"        ))    })?}pub(crate) async fn download_and_extract_remote_plugin_bundle_to_path(    bundle: ValidatedRemotePluginBundle,    destination: AbsolutePathBuf,) -> Result<AbsolutePathBuf, RemotePluginBundleInstallError> {    let bundle_bytes = download_remote_plugin_bundle_with_limit(        &bundle.bundle_download_url,        /*max_bytes*/ REMOTE_PLUGIN_BUNDLE_MAX_DOWNLOAD_BYTES,    )    .await?;    tokio::task::spawn_blocking(move || {        extract_remote_plugin_bundle_to_path(bundle, bundle_bytes, destination)    })    .await    .map_err(|err| {        RemotePluginBundleInstallError::InvalidBundle(format!(            "failed to join remote plugin bundle extraction task: {err}"        ))    })?}async fn download_remote_plugin_bundle_with_limit(    bundle_download_url: &str,    max_bytes: u64,) -> Result<Vec<u8>, RemotePluginBundleInstallError> {    let client = build_reqwest_client();    let response = client        .get(bundle_download_url)        .timeout(REMOTE_PLUGIN_BUNDLE_DOWNLOAD_TIMEOUT)        .send()        .await        .map_err(|source| RemotePluginBundleInstallError::DownloadRequest {            url: bundle_download_url.to_string(),            source,        })?;    let final_url = response.url().clone();    // reqwest may already have followed redirects here. For backend-issued bundle URLs, keep the    // shared client policy and fail unsupported final schemes before caching.    if !is_allowed_bundle_download_url(&final_url, allow_test_loopback_http_bundle_downloads()) {        return Err(            RemotePluginBundleInstallError::UnsupportedBundleDownloadFinalUrl {                url: bundle_download_url.to_string(),                final_url: final_url.to_string(),            },        );    }    let url = final_url.to_string();    let status = response.status();    if !status.is_success() {        let body = read_response_body_with_limit(            response,            &url,            /*max_bytes*/ REMOTE_PLUGIN_BUNDLE_ERROR_BODY_MAX_BYTES,        )        .await?;        let body = String::from_utf8_lossy(&body).to_string();        return Err(RemotePluginBundleInstallError::DownloadStatus { url, status, body });    }    read_response_body_with_limit(response, &url, max_bytes).await}async fn read_response_body_with_limit(    mut response: Response,    url: &str,    max_bytes: u64,) -> Result<Vec<u8>, RemotePluginBundleInstallError> {    if let Some(content_length) = response.content_length() {        enforce_download_size_limit(url, content_length, max_bytes)?;    }    let mut body = Vec::new();    while let Some(chunk) =        response            .chunk()            .await            .map_err(|source| RemotePluginBundleInstallError::DownloadBody {                url: url.to_string(),                source,            })?    {        let next_len = body.len() as u64 + chunk.len() as u64;        enforce_download_size_limit(url, next_len, max_bytes)?;        body.extend_from_slice(&chunk);    }    Ok(body)}fn enforce_download_size_limit(    url: &str,    bytes: u64,    max_bytes: u64,) -> Result<(), RemotePluginBundleInstallError> {    if bytes > max_bytes {        return Err(RemotePluginBundleInstallError::DownloadTooLarge {            url: url.to_string(),            max_bytes,        });    }    Ok(())}fn install_remote_plugin_bundle(    codex_home: PathBuf,    bundle: ValidatedRemotePluginBundle,    bundle_bytes: Vec<u8>,) -> Result<PluginInstallResult, RemotePluginBundleInstallError> {    let staging_root = codex_home.join(REMOTE_PLUGIN_INSTALL_STAGING_DIR);    fs::create_dir_all(&staging_root).map_err(|source| {        RemotePluginBundleInstallError::io(            "failed to create remote plugin bundle staging directory",            source,        )    })?;    let extract_dir = tempfile::Builder::new()        .prefix("remote-plugin-bundle-")        .tempdir_in(&staging_root)        .map_err(|source| {            RemotePluginBundleInstallError::io(                "failed to create remote plugin bundle extraction directory",                source,            )        })?;    extract_plugin_bundle_tar_gz(&bundle_bytes, extract_dir.path())?;    let plugin_root = find_extracted_plugin_root(extract_dir.path())?;    prepare_extracted_remote_plugin_root(&plugin_root, &bundle)?;    let plugin_root = AbsolutePathBuf::try_from(plugin_root).map_err(|err| {        RemotePluginBundleInstallError::InvalidBundle(format!(            "failed to resolve extracted remote plugin bundle root: {err}"        ))    })?;    let store = PluginStore::try_new(codex_home)?;    store        .install_with_version(plugin_root, bundle.plugin_id, bundle.plugin_version)        .map_err(RemotePluginBundleInstallError::from)}fn extract_remote_plugin_bundle_to_path(    bundle: ValidatedRemotePluginBundle,    bundle_bytes: Vec<u8>,    destination: AbsolutePathBuf,) -> Result<AbsolutePathBuf, RemotePluginBundleInstallError> {    if destination.as_path().exists() {        return Err(RemotePluginBundleInstallError::InvalidBundle(format!(            "plugin checkout destination already exists: {}",            destination.display()        )));    }    let parent = destination.as_path().parent().ok_or_else(|| {        RemotePluginBundleInstallError::InvalidBundle(format!(            "plugin checkout destination has no parent: {}",            destination.display()        ))    })?;    fs::create_dir_all(parent).map_err(|source| {        RemotePluginBundleInstallError::io("failed to create plugin checkout directory", source)    })?;    let extract_dir = tempfile::Builder::new()        .prefix("remote-plugin-checkout-")        .tempdir_in(parent)        .map_err(|source| {            RemotePluginBundleInstallError::io(                "failed to create remote plugin bundle extraction directory",                source,            )        })?;    extract_plugin_bundle_tar_gz(&bundle_bytes, extract_dir.path())?;    let plugin_root = find_extracted_plugin_root(extract_dir.path())?;    let manifest = crate::manifest::load_plugin_manifest(&plugin_root).ok_or_else(|| {        RemotePluginBundleInstallError::InvalidBundle(            "remote plugin bundle did not contain a valid plugin.json".to_string(),        )    })?;    if manifest.name != bundle.plugin_id.plugin_name {        return Err(RemotePluginBundleInstallError::InvalidBundle(format!(            "plugin.json name `{}` does not match remote plugin name `{}`",            manifest.name, bundle.plugin_id.plugin_name        )));    }    let staged_path = extract_dir.keep();    fs::rename(&staged_path, destination.as_path()).map_err(|source| {        RemotePluginBundleInstallError::io(            "failed to activate checked out plugin directory",            source,        )    })?;    Ok(destination)}fn prepare_extracted_remote_plugin_root(    plugin_root: &Path,    bundle: &ValidatedRemotePluginBundle,) -> Result<(), RemotePluginBundleInstallError> {    if bundle.plugin_id.marketplace_name != REMOTE_GLOBAL_MARKETPLACE_NAME {        return Ok(());    }    overwrite_plugin_manifest_version(plugin_root, &bundle.plugin_version)?;    if let Some(app_manifest) = &bundle.app_manifest {        overwrite_plugin_app_manifest(plugin_root, app_manifest)?;    }    Ok(())}fn overwrite_plugin_manifest_version(    plugin_root: &Path,    plugin_version: &str,) -> Result<(), RemotePluginBundleInstallError> {    let manifest_path = find_plugin_manifest_path(plugin_root).ok_or_else(|| {        RemotePluginBundleInstallError::InvalidBundle(            "remote plugin bundle did not contain a valid plugin.json".to_string(),        )    })?;    let contents = fs::read_to_string(&manifest_path).map_err(|source| {        RemotePluginBundleInstallError::io("failed to read remote plugin manifest", source)    })?;    let mut manifest: JsonValue = serde_json::from_str(&contents).map_err(|err| {        RemotePluginBundleInstallError::InvalidBundle(format!(            "failed to parse remote plugin manifest: {err}"        ))    })?;    let Some(manifest_object) = manifest.as_object_mut() else {        return Err(RemotePluginBundleInstallError::InvalidBundle(            "remote plugin manifest must be a JSON object".to_string(),        ));    };    manifest_object.insert(        "version".to_string(),        JsonValue::String(plugin_version.to_string()),    );    write_json_file(        &manifest_path,        &manifest,        "failed to write remote plugin manifest",    )}fn overwrite_plugin_app_manifest(    plugin_root: &Path,    app_manifest: &JsonValue,) -> Result<(), RemotePluginBundleInstallError> {    let app_manifest_path = crate::manifest::load_plugin_manifest(plugin_root)        .and_then(|manifest| manifest.paths.apps.map(|path| path.to_path_buf()))        .unwrap_or_else(|| plugin_root.join(".app.json"));    write_json_file(        &app_manifest_path,        app_manifest,        "failed to write remote plugin app manifest",    )}fn write_json_file(    path: &Path,    value: &JsonValue,    context: &'static str,) -> Result<(), RemotePluginBundleInstallError> {    let parent = path.parent().ok_or_else(|| {        RemotePluginBundleInstallError::InvalidBundle(format!(            "remote plugin output path has no parent: {}",            path.display()        ))    })?;    fs::create_dir_all(parent)        .map_err(|source| RemotePluginBundleInstallError::io(context, source))?;    let mut contents = serde_json::to_vec_pretty(value).map_err(|err| {        RemotePluginBundleInstallError::InvalidBundle(format!(            "failed to serialize remote plugin JSON override: {err}"        ))    })?;    contents.push(b'\n');    fs::write(path, contents).map_err(|source| RemotePluginBundleInstallError::io(context, source))}fn extract_plugin_bundle_tar_gz(    bytes: &[u8],    destination: &Path,) -> Result<(), RemotePluginBundleInstallError> {    extract_plugin_bundle_tar_gz_with_limits(        bytes,        destination,        REMOTE_PLUGIN_BUNDLE_MAX_EXTRACTED_BYTES,    )}fn extract_plugin_bundle_tar_gz_with_limits(    bytes: &[u8],    destination: &Path,    max_total_bytes: u64,) -> Result<(), RemotePluginBundleInstallError> {    unpack_plugin_bundle_tar_gz(bytes, destination, max_total_bytes).map_err(|err| match err {        PluginBundleUnpackError::ExtractedBundleTooLarge { bytes, max_bytes } => {            RemotePluginBundleInstallError::ExtractedBundleTooLarge { bytes, max_bytes }        }        PluginBundleUnpackError::Io { context, source } => {            RemotePluginBundleInstallError::io(context, source)        }        PluginBundleUnpackError::InvalidBundle(message) => {            RemotePluginBundleInstallError::InvalidBundle(message)        }    })}fn find_extracted_plugin_root(    extraction_root: &Path,) -> Result<PathBuf, RemotePluginBundleInstallError> {    if is_standard_plugin_root(extraction_root) {        return Ok(extraction_root.to_path_buf());    }    Err(RemotePluginBundleInstallError::InvalidBundle(        "remote plugin bundle did not contain a standard plugin root with plugin.json".to_string(),    ))}fn is_standard_plugin_root(path: &Path) -> bool {    find_plugin_manifest_path(path).is_some()}#[cfg(test)]mod tests {    use super::*;    use flate2::Compression;    use flate2::write::GzEncoder;    use pretty_assertions::assert_eq;    use std::io::Write;    use tempfile::tempdir;    const REMOTE_PLUGIN_ID: &str = "plugins~Plugin_00000000000000000000000000000000";    #[test]    fn validate_remote_plugin_bundle_uses_detail_name_for_local_plugin_id() {        let bundle = validate_remote_plugin_bundle(            REMOTE_PLUGIN_ID,            "openai-curated-remote",            "linear",            Some("1.2.3"),            Some("https://example.com/linear.tar.gz"),            /*app_manifest*/ None,        )        .expect("valid install plan");        assert_eq!(bundle.plugin_id.plugin_name, "linear");        assert_eq!(bundle.plugin_id.marketplace_name, "openai-curated-remote");        assert_eq!(bundle.plugin_version, "1.2.3");        assert_eq!(            bundle.bundle_download_url.as_str(),            "https://example.com/linear.tar.gz"        );    }    #[test]    fn validate_remote_plugin_bundle_rejects_missing_release_version() {        let err = validate_remote_plugin_bundle(            REMOTE_PLUGIN_ID,            "openai-curated-remote",            "linear",            /*release_version*/ None,            Some("https://example.com/linear.tar.gz"),            /*app_manifest*/ None,        )        .expect_err("missing release version should be rejected");        assert!(matches!(            err,            RemotePluginBundleInstallError::MissingReleaseVersion { .. }        ));    }    #[test]    fn validate_remote_plugin_bundle_rejects_invalid_release_version() {        let err = validate_remote_plugin_bundle(            REMOTE_PLUGIN_ID,            "openai-curated-remote",            "linear",            Some("../1.2.3"),            Some("https://example.com/linear.tar.gz"),            /*app_manifest*/ None,        )        .expect_err("invalid release version should be rejected");        assert!(matches!(            err,            RemotePluginBundleInstallError::InvalidReleaseVersion { .. }        ));    }    #[test]    fn validate_remote_plugin_bundle_rejects_missing_download_url() {        let err = validate_remote_plugin_bundle(            REMOTE_PLUGIN_ID,            "openai-curated-remote",            "linear",            Some("1.2.3"),            /*bundle_download_url*/ None,            /*app_manifest*/ None,        )        .expect_err("missing bundle download URL should be rejected");        assert!(matches!(            err,            RemotePluginBundleInstallError::MissingBundleDownloadUrl { .. }        ));    }    #[test]    fn validate_remote_plugin_bundle_rejects_unsupported_download_url_scheme() {        let err = validate_remote_plugin_bundle(            REMOTE_PLUGIN_ID,            "openai-curated-remote",            "linear",            Some("1.2.3"),            Some("http://example.com/linear.tar.gz"),            /*app_manifest*/ None,        )        .expect_err("plain HTTP URLs should be rejected before cloud install");        assert!(matches!(            err,            RemotePluginBundleInstallError::UnsupportedBundleDownloadUrlScheme { .. }        ));    }    #[test]    fn download_size_limit_rejects_oversized_bundle() {        let err = enforce_download_size_limit(            "https://example.com/linear.tar.gz",            /*bytes*/ 5,            /*max_bytes*/ 4,        )        .expect_err("oversized bundle download should fail");        assert!(matches!(            err,            RemotePluginBundleInstallError::DownloadTooLarge { .. }        ));    }    #[test]    fn install_rejects_invalid_tar_gz_bundle() {        let codex_home = tempdir().expect("tempdir");        let bundle = valid_remote_plugin_bundle();        let err = install_remote_plugin_bundle(            codex_home.path().to_path_buf(),            bundle,            b"not a tar.gz".to_vec(),        )        .expect_err("invalid tar.gz should be rejected");        assert!(format!("{err}").contains("failed to read plugin bundle tar"));    }    #[test]    fn install_rejects_bundle_without_standard_plugin_root() {        let codex_home = tempdir().expect("tempdir");        let bundle = valid_remote_plugin_bundle();        let err = install_remote_plugin_bundle(            codex_home.path().to_path_buf(),            bundle,            tar_gz_bytes(&[("README.md", b"missing plugin manifest", /*mode*/ 0o644)]),        )        .expect_err("bundle without plugin root should be rejected");        assert!(            format!("{err}").contains("did not contain a standard plugin root with plugin.json")        );    }    #[test]    fn install_preserves_non_global_bundle_manifest_metadata() {        let codex_home = tempdir().expect("tempdir");        let bundle = validate_remote_plugin_bundle(            REMOTE_PLUGIN_ID,            "workspace-shared-with-me",            "linear",            Some("backend-version"),            Some("https://example.com/linear.tar.gz"),            Some(serde_json::json!({                "apps": {                    "remote": {                        "id": "remote-app"                    }                }            })),        )        .expect("valid install plan");        let result = install_remote_plugin_bundle(            codex_home.path().to_path_buf(),            bundle,            tar_gz_bytes(&[                (                    ".codex-plugin/plugin.json",                    br#"{"name":"linear","version":"bundle-version"}"#,                    /*mode*/ 0o644,                ),                (                    ".app.json",                    br#"{"apps":{"bundled":{"id":"bundled-app"}}}"#,                    /*mode*/ 0o644,                ),            ]),        )        .expect("install bundle");        assert_eq!(result.plugin_version, "backend-version");        let installed_manifest: JsonValue = serde_json::from_str(            &std::fs::read_to_string(                result                    .installed_path                    .join(".codex-plugin/plugin.json")                    .as_path(),            )            .expect("read installed plugin manifest"),        )        .expect("parse installed plugin manifest");        assert_eq!(            installed_manifest,            serde_json::json!({                "name": "linear",                "version": "bundle-version",            })        );        let installed_app_manifest: JsonValue = serde_json::from_str(            &std::fs::read_to_string(result.installed_path.join(".app.json").as_path())                .expect("read installed app manifest"),        )        .expect("parse installed app manifest");        assert_eq!(            installed_app_manifest,            serde_json::json!({                "apps": {                    "bundled": {                        "id": "bundled-app",                    },                },            })        );    }    #[test]    fn find_extracted_plugin_root_uses_local_manifest_discovery() {        let extraction_root = tempdir().expect("tempdir");        std::fs::create_dir_all(extraction_root.path().join(".codex-plugin"))            .expect("create manifest dir");        std::fs::write(            extraction_root.path().join(".codex-plugin/plugin.json"),            r#"{"name":"linear"}"#,        )        .expect("write manifest");        assert_eq!(            find_extracted_plugin_root(extraction_root.path()).expect("plugin root"),            extraction_root.path()        );    }    #[test]    fn find_extracted_plugin_root_rejects_nested_plugin_root() {        let extraction_root = tempdir().expect("tempdir");        let plugin_root = extraction_root.path().join("linear");        std::fs::create_dir_all(plugin_root.join(".codex-plugin")).expect("create manifest dir");        std::fs::write(            plugin_root.join(".codex-plugin/plugin.json"),            r#"{"name":"linear"}"#,        )        .expect("write manifest");        let err = find_extracted_plugin_root(extraction_root.path())            .expect_err("nested plugin root should be rejected");        assert!(            format!("{err}").contains("did not contain a standard plugin root with plugin.json")        );    }    #[test]    fn extraction_rejects_tar_path_traversal() {        let destination = tempdir().expect("tempdir");        let err = extract_plugin_bundle_tar_gz(            &tar_gz_bytes_with_raw_path("../evil.txt", b"evil", /*mode*/ 0o644),            destination.path(),        )        .expect_err("tar path traversal should be rejected");        assert!(format!("{err}").contains("escapes extraction root"));    }    #[test]    fn extraction_rejects_total_size_over_limit() {        let destination = tempdir().expect("tempdir");        let err = extract_plugin_bundle_tar_gz_with_limits(            &tar_gz_bytes(&[                ("a.txt", b"1234", /*mode*/ 0o644),                ("b.txt", b"5678", /*mode*/ 0o644),            ]),            destination.path(),            /*max_total_bytes*/ 6,        )        .expect_err("oversized extracted bundle should be rejected");        assert!(matches!(            err,            RemotePluginBundleInstallError::ExtractedBundleTooLarge { .. }        ));    }    #[test]    fn extraction_supports_gnu_long_name_entries() {        let destination = tempdir().expect("tempdir");        let long_path = format!("{}/file.txt", ["segment"; 40].join("/"));        extract_plugin_bundle_tar_gz(            &tar_gz_bytes(&[(long_path.as_str(), b"long", /*mode*/ 0o644)]),            destination.path(),        )        .expect("extract bundle with GNU long name entry");        assert_eq!(            std::fs::read(destination.path().join(long_path)).expect("read extracted file"),            b"long"        );    }    #[cfg(unix)]    #[test]    fn extraction_preserves_executable_permissions() {        use std::os::unix::fs::PermissionsExt;        let destination = tempdir().expect("tempdir");        extract_plugin_bundle_tar_gz(            &tar_gz_bytes(&[                (                    ".codex-plugin/plugin.json",                    b"{\"name\":\"linear\"}",                    /*mode*/ 0o644,                ),                ("bin/helper", b"#!/bin/sh\n", /*mode*/ 0o755),            ]),            destination.path(),        )        .expect("extract bundle");        let mode = std::fs::metadata(destination.path().join("bin/helper"))            .expect("helper metadata")            .permissions()            .mode()            & 0o777;        assert_eq!(mode, 0o755);    }    fn valid_remote_plugin_bundle() -> ValidatedRemotePluginBundle {        validate_remote_plugin_bundle(            REMOTE_PLUGIN_ID,            "openai-curated-remote",            "linear",            Some("1.2.3"),            Some("https://example.com/linear.tar.gz"),            /*app_manifest*/ None,        )        .expect("valid install plan")    }    fn tar_gz_bytes(entries: &[(&str, &[u8], u32)]) -> Vec<u8> {        let encoder = GzEncoder::new(Vec::new(), Compression::default());        let mut tar = tar::Builder::new(encoder);        for (path, contents, mode) in entries {            append_tar_entry(&mut tar, tar::EntryType::Regular, path, contents, *mode);        }        finish_tar_gz(tar)    }    fn tar_gz_bytes_with_raw_path(path: &str, contents: &[u8], mode: u32) -> Vec<u8> {        let mut header = tar::Header::new_gnu();        header.set_entry_type(tar::EntryType::Regular);        header.set_size(contents.len() as u64);        header.set_mode(mode);        header.as_mut_bytes()[..path.len()].copy_from_slice(path.as_bytes());        header.set_cksum();        let mut encoder = GzEncoder::new(Vec::new(), Compression::default());        encoder            .write_all(header.as_bytes())            .expect("write tar header");        encoder.write_all(contents).expect("write tar contents");        let padding = (512 - (contents.len() % 512)) % 512;        encoder            .write_all(&vec![0; padding])            .expect("write tar padding");        encoder.write_all(&[0; 1024]).expect("write tar terminator");        encoder.finish().expect("finish gzip")    }    fn append_tar_entry<W: std::io::Write>(        tar: &mut tar::Builder<W>,        entry_type: tar::EntryType,        path: &str,        contents: &[u8],        mode: u32,    ) {        let mut header = tar::Header::new_gnu();        header.set_entry_type(entry_type);        header.set_size(contents.len() as u64);        header.set_mode(mode);        header.set_cksum();        if let Err(error) = tar.append_data(&mut header, path, contents) {            panic!("failed to append tar test data: {error}");        }    }    fn finish_tar_gz(tar: tar::Builder<GzEncoder<Vec<u8>>>) -> Vec<u8> {        let encoder = match tar.into_inner() {            Ok(encoder) => encoder,            Err(error) => panic!("failed to finish tar test data: {error}"),        };        match encoder.finish() {            Ok(bytes) => bytes,            Err(error) => panic!("failed to finish gzip test data: {error}"),        }    }}