Skip to main content

systemprompt_models/feedback/
receipts.rs

1//! Consumer receipts: the authenticated device, byte-exact file readbacks and
2//! the installation plan a consumer acknowledges.
3//!
4//! Copyright (c) systemprompt.io — Business Source License 1.1.
5//! See <https://systemprompt.io> for licensing details.
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use systemprompt_identifiers::{
10    ConsumerInstallationId, DeviceId, InstallationReceiptId, ManagedResourceId, NativeSessionId,
11    PublicationId, ResourceRevisionId, UserId,
12};
13
14use super::{ContentDigest, EvaluatorClient, FeedbackContractError, validate_relative_path};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct AuthenticatedConsumerDevice {
18    pub consumer_id: UserId,
19    pub device_id: DeviceId,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
23#[serde(rename_all = "snake_case")]
24pub enum ReadbackStatus {
25    Verified,
26    Mismatch,
27    Unavailable,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
31#[serde(deny_unknown_fields)]
32pub struct FileReadback {
33    pub revision_id: ResourceRevisionId,
34    pub path: String,
35    pub digest: ContentDigest,
36    pub bytes: u64,
37    pub executable: bool,
38    pub content_check: ReadbackStatus,
39    pub mode_check: ReadbackStatus,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
43#[serde(deny_unknown_fields)]
44pub struct RuntimeFileReadback {
45    pub path: String,
46    pub digest: ContentDigest,
47    pub bytes: u64,
48    pub executable: bool,
49    pub content_check: ReadbackStatus,
50    pub mode_check: ReadbackStatus,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
54pub struct InstallationPlanFile {
55    pub path: String,
56    pub bytes: Vec<u8>,
57    pub executable: bool,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
61pub struct ConsumerInstallationPlan {
62    pub publication_id: PublicationId,
63    pub resource_id: ManagedResourceId,
64    pub revision_id: ResourceRevisionId,
65    pub generation: i64,
66    pub bundle_digest: ContentDigest,
67    pub host: EvaluatorClient,
68    pub canonical_files: Vec<FileReadback>,
69    pub runtime_files: Vec<InstallationPlanFile>,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
73#[serde(deny_unknown_fields)]
74pub struct ConsumerReceiptRequest {
75    pub installation_id: ConsumerInstallationId,
76    pub publication_id: PublicationId,
77    pub resource_id: ManagedResourceId,
78    pub revision_id: ResourceRevisionId,
79    pub generation: i64,
80    pub bundle_digest: ContentDigest,
81    pub host: EvaluatorClient,
82    pub observed_at: DateTime<Utc>,
83    pub files: Vec<FileReadback>,
84    #[serde(default)]
85    pub runtime_files: Vec<RuntimeFileReadback>,
86}
87
88impl ConsumerReceiptRequest {
89    pub fn validate(&self) -> Result<(), FeedbackContractError> {
90        if self.generation < 1 || self.files.is_empty() || self.files.len() > 4096 {
91            return Err(FeedbackContractError::Bounds);
92        }
93        let mut paths = std::collections::BTreeSet::new();
94        for file in &self.files {
95            validate_relative_path(&file.path)?;
96            if !paths.insert((&file.revision_id, &file.path)) {
97                return Err(FeedbackContractError::IncompleteManifest);
98            }
99        }
100        let mut runtime_paths = std::collections::BTreeSet::new();
101        if self.runtime_files.len() > 8192 {
102            return Err(FeedbackContractError::Bounds);
103        }
104        for file in &self.runtime_files {
105            validate_relative_path(&file.path)?;
106            if !runtime_paths.insert(&file.path) {
107                return Err(FeedbackContractError::IncompleteManifest);
108            }
109        }
110        Ok(())
111    }
112
113    pub fn fully_verified(&self) -> bool {
114        self.validate().is_ok()
115            && !self.runtime_files.is_empty()
116            && self.runtime_files.iter().all(|file| {
117                file.content_check == ReadbackStatus::Verified
118                    && file.mode_check == ReadbackStatus::Verified
119            })
120            && self.files.iter().all(|file| {
121                file.content_check == ReadbackStatus::Verified
122                    && file.mode_check == ReadbackStatus::Verified
123            })
124    }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
128#[serde(deny_unknown_fields)]
129pub struct SessionBindingRequest {
130    pub receipt_id: InstallationReceiptId,
131    pub host: EvaluatorClient,
132    pub session_id: NativeSessionId,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
136#[serde(rename_all = "snake_case")]
137pub enum ReceiptAcknowledgement {
138    Accepted,
139    IdenticalRetry,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
143pub struct ConsumerReceiptResponse {
144    pub receipt_id: InstallationReceiptId,
145    pub acknowledgement: ReceiptAcknowledgement,
146    pub acknowledged_at: DateTime<Utc>,
147    pub fully_verified: bool,
148}