1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum ApplicationStatus {
13 Applied,
15 Partial,
17 Failed,
19 Skipped,
21}
22
23impl ApplicationStatus {
24 pub fn is_success(&self) -> bool {
26 matches!(self, Self::Applied | Self::Partial)
27 }
28}
29
30impl Default for ApplicationStatus {
31 fn default() -> Self {
32 Self::Applied
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum ApplicationErrorType {
40 Validation,
42 PathTraversal,
44 Permission,
46 SizeLimit,
48 Timeout,
50 Runtime,
52 NotFound,
54 Unknown,
56}
57
58impl Default for ApplicationErrorType {
59 fn default() -> Self {
60 Self::Unknown
61 }
62}
63
64#[derive(Debug, Clone, Default, Serialize, Deserialize)]
66pub struct ContextOverride {
67 #[serde(default)]
69 pub file_artifacts: HashMap<String, String>,
70 #[serde(default)]
72 pub preflight_script: Option<String>,
73 #[serde(default)]
75 pub env_vars: HashMap<String, String>,
76 #[serde(default)]
78 pub mutation_type: Option<String>,
79 #[serde(default)]
81 pub created_at: Option<String>,
82 #[serde(default)]
84 pub override_id: Option<String>,
85 #[serde(default)]
87 pub source: Option<String>,
88 #[serde(default)]
90 pub priority: Option<i32>,
91}
92
93impl ContextOverride {
94 pub fn new() -> Self {
96 Self::default()
97 }
98
99 pub fn is_empty(&self) -> bool {
101 self.file_artifacts.is_empty()
102 && self.preflight_script.is_none()
103 && self.env_vars.is_empty()
104 }
105
106 pub fn size_bytes(&self) -> usize {
108 let files: usize = self.file_artifacts.values().map(|v| v.len()).sum();
109 let script = self.preflight_script.as_ref().map(|s| s.len()).unwrap_or(0);
110 let env: usize = self.env_vars.iter().map(|(k, v)| k.len() + v.len()).sum();
111 files + script + env
112 }
113
114 pub fn with_file(mut self, path: impl Into<String>, content: impl Into<String>) -> Self {
116 self.file_artifacts.insert(path.into(), content.into());
117 self
118 }
119
120 pub fn with_preflight_script(mut self, script: impl Into<String>) -> Self {
122 self.preflight_script = Some(script.into());
123 self
124 }
125
126 pub fn with_env_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
128 self.env_vars.insert(key.into(), value.into());
129 self
130 }
131
132 pub fn with_id(mut self, id: impl Into<String>) -> Self {
134 self.override_id = Some(id.into());
135 self
136 }
137
138 pub fn validate(&self) -> Result<(), String> {
140 for path in self.file_artifacts.keys() {
142 if path.contains("..") {
143 return Err(format!("Path traversal detected in: {}", path));
144 }
145 if path.starts_with('/') {
146 return Err(format!("Absolute paths not allowed: {}", path));
147 }
148 }
149
150 const DANGEROUS_VARS: &[&str] = &["PATH", "LD_PRELOAD", "LD_LIBRARY_PATH"];
152 for key in self.env_vars.keys() {
153 if DANGEROUS_VARS.contains(&key.as_str()) {
154 return Err(format!("Setting {} is not allowed", key));
155 }
156 }
157
158 Ok(())
159 }
160
161 pub fn file_count(&self) -> usize {
163 self.file_artifacts.len()
164 }
165
166 pub fn env_var_count(&self) -> usize {
168 self.env_vars.len()
169 }
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct ContextOverrideStatus {
175 #[serde(default)]
177 pub override_id: Option<String>,
178 #[serde(rename = "overall_status", alias = "status")]
180 pub overall_status: ApplicationStatus,
181 #[serde(default)]
183 pub error_type: Option<ApplicationErrorType>,
184 #[serde(default)]
186 pub error_message: Option<String>,
187 #[serde(default)]
189 pub files_applied: Vec<String>,
190 #[serde(default)]
192 pub files_failed: Vec<String>,
193 #[serde(default)]
195 pub env_vars_applied: Vec<String>,
196 #[serde(default)]
198 pub preflight_succeeded: Option<bool>,
199 #[serde(default)]
201 pub duration_ms: Option<i64>,
202}
203
204impl ContextOverrideStatus {
205 pub fn success(override_id: Option<String>) -> Self {
207 Self {
208 override_id,
209 overall_status: ApplicationStatus::Applied,
210 error_type: None,
211 error_message: None,
212 files_applied: Vec::new(),
213 files_failed: Vec::new(),
214 env_vars_applied: Vec::new(),
215 preflight_succeeded: None,
216 duration_ms: None,
217 }
218 }
219
220 pub fn failure(
222 override_id: Option<String>,
223 error_type: ApplicationErrorType,
224 message: impl Into<String>,
225 ) -> Self {
226 Self {
227 override_id,
228 overall_status: ApplicationStatus::Failed,
229 error_type: Some(error_type),
230 error_message: Some(message.into()),
231 files_applied: Vec::new(),
232 files_failed: Vec::new(),
233 env_vars_applied: Vec::new(),
234 preflight_succeeded: None,
235 duration_ms: None,
236 }
237 }
238
239 pub fn with_files_applied(mut self, files: Vec<String>) -> Self {
241 self.files_applied = files;
242 self
243 }
244
245 pub fn with_duration(mut self, ms: i64) -> Self {
247 self.duration_ms = Some(ms);
248 self
249 }
250}
251
252impl Default for ContextOverrideStatus {
253 fn default() -> Self {
254 Self::success(None)
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn test_empty_override() {
264 let override_ = ContextOverride::new();
265 assert!(override_.is_empty());
266 assert_eq!(override_.size_bytes(), 0);
267 }
268
269 #[test]
270 fn test_override_with_content() {
271 let override_ = ContextOverride::new()
272 .with_file("AGENTS.md", "# Agent Instructions\n\nBe helpful.")
273 .with_env_var("DEBUG", "true")
274 .with_id("override-001");
275
276 assert!(!override_.is_empty());
277 assert_eq!(override_.file_count(), 1);
278 assert_eq!(override_.env_var_count(), 1);
279 assert!(override_.size_bytes() > 0);
280 }
281
282 #[test]
283 fn test_validation_path_traversal() {
284 let override_ = ContextOverride::new().with_file("../etc/passwd", "malicious");
285
286 assert!(override_.validate().is_err());
287 }
288
289 #[test]
290 fn test_validation_absolute_path() {
291 let override_ = ContextOverride::new().with_file("/etc/passwd", "malicious");
292
293 assert!(override_.validate().is_err());
294 }
295
296 #[test]
297 fn test_validation_dangerous_env() {
298 let override_ = ContextOverride::new().with_env_var("PATH", "/malicious");
299
300 assert!(override_.validate().is_err());
301 }
302
303 #[test]
304 fn test_validation_success() {
305 let override_ = ContextOverride::new()
306 .with_file("config/settings.json", "{}")
307 .with_env_var("MY_VAR", "value");
308
309 assert!(override_.validate().is_ok());
310 }
311
312 #[test]
313 fn test_status_success() {
314 let status = ContextOverrideStatus::success(Some("id-1".to_string()))
315 .with_files_applied(vec!["file1.txt".to_string()])
316 .with_duration(100);
317
318 assert!(status.overall_status.is_success());
319 assert_eq!(status.duration_ms, Some(100));
320 }
321
322 #[test]
323 fn test_status_failure() {
324 let status = ContextOverrideStatus::failure(
325 Some("id-1".to_string()),
326 ApplicationErrorType::Permission,
327 "Access denied",
328 );
329
330 assert!(!status.overall_status.is_success());
331 assert_eq!(status.error_type, Some(ApplicationErrorType::Permission));
332 }
333
334 #[test]
335 fn test_serde() {
336 let override_ = ContextOverride::new()
337 .with_file("test.txt", "content")
338 .with_id("test-id");
339
340 let json = serde_json::to_string(&override_).unwrap();
341 let parsed: ContextOverride = serde_json::from_str(&json).unwrap();
342
343 assert_eq!(parsed.override_id, Some("test-id".to_string()));
344 assert_eq!(
345 parsed.file_artifacts.get("test.txt"),
346 Some(&"content".to_string())
347 );
348 }
349}