mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
Refresh plugin skills after out-of-process version changes (#42284)
## Why Another process sharing `CODEX_HOME` can replace an installed plugin version without invalidating the current process's caches. This could leave skill listings tied to stale plugin paths and retain obsolete plugin generations. ## What changed - Reject cached plugin loads when their roots no longer match the active installation, so skills are reloaded after upgrades or rollbacks. - Keep the 32 most recently used configuration-based skill snapshots while allowing callers to continue using snapshots that have been evicted. ## Testing - Cover external plugin upgrades and rollbacks through `skills/list`, including a subsequent warm-cache read. - Cover cache eviction, reuse, and the lifetime of caller-held snapshots. GitOrigin-RevId: ca00f9539c01461e3945d340bf63f8436665220b
This commit is contained in:
1 parent
8d32abcd01
commit
50fffd5ed3
4 files changed
+218
-37
No files matched your search
@@ -30,8 +30,10 @@ use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_core::config::set_project_trust_level;
|
||||
use codex_core_plugins::store::PluginStore;
|
||||
use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||
use codex_exec_server::CreateDirectoryOptions;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
@@ -926,6 +928,95 @@ enabled = false
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn skills_list_refreshes_externally_updated_plugin_versions() -> Result<()> {
|
||||
skip_if_wine_exec!(
|
||||
Ok(()),
|
||||
"skills/list currently requires host-native cwd paths for workspace config"
|
||||
);
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let source = TempDir::new()?;
|
||||
std::fs::create_dir_all(source.path().join(".codex-plugin"))?;
|
||||
std::fs::create_dir_all(source.path().join("skills"))?;
|
||||
std::fs::write(
|
||||
source.path().join(".codex-plugin/plugin.json"),
|
||||
r#"{"name":"sample"}"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
|
||||
[plugins."sample@test"]
|
||||
enabled = true
|
||||
"#,
|
||||
)?;
|
||||
let plugin_id = PluginId::parse("sample@test")?;
|
||||
let store = PluginStore::new(codex_home.path().to_path_buf());
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build_initialized_with_timeout(DEFAULT_TIMEOUT)
|
||||
.await?;
|
||||
let file_system = mcp.auto_env()?.environment().get_filesystem();
|
||||
file_system
|
||||
.create_directory(
|
||||
&PathUri::from_abs_path(&AbsolutePathBuf::try_from(cwd.path())?).join(".git")?,
|
||||
CreateDirectoryOptions {
|
||||
recursive: true,
|
||||
follow_symlinks: true,
|
||||
},
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Mutate the shared store from outside app-server, without its cache invalidation callback.
|
||||
for version in ["1.0.0", "2.0.0", "0.9.0"] {
|
||||
let body = format!("---\nname: search\ndescription: version {version}\n---\n");
|
||||
std::fs::write(source.path().join("skills/SKILL.md"), &body)?;
|
||||
let installed = store.install_with_version(
|
||||
AbsolutePathBuf::try_from(source.path())?,
|
||||
plugin_id.clone(),
|
||||
version.to_string(),
|
||||
)?;
|
||||
let expected_path = AbsolutePathBuf::try_from(std::fs::canonicalize(
|
||||
installed.installed_path.join("skills/SKILL.md"),
|
||||
)?)?;
|
||||
// Exercise both invalidation and the subsequent warm read for the same cwd.
|
||||
for _ in 0..2 {
|
||||
let request_id = mcp
|
||||
.send_skills_list_request(SkillsListParams {
|
||||
cwds: vec![cwd.path().to_path_buf()],
|
||||
force_reload: false,
|
||||
})
|
||||
.await?;
|
||||
let SkillsListResponse { data } =
|
||||
timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??;
|
||||
let skills = data[0]
|
||||
.skills
|
||||
.iter()
|
||||
.filter(|skill| skill.name == "sample:search")
|
||||
.map(|skill| {
|
||||
Ok((
|
||||
skill.path.clone(),
|
||||
skill.description.clone(),
|
||||
std::fs::read_to_string(&skill.path)?,
|
||||
))
|
||||
})
|
||||
.collect::<std::io::Result<Vec<_>>>()?;
|
||||
assert_eq!(
|
||||
skills,
|
||||
vec![(
|
||||
expected_path.clone(),
|
||||
format!("version {version}"),
|
||||
body.clone()
|
||||
)],
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn skills_list_force_reload_refreshes_cached_plugin_roots() -> Result<()> {
|
||||
skip_if_wine_exec!(
|
||||
|
||||
Reference in new issue
Block a user