Skip to main content

synth_ai_core/data/
context_override.rs

1//! Context override types for unified optimization.
2//!
3//! Context overrides allow modifying task app behavior through file artifacts,
4//! environment variables, and preflight scripts.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Status of a context override application.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum ApplicationStatus {
13    /// Successfully applied.
14    Applied,
15    /// Partially applied (some components succeeded).
16    Partial,
17    /// Failed to apply.
18    Failed,
19    /// Skipped (not applicable).
20    Skipped,
21}
22
23impl ApplicationStatus {
24    /// Returns true if the override was at least partially successful.
25    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/// Type of error during override application.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum ApplicationErrorType {
40    /// Validation error (invalid content).
41    Validation,
42    /// Path traversal attempt detected.
43    PathTraversal,
44    /// Permission denied.
45    Permission,
46    /// Size limit exceeded.
47    SizeLimit,
48    /// Operation timed out.
49    Timeout,
50    /// Runtime error during application.
51    Runtime,
52    /// Target not found.
53    NotFound,
54    /// Unknown error.
55    Unknown,
56}
57
58impl Default for ApplicationErrorType {
59    fn default() -> Self {
60        Self::Unknown
61    }
62}
63
64/// A context override containing file artifacts, env vars, and scripts.
65#[derive(Debug, Clone, Default, Serialize, Deserialize)]
66pub struct ContextOverride {
67    /// File artifacts to write (path -> content).
68    #[serde(default)]
69    pub file_artifacts: HashMap<String, String>,
70    /// Preflight script to execute before rollout.
71    #[serde(default)]
72    pub preflight_script: Option<String>,
73    /// Environment variables to set.
74    #[serde(default)]
75    pub env_vars: HashMap<String, String>,
76    /// Type of mutation (e.g., "replace", "patch", "append").
77    #[serde(default)]
78    pub mutation_type: Option<String>,
79    /// When this override was created.
80    #[serde(default)]
81    pub created_at: Option<String>,
82    /// Unique override ID.
83    #[serde(default)]
84    pub override_id: Option<String>,
85    /// Source of the override (e.g., "optimizer", "manual").
86    #[serde(default)]
87    pub source: Option<String>,
88    /// Priority for ordering (higher = applied later).
89    #[serde(default)]
90    pub priority: Option<i32>,
91}
92
93impl ContextOverride {
94    /// Create a new empty context override.
95    pub fn new() -> Self {
96        Self::default()
97    }
98
99    /// Check if this override is empty.
100    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    /// Get total size in bytes.
107    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    /// Add a file artifact.
115    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    /// Set the preflight script.
121    pub fn with_preflight_script(mut self, script: impl Into<String>) -> Self {
122        self.preflight_script = Some(script.into());
123        self
124    }
125
126    /// Add an environment variable.
127    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    /// Set the override ID.
133    pub fn with_id(mut self, id: impl Into<String>) -> Self {
134        self.override_id = Some(id.into());
135        self
136    }
137
138    /// Validate the override for security issues.
139    pub fn validate(&self) -> Result<(), String> {
140        // Check for path traversal in file paths
141        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        // Check for dangerous env var names
151        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    /// Get file count.
162    pub fn file_count(&self) -> usize {
163        self.file_artifacts.len()
164    }
165
166    /// Get env var count.
167    pub fn env_var_count(&self) -> usize {
168        self.env_vars.len()
169    }
170}
171
172/// Result of applying a context override.
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct ContextOverrideStatus {
175    /// The override ID.
176    #[serde(default)]
177    pub override_id: Option<String>,
178    /// Application status.
179    pub status: ApplicationStatus,
180    /// Error type if failed.
181    #[serde(default)]
182    pub error_type: Option<ApplicationErrorType>,
183    /// Error message if failed.
184    #[serde(default)]
185    pub error_message: Option<String>,
186    /// Files successfully applied.
187    #[serde(default)]
188    pub files_applied: Vec<String>,
189    /// Files that failed.
190    #[serde(default)]
191    pub files_failed: Vec<String>,
192    /// Env vars successfully applied.
193    #[serde(default)]
194    pub env_vars_applied: Vec<String>,
195    /// Whether preflight script succeeded.
196    #[serde(default)]
197    pub preflight_succeeded: Option<bool>,
198    /// Duration in milliseconds.
199    #[serde(default)]
200    pub duration_ms: Option<i64>,
201}
202
203impl ContextOverrideStatus {
204    /// Create a success status.
205    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    /// Create a failure status.
220    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    /// Mark files as applied.
235    pub fn with_files_applied(mut self, files: Vec<String>) -> Self {
236        self.files_applied = files;
237        self
238    }
239
240    /// Set duration.
241    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}