feat: Add plugin share checkout (#22435)

Adds plugin/share/checkout to turn a shared remote plugin into a local
working copy under ~/plugins/<name>.

Registers the copy in the managed personal marketplace and records the
remote-to-local mapping for later share/save flows.

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
xl-openai
2026-05-13 00:50:29 -07:00
committed by GitHub
Unverified
parent 392e94e9ea
commit 2a67c46de4
21 changed files with 1384 additions and 2 deletions
@@ -239,6 +239,26 @@ pub async fn download_and_install_remote_plugin_bundle(
})?
}
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,
@@ -359,6 +379,63 @@ fn install_remote_plugin_bundle(
.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 extract_plugin_bundle_tar_gz(
bytes: &[u8],
destination: &Path,