fix(growth,kernel,runtime): 穷尽审计后 7 项修复 — body 持久化 + embedding 死路径 + 安全加固
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

CRITICAL 修复:
- body_markdown 数据丢失: SkillManifest.body 字段 + serialize_skill_md 使用 body 替代默认内容
- embedding 检索死路径: rerank_entries 使用异步 index_entry_with_embedding + score_similarity_with_embedding (70/30 混合)
- try_write 静默丢失: pending_embedding 字段 + apply_pending_embedding() 延迟应用

IMPORTANT 修复:
- auto_mode 内存泄漏: add_pending 容量限制 100 + 溢出时丢弃最旧
- name_to_slug 空 ID: uuid fallback for empty/whitespace-only names
- compaction embedding 缺失: compaction GrowthIntegration 也接收 embedding
- kernel 未初始化警告: viking_configure_embedding warn log

验证: 934+ tests PASS, 0 failures
This commit is contained in:
iven
2026-04-21 17:27:37 +08:00
parent 5b5491a08f
commit a43806ccc2
13 changed files with 97 additions and 20 deletions

View File

@@ -191,6 +191,7 @@ pub fn parse_skill_md(content: &str) -> Result<SkillManifest> {
triggers,
tools,
enabled: true,
body: None,
})
}
@@ -292,6 +293,7 @@ pub fn parse_skill_toml(content: &str) -> Result<SkillManifest> {
triggers,
tools,
enabled: true,
body: None,
})
}

View File

@@ -241,6 +241,7 @@ impl SkillRegistry {
// P2-19: Preserve tools field during update (was silently dropped)
tools: if updates.tools.is_empty() { existing.tools } else { updates.tools },
enabled: updates.enabled,
body: existing.body,
};
// Rewrite SKILL.md
@@ -318,10 +319,14 @@ fn serialize_skill_md(manifest: &SkillManifest) -> String {
parts.push("---".to_string());
parts.push(String::new());
// Body: use description as the skill content
parts.push(format!("# {}", manifest.name));
parts.push(String::new());
parts.push(manifest.description.clone());
// Body: use custom body if provided, otherwise default to "# {name}\n\n{description}"
if let Some(ref body) = manifest.body {
parts.push(body.clone());
} else {
parts.push(format!("# {}", manifest.name));
parts.push(String::new());
parts.push(manifest.description.clone());
}
parts.join("\n")
}

View File

@@ -534,6 +534,7 @@ mod tests {
triggers: triggers.into_iter().map(|s| s.to_string()).collect(),
tools: vec![],
enabled: true,
body: None,
}
}

View File

@@ -95,6 +95,9 @@ pub struct SkillManifest {
/// Whether the skill is enabled
#[serde(default = "default_enabled")]
pub enabled: bool,
/// Custom body content for SKILL.md (overrides default "# {name}\n\n{description}")
#[serde(default, skip)]
pub body: Option<String>,
}
fn default_enabled() -> bool { true }

View File

@@ -468,6 +468,7 @@ mod tests {
triggers: vec![],
tools: vec![],
enabled: true,
body: None,
}
}

View File

@@ -20,6 +20,7 @@ fn test_manifest(mode: SkillMode) -> SkillManifest {
triggers: vec![],
tools: vec![],
enabled: true,
body: None,
}
}

View File

@@ -81,6 +81,7 @@ fn skill_manifest_full_roundtrip() {
triggers: vec!["test trigger".to_string()],
tools: vec!["bash".to_string()],
enabled: true,
body: None,
};
let json = serde_json::to_string(&manifest).unwrap();
let parsed: SkillManifest = serde_json::from_str(&json).unwrap();
@@ -126,6 +127,7 @@ fn skill_manifest_all_modes_roundtrip() {
triggers: vec![],
tools: vec![],
enabled: true,
body: None,
};
let json = serde_json::to_string(&manifest).unwrap();
let parsed: SkillManifest = serde_json::from_str(&json).unwrap();