mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
Prefer remote Sites over the bundled plugin (#42100)
## Why When the remote Sites plugin is installed and its local bundle is available, exposing the bundled copy as well creates duplicate catalog entries and can load the wrong skills. ## What changed - Persist an account- and backend-scoped exclusion for `sites@openai-bundled` once the remote replacement is installed and loadable. - Apply the exclusion to plugin catalogs, search, direct reads and installs, runtime loading, hooks, and skill caches while preserving the remote plugin's server-owned enabled state. - Restore the bundled fallback when the remote plugin is absent or its local files are unavailable, and throttle repeated availability checks for 60 seconds. ## Testing - Added manager tests for persistence across restarts, account/backend isolation, fallback restoration, concurrent checks, and refresh throttling. - Added app-server and agent-turn tests confirming that catalog, search, and skill loading prefer remote Sites. GitOrigin-RevId: bc1154f79d3107910fee9fa27389a0ddb48ce6cf
This commit is contained in:
1 parent
a17ee5705c
commit
b6ab99ed87
16 files changed
+864
-27
No files matched your search
@@ -63,7 +63,11 @@ use core_test_support::zsh_fork::zsh_fork_test_builder;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use test_case::test_case;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
const SAMPLE_PLUGIN_CONFIG_NAME: &str = "sample@test";
|
||||
const SAMPLE_REMOTE_PLUGIN_CONFIG_NAME: &str = "sample@openai-curated-remote";
|
||||
@@ -629,10 +633,8 @@ async fn plugin_skill_product_policy_and_migrated_command_precedence_reach_agent
|
||||
})
|
||||
.await;
|
||||
|
||||
let developer_text = response
|
||||
.single_request()
|
||||
.message_input_texts("developer")
|
||||
.join("\n");
|
||||
let requests = response.requests();
|
||||
let developer_text = requests[0].message_input_texts("developer").join("\n");
|
||||
assert_eq!(
|
||||
(
|
||||
developer_text.contains("sample:source-command-review: native review skill"),
|
||||
@@ -688,6 +690,107 @@ async fn legacy_plugin_skill_prompt_remains_complete() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn sites_migration_agent_turn_prefers_loadable_remote_sites() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
let server = start_mock_server().await;
|
||||
let response = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp1"), ev_completed("resp1")]),
|
||||
)
|
||||
.await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/ps/plugins/installed"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"plugins": [{
|
||||
"id": "plugins~plugin_connector_1p_689987207de08191979cf68eca2941c6",
|
||||
"name": "sites",
|
||||
"scope": "GLOBAL",
|
||||
"status": "ENABLED",
|
||||
"installation_policy": "AVAILABLE",
|
||||
"authentication_policy": "ON_USE",
|
||||
"release": {
|
||||
"version": "local",
|
||||
"display_name": "Sites",
|
||||
"description": "Sites",
|
||||
"interface": {},
|
||||
},
|
||||
"enabled": true,
|
||||
}],
|
||||
"pagination": {"next_page_token": null},
|
||||
})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let codex_home = Arc::new(TempDir::new()?);
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
remote_plugin = true
|
||||
|
||||
[plugins."sites@openai-bundled"]
|
||||
enabled = true
|
||||
"#,
|
||||
)?;
|
||||
for (marketplace, skill) in [
|
||||
("openai-bundled", "bundled-sites"),
|
||||
("openai-curated-remote", "remote-sites"),
|
||||
] {
|
||||
let root = codex_home
|
||||
.path()
|
||||
.join(format!("plugins/cache/{marketplace}/sites/local"));
|
||||
std::fs::create_dir_all(root.join(".codex-plugin"))?;
|
||||
std::fs::write(
|
||||
root.join(".codex-plugin/plugin.json"),
|
||||
r#"{"name":"sites"}"#,
|
||||
)?;
|
||||
let skill_dir = root.join("skills").join(skill);
|
||||
std::fs::create_dir_all(&skill_dir)?;
|
||||
std::fs::write(
|
||||
skill_dir.join("SKILL.md"),
|
||||
format!("---\nname: {skill}\ndescription: test skill\n---\n\n# body\n"),
|
||||
)?;
|
||||
}
|
||||
|
||||
let chatgpt_base_url = server.uri();
|
||||
let mut builder = test_codex()
|
||||
.with_home(codex_home)
|
||||
.with_extensions(skills_extensions())
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(move |config| config.chatgpt_base_url = chatgpt_base_url);
|
||||
let test = builder.build_with_auto_env(&server).await?;
|
||||
|
||||
let auth = test.thread_manager.auth_manager().auth().await;
|
||||
test.thread_manager
|
||||
.plugins_manager()
|
||||
.reconcile_remote_installed_plugins(&test.config.plugins_config_input(), auth.as_ref())
|
||||
.await?;
|
||||
|
||||
test.codex
|
||||
.start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
|
||||
text: "Inspect the available Sites skills.".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]))
|
||||
.await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
|
||||
let requests = response.requests();
|
||||
let developer_text = requests[0].message_input_texts("developer").join("\n");
|
||||
assert_eq!(
|
||||
(
|
||||
developer_text.contains("sites:remote-sites"),
|
||||
developer_text.contains("sites:bundled-sites"),
|
||||
),
|
||||
(true, false),
|
||||
"unexpected Sites skills in developer prompt: {developer_text:?}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn agent_plugin_root_mcp_stdio_tool_round_trip_expands_reserved_paths_and_codex_env_overlay()
|
||||
-> Result<()> {
|
||||
|
||||
Reference in new issue
Block a user