1use crate::boundaries::{Guardrail, WriteAccess, WriteGovernance};
9use crate::error::PeError;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ExecutionMetrics {
31 pub output_tokens: u32,
32 pub tool_calls_made: u32,
33 pub output_text: String,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct WriteRecord {
55 pub destination: String,
57 pub key: String,
59 pub has_grant: bool,
61}
62
63impl Guardrail {
64 #[must_use = "this returns a Result that must be checked"]
72 pub fn validate(&self, metrics: &ExecutionMetrics) -> Result<(), PeError> {
73 match self {
74 Self::MaxOutputTokens(max) => {
75 if metrics.output_tokens > *max {
76 return Err(PeError::GuardrailViolation {
77 guardrail: format!("MaxOutputTokens({})", max),
78 details: format!("output was {} tokens", metrics.output_tokens),
79 });
80 }
81 }
82 Self::MaxToolCallsPerTurn(max) => {
83 if metrics.tool_calls_made > *max {
84 return Err(PeError::GuardrailViolation {
85 guardrail: format!("MaxToolCallsPerTurn({})", max),
86 details: format!("{} calls made", metrics.tool_calls_made),
87 });
88 }
89 }
90 Self::MustCiteSources | Self::NoCodeExecution => {}
92 Self::Custom { .. } => {}
94 }
95 Ok(())
96 }
97}
98
99impl WriteGovernance {
100 #[must_use = "this returns a Result that must be checked"]
106 pub fn validate_writes(&self, writes: &[WriteRecord]) -> Result<(), PeError> {
107 for write in writes {
108 let access = match write.destination.as_str() {
109 "own_memory" => &self.own_memory,
110 "collective" => &self.collective,
111 "vault" => &self.vault,
112 "task_store" => &self.task_store,
113 _ => continue, };
115 match access {
116 WriteAccess::ReadOnly => {
117 return Err(PeError::WriteGovernanceViolation {
118 destination: write.destination.clone(),
119 reason: "ReadOnly — no writes permitted".into(),
120 });
121 }
122 WriteAccess::RequiresGrant if !write.has_grant => {
123 return Err(PeError::WriteGovernanceViolation {
124 destination: write.destination.clone(),
125 reason: "RequiresGrant — no grant obtained".into(),
126 });
127 }
128 _ => {} }
130 }
131 Ok(())
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use crate::boundaries::WriteGovernance;
139
140 fn metrics(output_tokens: u32, tool_calls: u32) -> ExecutionMetrics {
141 ExecutionMetrics {
142 output_tokens,
143 tool_calls_made: tool_calls,
144 output_text: String::new(),
145 }
146 }
147
148 #[test]
149 fn max_output_tokens_within_limit() {
150 let g = Guardrail::MaxOutputTokens(2000);
151 assert!(g.validate(&metrics(1500, 0)).is_ok());
152 }
153
154 #[test]
155 fn max_output_tokens_violation() {
156 let g = Guardrail::MaxOutputTokens(2000);
157 let err = g.validate(&metrics(2500, 0)).unwrap_err();
158 assert!(matches!(err, PeError::GuardrailViolation { .. }));
159 }
160
161 #[test]
162 fn max_tool_calls_within_limit() {
163 let g = Guardrail::MaxToolCallsPerTurn(5);
164 assert!(g.validate(&metrics(0, 3)).is_ok());
165 }
166
167 #[test]
168 fn max_tool_calls_violation() {
169 let g = Guardrail::MaxToolCallsPerTurn(5);
170 let err = g.validate(&metrics(0, 8)).unwrap_err();
171 assert!(matches!(err, PeError::GuardrailViolation { .. }));
172 }
173
174 #[test]
175 fn write_governance_free_allows_all() {
176 let gov = WriteGovernance::default(); let writes = vec![WriteRecord {
178 destination: "own_memory".into(),
179 key: "test".into(),
180 has_grant: false,
181 }];
182 assert!(gov.validate_writes(&writes).is_ok());
183 }
184
185 #[test]
186 fn write_governance_read_only_rejects() {
187 let gov = WriteGovernance {
188 vault: WriteAccess::ReadOnly,
189 ..Default::default()
190 };
191 let writes = vec![WriteRecord {
192 destination: "vault".into(),
193 key: "secret".into(),
194 has_grant: false,
195 }];
196 let err = gov.validate_writes(&writes).unwrap_err();
197 assert!(matches!(err, PeError::WriteGovernanceViolation { .. }));
198 }
199
200 #[test]
201 fn write_governance_requires_grant_without_grant_rejects() {
202 let gov = WriteGovernance {
203 collective: WriteAccess::RequiresGrant,
204 ..Default::default()
205 };
206 let writes = vec![WriteRecord {
207 destination: "collective".into(),
208 key: "notes".into(),
209 has_grant: false,
210 }];
211 assert!(gov.validate_writes(&writes).is_err());
212
213 let writes_granted = vec![WriteRecord {
215 destination: "collective".into(),
216 key: "notes".into(),
217 has_grant: true,
218 }];
219 assert!(gov.validate_writes(&writes_granted).is_ok());
220 }
221}