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