refs(#2264): harden PrefixFingerprint with full tool JSON hash
Phase 1.5 — upgrade PrefixFingerprint::compute() to hash the full tool JSON serialization (name + description + schema) instead of just tool names. This catches schema/description drift in addition to name changes. - Serialize each tool via serde_json::to_string, sort by name, join - New test: fingerprint_detects_schema_change_not_just_name_change - All 21 prefix_cache tests pass - Aligned with prompt_zones.rs tool_catalog_digest approach
This commit is contained in:
@@ -42,7 +42,7 @@ use crate::models::{SystemPrompt, Tool};
|
||||
pub struct PrefixFingerprint {
|
||||
/// SHA-256 of the system prompt text.
|
||||
pub system_sha256: String,
|
||||
/// SHA-256 of the concatenated, sorted tool names.
|
||||
/// SHA-256 of the full tool catalog JSON (names, descriptions, schemas).
|
||||
pub tools_sha256: String,
|
||||
/// SHA-256 of system_sha256 ++ tools_sha256 (combined).
|
||||
pub combined_sha256: String,
|
||||
@@ -50,16 +50,21 @@ pub struct PrefixFingerprint {
|
||||
|
||||
impl PrefixFingerprint {
|
||||
/// Compute a fingerprint from system prompt text and tool list.
|
||||
///
|
||||
/// Tools are serialized to JSON (name + description + schema), sorted
|
||||
/// by name for deterministic ordering, then SHA-256 hashed. This
|
||||
/// catches schema/description drift, not just name changes (#2264).
|
||||
pub fn compute(system_text: &str, tools: Option<&[Tool]>) -> Self {
|
||||
let system_sha256 = sha256_hex(system_text.as_bytes());
|
||||
|
||||
let tools_sha256 = match tools {
|
||||
Some(tools) if !tools.is_empty() => {
|
||||
// Sort tool names deterministically so the hash is
|
||||
// stable regardless of registration order.
|
||||
let mut tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
|
||||
tool_names.sort();
|
||||
let joined = tool_names.join(",");
|
||||
let mut serialized: Vec<String> = tools
|
||||
.iter()
|
||||
.filter_map(|t| serde_json::to_string(t).ok())
|
||||
.collect();
|
||||
serialized.sort();
|
||||
let joined = serialized.join("\n");
|
||||
sha256_hex(joined.as_bytes())
|
||||
}
|
||||
_ => sha256_hex(b""),
|
||||
@@ -489,6 +494,19 @@ mod tests {
|
||||
assert_eq!(mgr.check_count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_detects_schema_change_not_just_name_change() {
|
||||
let tool_a = make_tool("my_tool");
|
||||
let mut tool_a_v2 = make_tool("my_tool");
|
||||
tool_a_v2.description = "updated description".to_string();
|
||||
|
||||
let a = PrefixFingerprint::compute("system", Some(&[tool_a]));
|
||||
let b = PrefixFingerprint::compute("system", Some(&[tool_a_v2]));
|
||||
// Same name, different description — must produce different hash.
|
||||
assert_ne!(a.tools_sha256, b.tools_sha256);
|
||||
assert_ne!(a.combined_sha256, b.combined_sha256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_prompt_text_returns_empty_for_none() {
|
||||
assert_eq!(system_prompt_text(None), "");
|
||||
|
||||
Reference in New Issue
Block a user