修复客户端 Skill 运行时目录残留
每次准备时重建当前启用 Skill 的隔离运行时目录 补充禁用、重命名和全部禁用后的残留清理测试 更新 DirectProject Skill 运行时投影约定文档
This commit is contained in:
@@ -884,12 +884,26 @@ fn copy_skill_runtime_tree(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_enabled_client_skill_root(
|
||||
fn prepare_client_skill_runtime_root(
|
||||
root: &Path,
|
||||
index: &ClientExtensionIndex,
|
||||
isolated_os_home: &Path,
|
||||
) -> Result<Option<PathBuf>, String> {
|
||||
let root = extensions_root()?;
|
||||
let index = read_index(&root)?;
|
||||
let client_skill_root = isolated_os_home.join(".agents").join("client-skills");
|
||||
|
||||
// This directory is a complete projection of the currently enabled
|
||||
// client Skills. Remove the previous projection before copying so a
|
||||
// disabled, deleted, renamed, or otherwise changed Skill cannot remain
|
||||
// discoverable through the extra root.
|
||||
if client_skill_root.exists() {
|
||||
fs::remove_dir_all(&client_skill_root).map_err(|error| {
|
||||
format!(
|
||||
"清理客户端 Skill 运行时目录失败:{}: {error}",
|
||||
client_skill_root.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
let mut copied_any = false;
|
||||
|
||||
for item in index
|
||||
@@ -927,6 +941,14 @@ pub(crate) fn prepare_enabled_client_skill_root(
|
||||
Ok(copied_any.then_some(client_skill_root))
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_enabled_client_skill_root(
|
||||
isolated_os_home: &Path,
|
||||
) -> Result<Option<PathBuf>, String> {
|
||||
let root = extensions_root()?;
|
||||
let index = read_index(&root)?;
|
||||
prepare_client_skill_runtime_root(&root, &index, isolated_os_home)
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_enabled_client_mcp_servers() -> Result<Vec<ClientMcpRuntimeServer>, String> {
|
||||
let root = extensions_root()?;
|
||||
let mut index = read_index(&root)?;
|
||||
@@ -1385,6 +1407,83 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_skill_runtime_root_removes_stale_disabled_and_renamed_items() {
|
||||
let directory = tempfile::tempdir().expect("temp directory");
|
||||
let extension_root = directory.path().join("extensions");
|
||||
let source_root = extension_root.join("sources/source-1");
|
||||
let skill_a = source_root.join("skill-a/SKILL.md");
|
||||
let skill_b = source_root.join("skill-b/SKILL.md");
|
||||
fs::create_dir_all(skill_a.parent().expect("skill-a parent")).expect("create skill-a");
|
||||
fs::create_dir_all(skill_b.parent().expect("skill-b parent")).expect("create skill-b");
|
||||
fs::write(&skill_a, "---\nname: skill-a\n---\n").expect("write skill-a");
|
||||
fs::write(&skill_b, "---\nname: skill-b\n---\n").expect("write skill-b");
|
||||
|
||||
let source = ImportedExtensionSource {
|
||||
id: "source-1".to_string(),
|
||||
original_name: "fixture".to_string(),
|
||||
storage_path: "sources/source-1".to_string(),
|
||||
fingerprint: "source-fingerprint".to_string(),
|
||||
};
|
||||
let mut index = ClientExtensionIndex {
|
||||
schema_version: CLIENT_EXTENSIONS_SCHEMA_VERSION.to_string(),
|
||||
sources: vec![source],
|
||||
items: vec![
|
||||
StoredExtensionItem {
|
||||
id: "item-a".to_string(),
|
||||
source_id: "source-1".to_string(),
|
||||
extension_type: "skill".to_string(),
|
||||
name: "skill-a".to_string(),
|
||||
original_name: "skill-a".to_string(),
|
||||
source_relative_path: "skill-a/SKILL.md".to_string(),
|
||||
enabled: true,
|
||||
fingerprint: "skill-a-fingerprint".to_string(),
|
||||
last_error: None,
|
||||
mcp_config: None,
|
||||
},
|
||||
StoredExtensionItem {
|
||||
id: "item-b".to_string(),
|
||||
source_id: "source-1".to_string(),
|
||||
extension_type: "skill".to_string(),
|
||||
name: "skill-b".to_string(),
|
||||
original_name: "skill-b".to_string(),
|
||||
source_relative_path: "skill-b/SKILL.md".to_string(),
|
||||
enabled: true,
|
||||
fingerprint: "skill-b-fingerprint".to_string(),
|
||||
last_error: None,
|
||||
mcp_config: None,
|
||||
},
|
||||
],
|
||||
};
|
||||
let isolated_home = directory.path().join("home");
|
||||
let runtime_root = isolated_home.join(".agents").join("client-skills");
|
||||
|
||||
prepare_client_skill_runtime_root(&extension_root, &index, &isolated_home)
|
||||
.expect("prepare initial Skill runtime root");
|
||||
assert!(runtime_root.join("skill-a/SKILL.md").is_file());
|
||||
assert!(runtime_root.join("skill-b/SKILL.md").is_file());
|
||||
|
||||
index.items[0].enabled = false;
|
||||
prepare_client_skill_runtime_root(&extension_root, &index, &isolated_home)
|
||||
.expect("remove disabled Skill from runtime root");
|
||||
assert!(!runtime_root.join("skill-a").exists());
|
||||
assert!(runtime_root.join("skill-b/SKILL.md").is_file());
|
||||
|
||||
index.items[1].name = "renamed-skill".to_string();
|
||||
prepare_client_skill_runtime_root(&extension_root, &index, &isolated_home)
|
||||
.expect("refresh renamed Skill runtime root");
|
||||
assert!(!runtime_root.join("skill-b").exists());
|
||||
assert!(runtime_root.join("renamed-skill/SKILL.md").is_file());
|
||||
|
||||
index.items[1].enabled = false;
|
||||
assert_eq!(
|
||||
prepare_client_skill_runtime_root(&extension_root, &index, &isolated_home)
|
||||
.expect("clear empty Skill runtime root"),
|
||||
None
|
||||
);
|
||||
assert!(!runtime_root.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_skill_fingerprint_tracks_enabled_names_and_content() {
|
||||
let mut index = ClientExtensionIndex::default();
|
||||
|
||||
@@ -306,6 +306,8 @@ MCP · 来自 my-plugin.zip 启动失败
|
||||
调用 skills/list 复核
|
||||
```
|
||||
|
||||
Skill root 是当前启用 Skill 集合的完整投影;每次准备时先清理已有运行时目录,再重新复制当前有效项,避免禁用、删除、重命名或源内容变化后的旧文件继续被 Codex 发现。
|
||||
|
||||
每个独立 Skill 的启用状态单独生效。Skill 内容中的脚本、二进制和引用资源不做行为扫描。
|
||||
|
||||
### 8.2 MCP
|
||||
|
||||
Reference in New Issue
Block a user