修复客户端 Skill 运行时目录残留
Project CI / Frontend tests (pull_request) Successful in 4m34s
Project CI / Repository checks (pull_request) Successful in 4m57s
Project CI / Backend tests (pull_request) Successful in 7m39s
Project CI / Native shell tests (pull_request) Successful in 15m57s

每次准备时重建当前启用 Skill 的隔离运行时目录

补充禁用、重命名和全部禁用后的残留清理测试

更新 DirectProject Skill 运行时投影约定文档
This commit is contained in:
2026-09-01 06:06:09 +00:00
parent fc82f1d3f2
commit 745416a83a
2 changed files with 104 additions and 3 deletions
@@ -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