mirror of
https://github.com/openai/codex.git
synced 2026-05-04 19:36:45 +00:00
feat: Add workspace plugin sharing APIs (#20278)
1. Adds v2 plugin/share/save, plugin/share/list, and plugin/share/delete RPCs. 2. Implements save by archiving a local plugin root, enforcing a size limit, uploading through the workspace upload flow, and supporting updates via remotePluginId. 3. Lists created workspace plugins 4. Deletes a previously uploaded/shared plugin.
This commit is contained in:
@@ -26,6 +26,7 @@ use wiremock::matchers::path;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const REMOTE_PLUGIN_ID: &str = "plugins~Plugin_linear";
|
||||
const WORKSPACE_REMOTE_PLUGIN_ID: &str = "plugins_69f27c3e67848191a45cbaa5f2adb39d";
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_uninstall_removes_plugin_cache_and_config_entry() -> Result<()> {
|
||||
@@ -323,6 +324,79 @@ async fn plugin_uninstall_uses_detail_scope_for_cache_namespace() -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_uninstall_accepts_workspace_remote_plugin_id_shape() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
write_remote_plugin_catalog_config(
|
||||
codex_home.path(),
|
||||
&format!("{}/backend-api/", server.uri()),
|
||||
)?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account-123")
|
||||
.chatgpt_user_id("user-123")
|
||||
.chatgpt_account_id("account-123"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
mount_remote_plugin_detail_with_name(
|
||||
&server,
|
||||
WORKSPACE_REMOTE_PLUGIN_ID,
|
||||
"skill-improver",
|
||||
"1.0.0",
|
||||
"WORKSPACE",
|
||||
)
|
||||
.await;
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path(format!(
|
||||
"/backend-api/plugins/{WORKSPACE_REMOTE_PLUGIN_ID}/uninstall"
|
||||
)))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.and(header("chatgpt-account-id", "account-123"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string(format!(
|
||||
r#"{{"id":"{WORKSPACE_REMOTE_PLUGIN_ID}","enabled":false}}"#
|
||||
)))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let remote_plugin_cache_root = codex_home
|
||||
.path()
|
||||
.join("plugins/cache/chatgpt-workspace/skill-improver");
|
||||
std::fs::create_dir_all(remote_plugin_cache_root.join("1.0.0/.codex-plugin"))?;
|
||||
std::fs::write(
|
||||
remote_plugin_cache_root.join("1.0.0/.codex-plugin/plugin.json"),
|
||||
r#"{"name":"skill-improver","version":"1.0.0"}"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_uninstall_request(PluginUninstallParams {
|
||||
plugin_id: WORKSPACE_REMOTE_PLUGIN_ID.to_string(),
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginUninstallResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(response, PluginUninstallResponse {});
|
||||
wait_for_remote_plugin_request_count(
|
||||
&server,
|
||||
"POST",
|
||||
&format!("/plugins/{WORKSPACE_REMOTE_PLUGIN_ID}/uninstall"),
|
||||
/*expected_count*/ 1,
|
||||
)
|
||||
.await?;
|
||||
assert!(!remote_plugin_cache_root.exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_uninstall_rejects_before_post_when_remote_detail_fetch_fails() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -380,7 +454,7 @@ async fn plugin_uninstall_rejects_before_post_when_remote_detail_fetch_fails() -
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_uninstall_rejects_malformed_local_plugin_id_before_remote_path() -> Result<()> {
|
||||
async fn plugin_uninstall_rejects_invalid_plugin_id_before_remote_path() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
write_remote_plugin_catalog_config(
|
||||
@@ -392,7 +466,7 @@ async fn plugin_uninstall_rejects_malformed_local_plugin_id_before_remote_path()
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_uninstall_request(PluginUninstallParams {
|
||||
plugin_id: "sample-plugin".to_string(),
|
||||
plugin_id: "sample plugin".to_string(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -407,7 +481,7 @@ async fn plugin_uninstall_rejects_malformed_local_plugin_id_before_remote_path()
|
||||
wait_for_remote_plugin_request_count(
|
||||
&server,
|
||||
"POST",
|
||||
"/plugins/sample-plugin/uninstall",
|
||||
"/plugins/sample plugin/uninstall",
|
||||
/*expected_count*/ 0,
|
||||
)
|
||||
.await?;
|
||||
@@ -519,11 +593,28 @@ async fn mount_remote_plugin_detail(
|
||||
remote_plugin_id: &str,
|
||||
release_version: &str,
|
||||
scope: &str,
|
||||
) {
|
||||
mount_remote_plugin_detail_with_name(
|
||||
server,
|
||||
remote_plugin_id,
|
||||
"linear",
|
||||
release_version,
|
||||
scope,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn mount_remote_plugin_detail_with_name(
|
||||
server: &MockServer,
|
||||
remote_plugin_id: &str,
|
||||
plugin_name: &str,
|
||||
release_version: &str,
|
||||
scope: &str,
|
||||
) {
|
||||
let detail_body = format!(
|
||||
r#"{{
|
||||
"id": "{remote_plugin_id}",
|
||||
"name": "linear",
|
||||
"name": "{plugin_name}",
|
||||
"scope": "{scope}",
|
||||
"installation_policy": "AVAILABLE",
|
||||
"authentication_policy": "ON_USE",
|
||||
|
||||
Reference in New Issue
Block a user