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    #[serde(rename = "overall_status", alias = "status")]
180    pub overall_status: ApplicationStatus,
181    /// Error type if failed.
182    #[serde(default)]
183    pub error_type: Option<ApplicationErrorType>,
184    /// Error message if failed.
185    #[serde(default)]
186    pub error_message: Option<String>,
187    /// Files successfully applied.
188    #[serde(default)]
189    pub files_applied: Vec<String>,
190    /// Files that failed.
191    #[serde(default)]
192    pub files_failed: Vec<String>,
193    /// Env vars successfully applied.
194    #[serde(default)]
195    pub env_vars_applied: Vec<String>,
196    /// Whether preflight script succeeded.
197    #[serde(default)]
198    pub preflight_succeeded: Option<bool>,
199    /// Duration in milliseconds.
200    #[serde(default)]
201    pub duration_ms: Option<i64>,
202}
203
204impl ContextOverrideStatus {
205    /// Create a success status.
206    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    /// Create a failure status.
221    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    /// Mark files as applied.
240    pub fn with_files_applied(mut self, files: Vec<String>) -> Self {
241        self.files_applied = files;
242        self
243    }
244
245    /// Set duration.
246    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}