systemprompt_models/feedback/
verification.rs1use std::collections::{BTreeMap, BTreeSet};
8
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use systemprompt_identifiers::{DependencyVerificationId, ManagedSourceId, ResourceRevisionId};
12
13use super::{ContentDigest, FeedbackContractError, validate_relative_path};
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
16#[serde(deny_unknown_fields)]
17pub struct DependencyVerificationInput {
18 pub revision_id: ResourceRevisionId,
19 pub source_id: ManagedSourceId,
20 pub exact_commit: String,
21 pub relative_root: String,
22 pub dependencies: Vec<ResourceRevisionId>,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
26#[serde(deny_unknown_fields)]
27pub struct DependencyVerificationRequest {
28 pub root_revision_id: ResourceRevisionId,
29 pub revisions: Vec<DependencyVerificationInput>,
30}
31
32impl DependencyVerificationRequest {
33 pub fn validate(&self) -> Result<(), FeedbackContractError> {
34 if self.revisions.is_empty() || self.revisions.len() > 256 {
35 return Err(FeedbackContractError::Bounds);
36 }
37 let mut graph = BTreeMap::new();
38 for revision in &self.revisions {
39 validate_relative_path(&revision.relative_root)?;
40 if !matches!(revision.exact_commit.len(), 40 | 64)
41 || !revision
42 .exact_commit
43 .bytes()
44 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
45 || revision.dependencies.len() > 256
46 || graph.insert(&revision.revision_id, revision).is_some()
47 {
48 return Err(FeedbackContractError::IncompleteManifest);
49 }
50 }
51 let mut active = BTreeSet::new();
52 let mut visited = BTreeSet::new();
53 visit(&self.root_revision_id, &graph, &mut active, &mut visited)?;
54 if visited.len() != graph.len() {
55 return Err(FeedbackContractError::IncompleteManifest);
56 }
57 Ok(())
58 }
59}
60
61fn visit<'a>(
62 id: &'a ResourceRevisionId,
63 graph: &BTreeMap<&'a ResourceRevisionId, &'a DependencyVerificationInput>,
64 active: &mut BTreeSet<&'a ResourceRevisionId>,
65 visited: &mut BTreeSet<&'a ResourceRevisionId>,
66) -> Result<(), FeedbackContractError> {
67 if active.contains(id) {
68 return Err(FeedbackContractError::DependencyCycle);
69 }
70 if visited.contains(id) {
71 return Ok(());
72 }
73 let node = graph
74 .get(id)
75 .ok_or(FeedbackContractError::IncompleteManifest)?;
76 active.insert(id);
77 let mut unique = BTreeSet::new();
78 for dependency in &node.dependencies {
79 if !unique.insert(dependency) {
80 return Err(FeedbackContractError::IncompleteManifest);
81 }
82 visit(dependency, graph, active, visited)?;
83 }
84 active.remove(id);
85 visited.insert(id);
86 Ok(())
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
90pub struct VerifiedRevisionManifest {
91 pub provenance: DependencyVerificationInput,
92 pub content_digest: ContentDigest,
93 pub file_count: u32,
94 pub bytes_verified: bool,
95 pub modes_verified: bool,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
99pub struct DependencyVerificationManifest {
100 pub id: DependencyVerificationId,
101 pub version: u16,
102 pub root_revision_id: ResourceRevisionId,
103 pub bundle_digest: ContentDigest,
104 pub revisions: Vec<VerifiedRevisionManifest>,
105 pub verified_at: DateTime<Utc>,
106}
107
108impl DependencyVerificationManifest {
109 pub fn validate_complete(&self) -> Result<(), FeedbackContractError> {
110 if self.version != 1
111 || self.revisions.iter().any(|revision| {
112 !revision.bytes_verified || !revision.modes_verified || revision.file_count == 0
113 })
114 {
115 return Err(FeedbackContractError::IncompleteManifest);
116 }
117 DependencyVerificationRequest {
118 root_revision_id: self.root_revision_id.clone(),
119 revisions: self
120 .revisions
121 .iter()
122 .map(|revision| revision.provenance.clone())
123 .collect(),
124 }
125 .validate()
126 }
127}