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