use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
const PROVENANCE_VERSION: u8 = 1;
const MAX_SKILL_READS: usize = 8;
const MAX_SOURCE_BYTES: usize = 1024;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct SkillReadProvenance {
pub(crate) version: u8,
pub(crate) identity: String,
pub(crate) source: String,
pub(crate) start: usize,
pub(crate) end: usize,
pub(crate) sha256: String,
}
impl SkillReadProvenance {
pub(crate) fn validates(&self, output: &str) -> bool {
self.version == PROVENANCE_VERSION
&& valid_digest(&self.identity)
&& valid_digest(&self.sha256)
&& self.source.starts_with("skill://")
&& self.source.len() <= MAX_SOURCE_BYTES
&& !self.source.chars().any(char::is_control)
&& self.start < self.end
&& output.is_char_boundary(self.start)
&& output.is_char_boundary(self.end)
&& output
.get(self.start..self.end)
.is_some_and(|slice| digest_hex(slice.as_bytes()) == self.sha256)
}
}
pub(crate) fn validated_skill_reads(
tool_name: &str,
success: bool,
output: &str,
metadata: &Value,
) -> Vec<SkillReadProvenance> {
if tool_name != "read" || !success {
return Vec::new();
}
let Some(values) = metadata.get("skill_reads").and_then(Value::as_array) else {
return Vec::new();
};
if values.len() > MAX_SKILL_READS {
return Vec::new();
}
let Ok(reads) = values
.iter()
.cloned()
.map(serde_json::from_value)
.collect::<Result<Vec<SkillReadProvenance>, _>>()
else {
return Vec::new();
};
if reads.iter().any(|read| !read.validates(output))
|| reads.windows(2).any(|pair| pair[0].end > pair[1].start)
{
return Vec::new();
}
reads
}
pub(super) fn digest_hex(bytes: &[u8]) -> String {
crate::hex::lower_hex(Sha256::digest(bytes))
}
fn valid_digest(value: &str) -> bool {
value.len() == 64
&& value
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}