Skip to main content

systemprompt_models/feedback/
verification.rs

1//! Dependency verification: the request a consumer submits and the manifest of
2//! verified revisions the gateway answers with.
3//!
4//! Copyright (c) systemprompt.io — Business Source License 1.1.
5//! See <https://systemprompt.io> for licensing details.
6
7use 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}