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 pub status: ApplicationStatus,
180 #[serde(default)]
182 pub error_type: Option<ApplicationErrorType>,
183 #[serde(default)]
185 pub error_message: Option<String>,
186 #[serde(default)]
188 pub files_applied: Vec<String>,
189 #[serde(default)]
191 pub files_failed: Vec<String>,
192 #[serde(default)]
194 pub env_vars_applied: Vec<String>,
195 #[serde(default)]
197 pub preflight_succeeded: Option<bool>,
198 #[serde(default)]
200 pub duration_ms: Option<i64>,
201}
202
203impl ContextOverrideStatus {
204 pub fn success(override_id: Option<String>) -> Self {
206 Self {
207 override_id,
208 status: ApplicationStatus::Applied,
209 error_type: None,
210 error_message: None,
211 files_applied: Vec::new(),
212 files_failed: Vec::new(),
213 env_vars_applied: Vec::new(),
214 preflight_succeeded: None,
215 duration_ms: None,
216 }
217 }
218
219 pub fn failure(override_id: Option<String>, error_type: ApplicationErrorType, message: impl Into<String>) -> Self {
221 Self {
222 override_id,
223 status: ApplicationStatus::Failed,
224 error_type: Some(error_type),
225 error_message: Some(message.into()),
226 files_applied: Vec::new(),
227 files_failed: Vec::new(),
228 env_vars_applied: Vec::new(),
229 preflight_succeeded: None,
230 duration_ms: None,
231 }
232 }
233
234 pub fn with_files_applied(mut self, files: Vec<String>) -> Self {
236 self.files_applied = files;
237 self
238 }
239
240 pub fn with_duration(mut self, ms: i64) -> Self {
242 self.duration_ms = Some(ms);
243 self
244 }
245}
246
247impl Default for ContextOverrideStatus {
248 fn default() -> Self {
249 Self::success(None)
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_empty_override() {
259 let override_ = ContextOverride::new();
260 assert!(override_.is_empty());
261 assert_eq!(override_.size_bytes(), 0);
262 }
263
264 #[test]
265 fn test_override_with_content() {
266 let override_ = ContextOverride::new()
267 .with_file("AGENTS.md", "# Agent Instructions\n\nBe helpful.")
268 .with_env_var("DEBUG", "true")
269 .with_id("override-001");
270
271 assert!(!override_.is_empty());
272 assert_eq!(override_.file_count(), 1);
273 assert_eq!(override_.env_var_count(), 1);
274 assert!(override_.size_bytes() > 0);
275 }
276
277 #[test]
278 fn test_validation_path_traversal() {
279 let override_ = ContextOverride::new()
280 .with_file("../etc/passwd", "malicious");
281
282 assert!(override_.validate().is_err());
283 }
284
285 #[test]
286 fn test_validation_absolute_path() {
287 let override_ = ContextOverride::new()
288 .with_file("/etc/passwd", "malicious");
289
290 assert!(override_.validate().is_err());
291 }
292
293 #[test]
294 fn test_validation_dangerous_env() {
295 let override_ = ContextOverride::new()
296 .with_env_var("PATH", "/malicious");
297
298 assert!(override_.validate().is_err());
299 }
300
301 #[test]
302 fn test_validation_success() {
303 let override_ = ContextOverride::new()
304 .with_file("config/settings.json", "{}")
305 .with_env_var("MY_VAR", "value");
306
307 assert!(override_.validate().is_ok());
308 }
309
310 #[test]
311 fn test_status_success() {
312 let status = ContextOverrideStatus::success(Some("id-1".to_string()))
313 .with_files_applied(vec!["file1.txt".to_string()])
314 .with_duration(100);
315
316 assert!(status.status.is_success());
317 assert_eq!(status.duration_ms, Some(100));
318 }
319
320 #[test]
321 fn test_status_failure() {
322 let status = ContextOverrideStatus::failure(
323 Some("id-1".to_string()),
324 ApplicationErrorType::Permission,
325 "Access denied",
326 );
327
328 assert!(!status.status.is_success());
329 assert_eq!(status.error_type, Some(ApplicationErrorType::Permission));
330 }
331
332 #[test]
333 fn test_serde() {
334 let override_ = ContextOverride::new()
335 .with_file("test.txt", "content")
336 .with_id("test-id");
337
338 let json = serde_json::to_string(&override_).unwrap();
339 let parsed: ContextOverride = serde_json::from_str(&json).unwrap();
340
341 assert_eq!(parsed.override_id, Some("test-id".to_string()));
342 assert_eq!(parsed.file_artifacts.get("test.txt"), Some(&"content".to_string()));
343 }
344}