Download codex-rs/core-plugins/src/remote/share.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 18.2 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/core-plugins/src/remote/share.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/core-plugins/src/remote/share.rs
-
curl -L -o share.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/core-plugins/src/remote/share.rs
18.2 kB
| use super::*; | |
| use crate::plugin_bundle_archive::PluginBundlePackError; | |
| use crate::plugin_bundle_archive::pack_plugin_bundle_tar_gz; | |
| use codex_http_client::RouteAwareRequestBuilder; | |
| use codex_login::CodexAuth; | |
| use codex_utils_absolute_path::AbsolutePathBuf; | |
| use http::Method; | |
| use http::StatusCode; | |
| use serde::Deserialize; | |
| use serde::Serialize; | |
| use std::collections::BTreeMap; | |
| use std::io; | |
| use std::path::Path; | |
| use tracing::warn; | |
| use url::Url; | |
| mod checkout; | |
| mod local_paths; | |
| const REMOTE_PLUGIN_SHARE_MAX_ARCHIVE_BYTES: usize = 50 * 1024 * 1024; | |
| pub use checkout::checkout_remote_plugin_share; | |
| pub struct RemotePluginShareSaveResult { | |
| pub remote_plugin_id: String, | |
| pub share_url: Option<String>, | |
| pub can_publish_to_workspace: Option<bool>, | |
| } | |
| pub struct RemotePluginShareAccessPolicy { | |
| pub discoverability: Option<RemotePluginShareDiscoverability>, | |
| pub share_targets: Option<Vec<RemotePluginShareTarget>>, | |
| } | |
| pub enum RemotePluginShareDiscoverability { | |
| Listed, | |
| Unlisted, | |
| Private, | |
| } | |
| pub enum RemotePluginShareUpdateDiscoverability { | |
| Listed, | |
| Unlisted, | |
| Private, | |
| } | |
| pub enum RemotePluginSharePrincipalType { | |
| User, | |
| Group, | |
| Workspace, | |
| } | |
| pub struct RemotePluginShareTarget { | |
| pub principal_type: RemotePluginSharePrincipalType, | |
| pub principal_id: String, | |
| pub role: RemotePluginShareTargetRole, | |
| } | |
| pub struct RemotePluginSharePrincipal { | |
| pub principal_type: RemotePluginSharePrincipalType, | |
| pub principal_id: String, | |
| pub role: RemotePluginSharePrincipalRole, | |
| pub name: String, | |
| } | |
| pub enum RemotePluginShareTargetRole { | |
| Reader, | |
| Editor, | |
| } | |
| pub enum RemotePluginSharePrincipalRole { | |
| Reader, | |
| Editor, | |
| Owner, | |
| } | |
| pub struct RemotePluginShareUpdateTargetsResult { | |
| pub principals: Vec<RemotePluginSharePrincipal>, | |
| pub discoverability: RemotePluginShareDiscoverability, | |
| } | |
| struct RemoteWorkspacePluginUploadUrlRequest<'a> { | |
| filename: &'a str, | |
| mime_type: &'a str, | |
| size_bytes: usize, | |
| plugin_id: Option<&'a str>, | |
| } | |
| struct RemoteWorkspacePluginUploadUrlResponse { | |
| file_id: String, | |
| upload_url: String, | |
| etag: Option<String>, | |
| } | |
| struct RemoteWorkspacePluginCreateRequest { | |
| file_id: String, | |
| etag: String, | |
| discoverability: Option<RemotePluginShareDiscoverability>, | |
| share_targets: Option<Vec<RemotePluginShareTarget>>, | |
| } | |
| struct RemoteWorkspacePluginCreateResponse { | |
| plugin_id: String, | |
| share_url: Option<String>, | |
| can_publish_to_workspace: Option<bool>, | |
| } | |
| struct RemotePluginShareUpdateTargetsRequest { | |
| discoverability: RemotePluginShareUpdateDiscoverability, | |
| targets: Vec<RemotePluginShareTarget>, | |
| } | |
| struct RemotePluginShareUpdateTargetsResponse { | |
| principals: Vec<RemotePluginSharePrincipal>, | |
| discoverability: RemotePluginShareDiscoverability, | |
| } | |
| pub async fn save_remote_plugin_share( | |
| config: &RemotePluginServiceConfig, | |
| auth: Option<&CodexAuth>, | |
| codex_home: &Path, | |
| plugin_path: &AbsolutePathBuf, | |
| remote_plugin_id: Option<&str>, | |
| access_policy: RemotePluginShareAccessPolicy, | |
| ) -> Result<RemotePluginShareSaveResult, RemotePluginCatalogError> { | |
| let auth = ensure_chatgpt_auth(auth)?; | |
| let plugin_path_for_archive = plugin_path.as_path().to_path_buf(); | |
| let (filename, archive_bytes) = tokio::task::spawn_blocking(move || { | |
| let filename = archive_filename(&plugin_path_for_archive)?; | |
| let archive_bytes = archive_plugin_for_upload(&plugin_path_for_archive)?; | |
| Ok::<_, RemotePluginCatalogError>((filename, archive_bytes)) | |
| }) | |
| .await | |
| .map_err(RemotePluginCatalogError::ArchiveJoin)??; | |
| let upload = create_workspace_plugin_upload( | |
| config, | |
| auth, | |
| &filename, | |
| archive_bytes.len(), | |
| remote_plugin_id, | |
| ) | |
| .await?; | |
| let etag = upload | |
| .etag | |
| .ok_or(RemotePluginCatalogError::MissingUploadEtag)?; | |
| put_workspace_plugin_upload(config, &upload.upload_url, archive_bytes).await?; | |
| let share_targets = access_policy.share_targets; | |
| let share_targets = | |
| ensure_unlisted_workspace_target(auth, access_policy.discoverability, share_targets)?; | |
| let response = finalize_workspace_plugin_upload( | |
| config, | |
| auth, | |
| remote_plugin_id, | |
| RemoteWorkspacePluginCreateRequest { | |
| file_id: upload.file_id, | |
| etag, | |
| discoverability: access_policy.discoverability, | |
| share_targets, | |
| }, | |
| ) | |
| .await?; | |
| if response.plugin_id.is_empty() { | |
| return Err(RemotePluginCatalogError::UnexpectedResponse( | |
| "workspace plugin create response did not include a plugin id".to_string(), | |
| )); | |
| } | |
| if let Err(err) = local_paths::record_plugin_share_local_path( | |
| codex_home, | |
| &response.plugin_id, | |
| plugin_path.clone(), | |
| ) { | |
| warn!( | |
| remote_plugin_id = %response.plugin_id, | |
| "failed to record plugin share local path mapping: {err}" | |
| ); | |
| } | |
| Ok(RemotePluginShareSaveResult { | |
| remote_plugin_id: response.plugin_id, | |
| share_url: response.share_url, | |
| can_publish_to_workspace: response.can_publish_to_workspace, | |
| }) | |
| } | |
| pub async fn list_remote_plugin_shares( | |
| config: &RemotePluginServiceConfig, | |
| auth: Option<&CodexAuth>, | |
| codex_home: &Path, | |
| ) -> Result<Vec<RemotePluginShareSummary>, RemotePluginCatalogError> { | |
| let auth = ensure_chatgpt_auth(auth)?; | |
| let created_plugins = fetch_created_workspace_plugins(config, auth).await?; | |
| if created_plugins.is_empty() { | |
| return Ok(Vec::new()); | |
| } | |
| let installed_by_id = | |
| fetch_installed_plugins_for_scope(config, auth, RemotePluginScope::Workspace) | |
| .await? | |
| .into_iter() | |
| .map(|plugin| (plugin.plugin.id.clone(), plugin)) | |
| .collect::<BTreeMap<_, _>>(); | |
| let local_plugin_paths = | |
| local_paths::load_plugin_share_local_paths(codex_home).map_err(|err| { | |
| RemotePluginCatalogError::UnexpectedResponse(format!( | |
| "failed to load plugin share local path mapping: {err}" | |
| )) | |
| })?; | |
| created_plugins | |
| .into_iter() | |
| .map(|plugin| { | |
| let summary = build_remote_plugin_summary(&plugin, installed_by_id.get(&plugin.id))?; | |
| if summary | |
| .share_context | |
| .as_ref() | |
| .and_then(|context| context.share_principals.as_ref()) | |
| .is_none() | |
| { | |
| return Err(RemotePluginCatalogError::UnexpectedResponse(format!( | |
| "created workspace plugin `{}` did not include share_principals", | |
| plugin.id | |
| ))); | |
| } | |
| let local_plugin_path = local_plugin_paths.get(&plugin.id).cloned(); | |
| Ok(RemotePluginShareSummary { | |
| summary, | |
| local_plugin_path, | |
| }) | |
| }) | |
| .collect() | |
| } | |
| pub fn load_plugin_share_remote_ids_by_local_path( | |
| codex_home: &Path, | |
| ) -> io::Result<BTreeMap<AbsolutePathBuf, String>> { | |
| let local_paths = local_paths::load_plugin_share_local_paths(codex_home)?; | |
| local_paths | |
| .into_iter() | |
| .map(|(remote_plugin_id, local_plugin_path)| { | |
| if !is_valid_remote_plugin_id(&remote_plugin_id) { | |
| return Err(io::Error::new( | |
| io::ErrorKind::InvalidData, | |
| format!( | |
| "invalid remote plugin id in share local path mapping: {remote_plugin_id}" | |
| ), | |
| )); | |
| } | |
| Ok((local_plugin_path, remote_plugin_id)) | |
| }) | |
| .collect() | |
| } | |
| pub async fn delete_remote_plugin_share( | |
| config: &RemotePluginServiceConfig, | |
| auth: Option<&CodexAuth>, | |
| codex_home: &Path, | |
| remote_plugin_id: &str, | |
| ) -> Result<(), RemotePluginCatalogError> { | |
| let auth = ensure_chatgpt_auth(auth)?; | |
| let base_url = config.chatgpt_base_url.trim_end_matches('/'); | |
| let url = format!("{base_url}/public/plugins/workspace/{remote_plugin_id}"); | |
| let request = authenticated_request(config.http_request(Method::DELETE, &url), auth); | |
| send_and_expect_status(request, &url, &[StatusCode::NO_CONTENT]).await?; | |
| if let Err(err) = local_paths::remove_plugin_share_local_path(codex_home, remote_plugin_id) { | |
| warn!( | |
| remote_plugin_id = %remote_plugin_id, | |
| "failed to remove plugin share local path mapping: {err}" | |
| ); | |
| } | |
| Ok(()) | |
| } | |
| pub async fn update_remote_plugin_share_targets( | |
| config: &RemotePluginServiceConfig, | |
| auth: Option<&CodexAuth>, | |
| remote_plugin_id: &str, | |
| targets: Vec<RemotePluginShareTarget>, | |
| discoverability: RemotePluginShareUpdateDiscoverability, | |
| ) -> Result<RemotePluginShareUpdateTargetsResult, RemotePluginCatalogError> { | |
| let auth = ensure_chatgpt_auth(auth)?; | |
| let target_discoverability = match discoverability { | |
| RemotePluginShareUpdateDiscoverability::Listed => RemotePluginShareDiscoverability::Listed, | |
| RemotePluginShareUpdateDiscoverability::Unlisted => { | |
| RemotePluginShareDiscoverability::Unlisted | |
| } | |
| RemotePluginShareUpdateDiscoverability::Private => { | |
| RemotePluginShareDiscoverability::Private | |
| } | |
| }; | |
| let targets = | |
| ensure_unlisted_workspace_target(auth, Some(target_discoverability), Some(targets))? | |
| .unwrap_or_default(); | |
| let base_url = config.chatgpt_base_url.trim_end_matches('/'); | |
| let url = format!("{base_url}/ps/plugins/{remote_plugin_id}/shares"); | |
| let request = authenticated_request(config.http_request(Method::PUT, &url), auth).json( | |
| &RemotePluginShareUpdateTargetsRequest { | |
| discoverability, | |
| targets, | |
| }, | |
| ); | |
| let response: RemotePluginShareUpdateTargetsResponse = send_and_decode(request, &url).await?; | |
| Ok(RemotePluginShareUpdateTargetsResult { | |
| principals: response.principals, | |
| discoverability: response.discoverability, | |
| }) | |
| } | |
| fn ensure_unlisted_workspace_target( | |
| auth: &CodexAuth, | |
| discoverability: Option<RemotePluginShareDiscoverability>, | |
| targets: Option<Vec<RemotePluginShareTarget>>, | |
| ) -> Result<Option<Vec<RemotePluginShareTarget>>, RemotePluginCatalogError> { | |
| if discoverability != Some(RemotePluginShareDiscoverability::Unlisted) { | |
| return Ok(targets); | |
| } | |
| let account_id = auth.get_account_id().ok_or_else(|| { | |
| RemotePluginCatalogError::UnexpectedResponse( | |
| "workspace plugin share requires an account id".to_string(), | |
| ) | |
| })?; | |
| let mut targets = targets.unwrap_or_default(); | |
| if !targets.iter().any(|target| { | |
| target.principal_type == RemotePluginSharePrincipalType::Workspace | |
| && target.principal_id == account_id | |
| }) { | |
| targets.push(RemotePluginShareTarget { | |
| principal_type: RemotePluginSharePrincipalType::Workspace, | |
| principal_id: account_id, | |
| role: RemotePluginShareTargetRole::Reader, | |
| }); | |
| } | |
| Ok(Some(targets)) | |
| } | |
| async fn fetch_created_workspace_plugins( | |
| config: &RemotePluginServiceConfig, | |
| auth: &CodexAuth, | |
| ) -> Result<Vec<RemotePluginDirectoryItem>, RemotePluginCatalogError> { | |
| let mut plugins = Vec::new(); | |
| let mut page_token = None; | |
| loop { | |
| let response = | |
| get_created_workspace_plugins_page(config, auth, page_token.as_deref()).await?; | |
| plugins.extend(response.plugins); | |
| let Some(next_page_token) = response.pagination.next_page_token else { | |
| break; | |
| }; | |
| page_token = Some(next_page_token); | |
| } | |
| Ok(plugins) | |
| } | |
| async fn get_created_workspace_plugins_page( | |
| config: &RemotePluginServiceConfig, | |
| auth: &CodexAuth, | |
| page_token: Option<&str>, | |
| ) -> Result<RemotePluginListResponse, RemotePluginCatalogError> { | |
| let base_url = config.chatgpt_base_url.trim_end_matches('/'); | |
| let mut url = Url::parse(&format!("{base_url}/ps/plugins/workspace/created")) | |
| .map_err(RemotePluginCatalogError::InvalidBaseUrl)?; | |
| url.query_pairs_mut() | |
| .append_pair("limit", &REMOTE_PLUGIN_LIST_PAGE_LIMIT.to_string()); | |
| if let Some(page_token) = page_token { | |
| url.query_pairs_mut().append_pair("pageToken", page_token); | |
| } | |
| let url = url.to_string(); | |
| let request = authenticated_request(config.http_request(Method::GET, &url), auth); | |
| send_and_decode(request, &url).await | |
| } | |
| async fn create_workspace_plugin_upload( | |
| config: &RemotePluginServiceConfig, | |
| auth: &CodexAuth, | |
| filename: &str, | |
| size_bytes: usize, | |
| remote_plugin_id: Option<&str>, | |
| ) -> Result<RemoteWorkspacePluginUploadUrlResponse, RemotePluginCatalogError> { | |
| let base_url = config.chatgpt_base_url.trim_end_matches('/'); | |
| let url = format!("{base_url}/public/plugins/workspace/upload-url"); | |
| let request = authenticated_request(config.http_request(Method::POST, &url), auth).json( | |
| &RemoteWorkspacePluginUploadUrlRequest { | |
| filename, | |
| mime_type: "application/gzip", | |
| size_bytes, | |
| plugin_id: remote_plugin_id, | |
| }, | |
| ); | |
| send_and_decode(request, &url).await | |
| } | |
| async fn put_workspace_plugin_upload( | |
| config: &RemotePluginServiceConfig, | |
| upload_url: &str, | |
| archive_bytes: Vec<u8>, | |
| ) -> Result<(), RemotePluginCatalogError> { | |
| let request = config | |
| .http_request(Method::PUT, upload_url) | |
| .timeout(REMOTE_PLUGIN_CATALOG_TIMEOUT) | |
| .header("x-ms-blob-type", "BlockBlob") | |
| .header("Content-Type", "application/gzip") | |
| .body(archive_bytes); | |
| let response = request | |
| .send() | |
| .await | |
| .map_err(|source| RemotePluginCatalogError::Request { | |
| url: "workspace plugin upload URL".to_string(), | |
| source, | |
| })?; | |
| let status = response.status(); | |
| let body = response.text().await.unwrap_or_default(); | |
| if ![StatusCode::OK, StatusCode::CREATED].contains(&status) { | |
| return Err(RemotePluginCatalogError::UnexpectedStatus { | |
| url: "workspace plugin upload URL".to_string(), | |
| status, | |
| body, | |
| }); | |
| } | |
| Ok(()) | |
| } | |
| async fn finalize_workspace_plugin_upload( | |
| config: &RemotePluginServiceConfig, | |
| auth: &CodexAuth, | |
| remote_plugin_id: Option<&str>, | |
| body: RemoteWorkspacePluginCreateRequest, | |
| ) -> Result<RemoteWorkspacePluginCreateResponse, RemotePluginCatalogError> { | |
| let base_url = config.chatgpt_base_url.trim_end_matches('/'); | |
| let url = if let Some(remote_plugin_id) = remote_plugin_id { | |
| format!("{base_url}/public/plugins/workspace/{remote_plugin_id}") | |
| } else { | |
| format!("{base_url}/public/plugins/workspace") | |
| }; | |
| let request = authenticated_request(config.http_request(Method::POST, &url), auth).json(&body); | |
| send_and_decode(request, &url).await | |
| } | |
| fn archive_filename(plugin_path: &Path) -> Result<String, RemotePluginCatalogError> { | |
| let plugin_name = plugin_path | |
| .file_name() | |
| .and_then(|name| name.to_str()) | |
| .ok_or_else(|| RemotePluginCatalogError::InvalidPluginPath { | |
| path: plugin_path.to_path_buf(), | |
| reason: "plugin path must end in a valid UTF-8 directory name".to_string(), | |
| })?; | |
| Ok(format!("{plugin_name}.tar.gz")) | |
| } | |
| fn archive_plugin_for_upload(plugin_path: &Path) -> Result<Vec<u8>, RemotePluginCatalogError> { | |
| archive_plugin_for_upload_with_limit(plugin_path, REMOTE_PLUGIN_SHARE_MAX_ARCHIVE_BYTES) | |
| } | |
| fn archive_plugin_for_upload_with_limit( | |
| plugin_path: &Path, | |
| max_bytes: usize, | |
| ) -> Result<Vec<u8>, RemotePluginCatalogError> { | |
| pack_plugin_bundle_tar_gz(plugin_path, max_bytes).map_err(|err| match err { | |
| PluginBundlePackError::InvalidPluginPath { path, reason } => { | |
| RemotePluginCatalogError::InvalidPluginPath { path, reason } | |
| } | |
| PluginBundlePackError::ArchiveTooLarge { bytes, max_bytes } => { | |
| RemotePluginCatalogError::ArchiveTooLarge { bytes, max_bytes } | |
| } | |
| PluginBundlePackError::Io { source } => RemotePluginCatalogError::Archive { | |
| path: plugin_path.to_path_buf(), | |
| source, | |
| }, | |
| }) | |
| } | |
| async fn send_and_expect_status( | |
| request: RouteAwareRequestBuilder, | |
| url_for_error: &str, | |
| expected_statuses: &[StatusCode], | |
| ) -> Result<(), RemotePluginCatalogError> { | |
| let response = request | |
| .send() | |
| .await | |
| .map_err(|source| RemotePluginCatalogError::Request { | |
| url: url_for_error.to_string(), | |
| source, | |
| })?; | |
| let status = response.status(); | |
| let body = response.text().await.unwrap_or_default(); | |
| if !expected_statuses.contains(&status) { | |
| return Err(RemotePluginCatalogError::UnexpectedStatus { | |
| url: url_for_error.to_string(), | |
| status, | |
| body, | |
| }); | |
| } | |
| Ok(()) | |
| } | |
| mod tests; | |