Skip symlinks when installing plugins (#36967)

## What changed

- Ignore symbolic links and other non-file, non-directory entries while copying a plugin into the store instead of rejecting the installation.
- Cover symlinked skill files and executables, verifying that the plugin installs without copying either link.

GitOrigin-RevId: da0cbbd95d12313afcaecdcb52824fc1922d0b74
This commit is contained in:
jacobzhou-oai
2026-08-04 20:11:00 +00:00
committed by copyberry
parent 78f00743f9
commit 720c9d68e1
2 changed files with 10 additions and 24 deletions

View File

@@ -745,16 +745,6 @@ fn copy_dir_recursive(source: &Path, target: &Path) -> Result<(), PluginStoreErr
} else if file_type.is_file() {
fs::copy(&source_path, &target_path)
.map_err(|err| PluginStoreError::io("failed to copy plugin file", err))?;
} else if file_type.is_symlink() {
return Err(PluginStoreError::Invalid(format!(
"plugin source contains unsupported symbolic link: {}",
source_path.display()
)));
} else {
return Err(PluginStoreError::Invalid(format!(
"plugin source contains unsupported file type: {}",
source_path.display()
)));
}
}

View File

@@ -396,7 +396,7 @@ fn agent_plugin_install_does_not_migrate_commands() {
#[cfg(unix)]
#[test]
fn agent_plugin_install_rejects_symlinked_skill_file() {
fn agent_plugin_install_skips_symlinked_skill_file() {
let tmp = tempdir().unwrap();
let plugin_root = tmp.path().join("agent-plugin");
let skill_root = plugin_root.join("skills/greet");
@@ -411,19 +411,17 @@ fn agent_plugin_install_rejects_symlinked_skill_file() {
std::os::unix::fs::symlink(&outside_skill, skill_root.join("SKILL.md")).unwrap();
let plugin_id = PluginId::new("agent-plugin".to_string(), "debug".to_string()).unwrap();
let err = PluginStore::new(tmp.path().to_path_buf())
let result = PluginStore::new(tmp.path().to_path_buf())
.install(AbsolutePathBuf::try_from(plugin_root).unwrap(), plugin_id)
.expect_err("symlinked Agent Plugin skill should be rejected");
.expect("install Agent Plugin");
assert!(
err.to_string()
.contains("plugin source contains unsupported symbolic link")
);
assert!(result.installed_path.join("plugin.json").is_file());
assert!(!result.installed_path.join("skills/greet/SKILL.md").exists());
}
#[cfg(unix)]
#[test]
fn agent_plugin_install_rejects_symlinked_executable() {
fn agent_plugin_install_skips_symlinked_executable() {
let tmp = tempdir().unwrap();
let plugin_root = tmp.path().join("agent-plugin");
let bin_root = plugin_root.join("bin");
@@ -438,14 +436,12 @@ fn agent_plugin_install_rejects_symlinked_executable() {
std::os::unix::fs::symlink(&outside_executable, bin_root.join("tool")).unwrap();
let plugin_id = PluginId::new("agent-plugin".to_string(), "debug".to_string()).unwrap();
let err = PluginStore::new(tmp.path().to_path_buf())
let result = PluginStore::new(tmp.path().to_path_buf())
.install(AbsolutePathBuf::try_from(plugin_root).unwrap(), plugin_id)
.expect_err("symlinked Agent Plugin executable should be rejected");
.expect("install Agent Plugin");
assert!(
err.to_string()
.contains("plugin source contains unsupported symbolic link")
);
assert!(result.installed_path.join("plugin.json").is_file());
assert!(!result.installed_path.join("bin/tool").exists());
}
#[test]