use sha2::{Digest, Sha256};
use std::path::Path;
use super::content::PackageContent;
use super::manifest::{PackageKind, PackageManifest};
pub(crate) fn compact_json_text(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut chars = text.chars();
let mut in_string = false;
while let Some(ch) = chars.next() {
if in_string {
out.push(ch);
match ch {
'\\' => {
if let Some(esc) = chars.next() {
out.push(esc);
}
}
'"' => in_string = false,
_ => {}
}
} else {
match ch {
'"' => {
in_string = true;
out.push(ch);
}
' ' | '\t' | '\n' | '\r' => {}
_ => out.push(ch),
}
}
}
out
}
pub(crate) fn extract_top_level_value_text<'a>(doc: &'a str, member: &str) -> Option<&'a str> {
let bytes = doc.as_bytes();
let n = bytes.len();
let mut i = 0;
let skip_ws = |i: &mut usize| {
while *i < n && matches!(bytes[*i], b' ' | b'\t' | b'\n' | b'\r') {
*i += 1;
}
};
let skip_string = |i: &mut usize| {
*i += 1; while *i < n {
match bytes[*i] {
b'\\' => *i += 2,
b'"' => {
*i += 1;
return;
}
_ => *i += 1,
}
}
};
let skip_value = |i: &mut usize| {
skip_ws(i);
match bytes.get(*i) {
Some(b'"') => skip_string(i),
Some(&open @ (b'{' | b'[')) => {
let close = if open == b'{' { b'}' } else { b']' };
let mut depth = 0usize;
while *i < n {
match bytes[*i] {
b'"' => {
skip_string(i);
continue;
}
c if c == open => depth += 1,
c if c == close => {
depth -= 1;
if depth == 0 {
*i += 1;
return;
}
}
_ => {}
}
*i += 1;
}
}
_ => {
while *i < n
&& !matches!(bytes[*i], b',' | b'}' | b']' | b' ' | b'\t' | b'\n' | b'\r')
{
*i += 1;
}
}
}
};
skip_ws(&mut i);
if bytes.get(i) != Some(&b'{') {
return None;
}
i += 1;
loop {
skip_ws(&mut i);
match bytes.get(i) {
Some(b'"') => {}
_ => return None,
}
let key_start = i;
skip_string(&mut i);
let key: String = serde_json::from_str(&doc[key_start..i]).ok()?;
skip_ws(&mut i);
if bytes.get(i) != Some(&b':') {
return None;
}
i += 1;
skip_ws(&mut i);
if key == member {
let start = i;
skip_value(&mut i);
return Some(&doc[start..i]);
}
skip_value(&mut i);
skip_ws(&mut i);
if bytes.get(i) == Some(&b',') {
i += 1;
}
}
}
pub fn validate_kind_coherence(
manifest: &PackageManifest,
content: &PackageContent,
) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
match manifest.kind {
PackageKind::Addon => match &content.addon {
None => errors.push("kind=addon requires a content.addon payload".into()),
Some(payload) => {
match crate::core::addons::manifest::AddonManifest::from_toml(
&payload.manifest_toml,
) {
Err(e) => errors.push(format!("embedded addon manifest does not parse: {e}")),
Ok(addon) => {
if let Err(e) = addon.validate() {
errors.push(format!("embedded addon manifest is invalid: {e}"));
}
let slug = manifest
.name
.rsplit('/')
.next()
.unwrap_or(manifest.name.as_str());
if addon.addon.name != slug {
errors.push(format!(
"embedded addon name `{}` does not match the package name \
`{slug}`",
addon.addon.name
));
}
if addon.addon.version != manifest.version {
errors.push(format!(
"embedded addon version `{}` does not match the package \
version `{}`",
addon.addon.version, manifest.version
));
}
if !addon.is_installable() {
errors.push(
"embedded addon manifest has no runnable [mcp] endpoint".into(),
);
}
}
}
}
},
PackageKind::Skills => {
if content.addon.is_some() {
errors.push("content.addon payload requires kind=addon".into());
}
match &content.documents {
None => errors.push("kind=skills requires a content.documents payload".into()),
Some(docs) => validate_documents(docs, &mut errors),
}
}
PackageKind::Context | PackageKind::Grammar => {
if content.addon.is_some() {
errors.push(format!(
"content.addon payload requires kind=addon (manifest declares kind={})",
manifest.kind.as_str()
));
}
if content.documents.is_some() {
errors.push(format!(
"content.documents payload requires kind=skills (manifest declares kind={})",
manifest.kind.as_str()
));
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn validate_documents(docs: &super::content::DocumentsContent, errors: &mut Vec<String>) {
use super::content::{MAX_DOCUMENT_FILES, MAX_DOCUMENTS_TOTAL_BYTES};
if docs.files.is_empty() {
errors.push("kind=skills payload has no files".into());
return;
}
if docs.files.len() > MAX_DOCUMENT_FILES {
errors.push(format!(
"skills payload has {} files (cap: {MAX_DOCUMENT_FILES})",
docs.files.len()
));
return;
}
let mut seen = std::collections::HashSet::new();
let mut total: usize = 0;
for blob in &docs.files {
if let Err(e) = validate_document_path(&blob.path) {
errors.push(e);
continue;
}
if !seen.insert(blob.path.as_str()) {
errors.push(format!("duplicate document path `{}`", blob.path));
continue;
}
if blob.sha256.len() != 64 || !blob.sha256.chars().all(|c| c.is_ascii_hexdigit()) {
errors.push(format!(
"`{}`: sha256 must be a 64-char hex string",
blob.path
));
continue;
}
match blob.decode_verified() {
Ok(plain) => total += plain.len(),
Err(e) => errors.push(e),
}
}
if total > MAX_DOCUMENTS_TOTAL_BYTES {
errors.push(format!(
"skills payload decodes to {total} bytes (cap: {MAX_DOCUMENTS_TOTAL_BYTES})"
));
}
}
pub(crate) fn validate_document_path(path: &str) -> Result<(), String> {
if path.is_empty() || path.len() > 512 {
return Err(format!(
"invalid document path `{path}` (empty or too long)"
));
}
if path.starts_with('/') || path.contains('\\') || path.contains(':') {
return Err(format!(
"invalid document path `{path}` (must be relative with `/` separators)"
));
}
let has_bad_component = path
.split('/')
.any(|c| c.is_empty() || c == "." || c == ".." || c.starts_with(".."));
if has_bad_component || path.chars().any(char::is_control) {
return Err(format!("invalid document path `{path}` (unsafe component)"));
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CheckOutcome {
Pass,
Fail,
Skipped,
}
impl CheckOutcome {
pub fn as_str(&self) -> &'static str {
match self {
Self::Pass => "pass",
Self::Fail => "fail",
Self::Skipped => "skipped",
}
}
}
#[derive(Debug)]
pub struct VerifyReport {
pub name: Option<String>,
pub version: Option<String>,
pub structure: CheckOutcome,
pub content_hash: CheckOutcome,
pub package_hash: CheckOutcome,
pub signature: CheckOutcome,
pub errors: Vec<String>,
}
impl VerifyReport {
pub fn valid(&self) -> bool {
self.errors.is_empty()
}
fn failed(error: String) -> Self {
Self {
name: None,
version: None,
structure: CheckOutcome::Fail,
content_hash: CheckOutcome::Skipped,
package_hash: CheckOutcome::Skipped,
signature: CheckOutcome::Skipped,
errors: vec![error],
}
}
}
fn sha256_hex(data: &[u8]) -> String {
let mut h = Sha256::new();
h.update(data);
crate::core::agent_identity::hex_encode(&h.finalize())
}
pub fn verify_package_text(doc: &str) -> VerifyReport {
let value: serde_json::Value = match serde_json::from_str(doc) {
Ok(v) => v,
Err(e) => return VerifyReport::failed(format!("not valid JSON: {e}")),
};
let Some(manifest_value) = value.get("manifest") else {
return VerifyReport::failed("missing required member: manifest".into());
};
if value.get("content").is_none() {
return VerifyReport::failed("missing required member: content".into());
}
let manifest: PackageManifest = match serde_json::from_value(manifest_value.clone()) {
Ok(m) => m,
Err(e) => return VerifyReport::failed(format!("manifest does not parse: {e}")),
};
let mut report = VerifyReport {
name: Some(manifest.name.clone()),
version: Some(manifest.version.clone()),
structure: CheckOutcome::Pass,
content_hash: CheckOutcome::Skipped,
package_hash: CheckOutcome::Skipped,
signature: CheckOutcome::Skipped,
errors: Vec::new(),
};
if let Err(errs) = manifest.validate() {
report.structure = CheckOutcome::Fail;
report.errors.extend(errs);
return report;
}
if let Ok(content) =
serde_json::from_value::<PackageContent>(value.get("content").cloned().unwrap_or_default())
&& let Err(errs) = validate_kind_coherence(&manifest, &content)
{
report.structure = CheckOutcome::Fail;
report.errors.extend(errs);
return report;
}
let Some(content_text) = extract_top_level_value_text(doc, "content") else {
report.structure = CheckOutcome::Fail;
report
.errors
.push("could not locate the content member in the document".into());
return report;
};
let canonical = compact_json_text(content_text);
let actual_content_hash = sha256_hex(canonical.as_bytes());
if actual_content_hash == manifest.integrity.content_hash {
report.content_hash = CheckOutcome::Pass;
} else {
report.content_hash = CheckOutcome::Fail;
report.errors.push(format!(
"content_hash mismatch: manifest says {}, content hashes to {actual_content_hash}",
manifest.integrity.content_hash
));
}
if manifest.integrity.byte_size != canonical.len() as u64 {
report.content_hash = CheckOutcome::Fail;
report.errors.push(format!(
"byte_size mismatch: manifest says {}, content is {} bytes",
manifest.integrity.byte_size,
canonical.len()
));
}
let expected_sha = sha256_hex(
format!(
"{}:{}:{actual_content_hash}",
manifest.name, manifest.version
)
.as_bytes(),
);
if expected_sha == manifest.integrity.sha256 {
report.package_hash = CheckOutcome::Pass;
} else {
report.package_hash = CheckOutcome::Fail;
report.errors.push(format!(
"package sha256 mismatch: manifest says {}, recomputed {expected_sha}",
manifest.integrity.sha256
));
}
if manifest.signature.is_some() {
match super::signing::verify_signature(&manifest) {
Ok(true) => report.signature = CheckOutcome::Pass,
Ok(false) => {
report.signature = CheckOutcome::Fail;
report.errors.push(
"signature verification failed — the package was modified after signing".into(),
);
}
Err(e) => {
report.signature = CheckOutcome::Fail;
report.errors.push(format!("signature check errored: {e}"));
}
}
}
report
}
pub fn verify_package_file(path: &Path) -> Result<VerifyReport, String> {
if !crate::core::contracts::is_package_file(path) {
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("(none)");
return Err(format!(
"unsupported file extension '.{ext}' — expected .{} or .{}",
crate::core::contracts::PACKAGE_EXTENSION,
crate::core::contracts::LEGACY_PACKAGE_EXTENSION,
));
}
let meta = std::fs::metadata(path).map_err(|e| format!("stat package file: {e}"))?;
if meta.len() > crate::core::contracts::MAX_PACKAGE_FILE_BYTES {
return Err(format!(
"package file too large ({} bytes, max {} bytes)",
meta.len(),
crate::core::contracts::MAX_PACKAGE_FILE_BYTES,
));
}
let doc = std::fs::read_to_string(path).map_err(|e| format!("read package file: {e}"))?;
Ok(verify_package_text(&doc))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::context_package::content::PackageContent;
use crate::core::context_package::manifest::{
CompatibilitySpec, PackageIntegrity, PackageLayer, PackageProvenance, PackageStats,
};
use chrono::Utc;
fn signed_bundle_doc() -> String {
let content = PackageContent::default();
let content_json = r#"{"note":"hello","weight":1.0}"#.to_string();
let content_hash = sha256_hex(content_json.as_bytes());
let sha = sha256_hex(format!("vt-pkg:1.0.0:{content_hash}").as_bytes());
let mut manifest = PackageManifest {
schema_version: crate::core::contracts::CONTEXT_PACKAGE_V1_SCHEMA_VERSION,
conformance_level: None,
kind: crate::core::context_package::manifest::PackageKind::default(),
name: "vt-pkg".into(),
version: "1.0.0".into(),
description: "verify test".into(),
author: None,
scope: None,
created_at: Utc::now(),
updated_at: None,
layers: vec![PackageLayer::Knowledge],
dependencies: vec![],
tags: vec![],
visibility: None,
integrity: PackageIntegrity {
sha256: sha,
content_hash,
byte_size: content_json.len() as u64,
},
provenance: PackageProvenance {
tool: "lean-ctx".into(),
tool_version: "0.0.0".into(),
project_hash: None,
source_session_id: None,
},
compatibility: CompatibilitySpec::default(),
stats: PackageStats::default(),
signature: None,
graph_summary: None,
marketplace: None,
};
let key = ed25519_dalek::SigningKey::from_bytes(&[9u8; 32]);
super::super::signing::sign_package(&mut manifest, &content, &key);
format!(
"{{\"manifest\":{},\"content\":{}}}",
serde_json::to_string(&manifest).unwrap(),
content_json
)
}
#[test]
fn valid_signed_package_passes_all_checks() {
let report = verify_package_text(&signed_bundle_doc());
assert!(report.valid(), "errors: {:?}", report.errors);
assert_eq!(report.structure, CheckOutcome::Pass);
assert_eq!(report.content_hash, CheckOutcome::Pass);
assert_eq!(report.package_hash, CheckOutcome::Pass);
assert_eq!(report.signature, CheckOutcome::Pass);
}
#[test]
fn unsigned_package_skips_signature() {
let doc = signed_bundle_doc();
let mut v: serde_json::Value = serde_json::from_str(&doc).unwrap();
v["manifest"]["signature"] = serde_json::Value::Null;
let report = verify_package_text(&serde_json::to_string(&v).unwrap());
assert_eq!(report.signature, CheckOutcome::Skipped);
}
#[test]
fn tampered_content_fails_content_hash() {
let doc = signed_bundle_doc().replace("\"hello\"", "\"evil\"");
let report = verify_package_text(&doc);
assert_eq!(report.content_hash, CheckOutcome::Fail);
assert!(!report.valid());
}
#[test]
fn whitespace_only_changes_do_not_break_hashing() {
let doc = signed_bundle_doc()
.replace("\"content\":{", "\"content\": {\n ")
.replace(",\"weight\"", ",\n \"weight\"");
let report = verify_package_text(&doc);
assert!(report.valid(), "errors: {:?}", report.errors);
}
#[test]
fn corrupted_signature_fails() {
let doc = signed_bundle_doc();
let mut v: serde_json::Value = serde_json::from_str(&doc).unwrap();
let sig = v["manifest"]["signature"]["value"].as_str().unwrap();
let flipped = if let Some(rest) = sig.strip_prefix("0000") {
format!("ffff{rest}")
} else {
format!("0000{}", &sig[4..])
};
v["manifest"]["signature"]["value"] = flipped.into();
let report = verify_package_text(&serde_json::to_string(&v).unwrap());
assert_eq!(report.signature, CheckOutcome::Fail);
}
#[test]
fn missing_manifest_fails_structure() {
let report = verify_package_text("{\"content\":{}}");
assert_eq!(report.structure, CheckOutcome::Fail);
assert!(report.errors[0].contains("manifest"));
}
const COHERENT_ADDON_TOML: &str = r#"
[addon]
name = "lean-md"
version = "1.2.0"
description = "Markdown skills runtime"
[mcp]
transport = "stdio"
command = "lean-md"
args = ["serve"]
"#;
fn kinded_manifest(kind: super::PackageKind, name: &str, version: &str) -> PackageManifest {
PackageManifest {
schema_version: crate::core::contracts::CONTEXT_PACKAGE_V2_SCHEMA_VERSION,
conformance_level: None,
kind,
name: name.into(),
version: version.into(),
description: "coherence test".into(),
author: None,
scope: None,
created_at: Utc::now(),
updated_at: None,
layers: vec![],
dependencies: vec![],
tags: vec![],
visibility: None,
integrity: PackageIntegrity {
sha256: "a".repeat(64),
content_hash: "b".repeat(64),
byte_size: 1,
},
provenance: PackageProvenance {
tool: "lean-ctx".into(),
tool_version: "0.0.0".into(),
project_hash: None,
source_session_id: None,
},
compatibility: CompatibilitySpec::default(),
stats: PackageStats::default(),
signature: None,
graph_summary: None,
marketplace: None,
}
}
fn addon_content(toml: &str) -> PackageContent {
PackageContent {
addon: Some(crate::core::context_package::content::AddonContent {
manifest_toml: toml.to_string(),
}),
..PackageContent::default()
}
}
#[test]
fn coherent_addon_pack_passes() {
let manifest = kinded_manifest(super::PackageKind::Addon, "@acme/lean-md", "1.2.0");
assert!(validate_kind_coherence(&manifest, &addon_content(COHERENT_ADDON_TOML)).is_ok());
}
#[test]
fn addon_kind_without_payload_fails() {
let manifest = kinded_manifest(super::PackageKind::Addon, "@acme/lean-md", "1.2.0");
let errs =
validate_kind_coherence(&manifest, &PackageContent::default()).expect_err("must fail");
assert!(errs[0].contains("requires a content.addon"), "{errs:?}");
}
#[test]
fn context_pack_with_addon_payload_fails() {
let manifest = kinded_manifest(super::PackageKind::Context, "plain-pack", "1.0.0");
let errs = validate_kind_coherence(&manifest, &addon_content(COHERENT_ADDON_TOML))
.expect_err("must fail");
assert!(errs[0].contains("requires kind=addon"), "{errs:?}");
}
#[test]
fn addon_name_and_version_must_match_the_pack() {
let manifest = kinded_manifest(super::PackageKind::Addon, "@acme/other-name", "9.9.9");
let errs = validate_kind_coherence(&manifest, &addon_content(COHERENT_ADDON_TOML))
.expect_err("must fail");
assert!(
errs.iter()
.any(|e| e.contains("does not match the package name")),
"{errs:?}"
);
assert!(
errs.iter()
.any(|e| e.contains("does not match the package version")),
"{errs:?}"
);
}
}