systemprompt_models/feedback/
mod.rs1pub mod inventory;
8pub mod receipts;
9
10use serde::{Deserialize, Serialize};
11use sha2::{Digest, Sha256};
12
13use crate::bridge::host::{HostAppKind, HostKind, HostProduct};
14
15#[derive(
16 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
17)]
18#[serde(rename_all = "kebab-case")]
19pub enum EvaluatorClient {
20 ClaudeCode,
21 OpenCode,
22 Codex,
23 Hermes,
24 ClaudeDesktop,
25}
26
27impl EvaluatorClient {
28 #[must_use]
29 pub fn accepts(self, host: HostKind) -> bool {
30 Self::from(host) == self
31 }
32}
33
34impl From<HostKind> for EvaluatorClient {
35 fn from(host: HostKind) -> Self {
36 match host.product() {
37 HostProduct::Claude => match host.kind() {
38 HostAppKind::DesktopApp => Self::ClaudeDesktop,
39 HostAppKind::CliTool => Self::ClaudeCode,
40 },
41 HostProduct::Codex => Self::Codex,
42 HostProduct::OpenCode => Self::OpenCode,
43 HostProduct::Hermes => Self::Hermes,
44 }
45 }
46}
47
48#[derive(
49 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
50)]
51#[serde(try_from = "String", into = "String")]
52pub struct ContentDigest(String);
53
54impl ContentDigest {
55 pub fn of(bytes: &[u8]) -> Self {
56 Self(hex::encode(Sha256::digest(bytes)))
57 }
58
59 pub fn as_str(&self) -> &str {
60 &self.0
61 }
62}
63
64impl TryFrom<String> for ContentDigest {
65 type Error = FeedbackContractError;
66
67 fn try_from(value: String) -> Result<Self, Self::Error> {
68 if value.len() != 64
69 || !value
70 .bytes()
71 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
72 {
73 return Err(FeedbackContractError::InvalidDigest);
74 }
75 Ok(Self(value))
76 }
77}
78
79impl From<ContentDigest> for String {
80 fn from(value: ContentDigest) -> Self {
81 value.0
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
86pub enum FeedbackContractError {
87 #[error("Expected a lowercase SHA-256 digest")]
88 InvalidDigest,
89 #[error("Invalid relative path")]
90 InvalidPath,
91 #[error("Verification manifest is incomplete or inconsistent")]
92 IncompleteManifest,
93 #[error("Input exceeds contract bounds")]
94 Bounds,
95}
96
97pub fn validate_relative_path(path: &str) -> Result<(), FeedbackContractError> {
98 if path.is_empty()
99 || path.len() > 4096
100 || path.contains(['\\', ':', '\0'])
101 || path.split('/').any(|part| matches!(part, "" | "." | ".."))
102 {
103 return Err(FeedbackContractError::InvalidPath);
104 }
105 Ok(())
106}