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(crate) fn digest_hex(bytes: &[u8]) -> String {
crate::support::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))
}
pub(super) fn finalize_skill_provenance(
metadata: &mut serde_json::Map<String, Value>,
source: &str,
selector: &Value,
raw: bool,
selected: &str,
) -> Option<SkillReadProvenance> {
let seed = metadata.remove("_skill_provenance")?;
if source.len() > 1024 || source.chars().any(char::is_control) {
return None;
}
let identity_material = serde_json::json!({
"version": 1,
"access_policy_version": seed.get("access_policy_version")?,
"descriptor_path_sha256": seed.get("descriptor_path_sha256")?,
"snapshot_sha256": seed.get("snapshot_sha256")?,
"skill": seed.get("skill")?,
"reference": seed.get("reference")?,
"selector": selector,
"raw": raw,
});
let encoded = serde_json::to_vec(&identity_material).ok()?;
Some(SkillReadProvenance {
version: 1,
identity: digest_hex(&encoded),
source: source.to_string(),
start: 0,
end: selected.len(),
sha256: digest_hex(selected.as_bytes()),
})
}