Skip to main content

fd_policy/airlock/
config.rs

1//! Airlock configuration types
2
3use serde::{Deserialize, Serialize};
4
5/// Airlock operation mode
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
7#[serde(rename_all = "snake_case")]
8pub enum AirlockMode {
9    /// Block violations (production mode)
10    Enforce,
11    /// Log violations but don't block (testing/rollout mode) - default for safety
12    #[default]
13    Shadow,
14}
15
16/// Main Airlock configuration
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct AirlockConfig {
19    /// Operating mode (enforce or shadow)
20    #[serde(default)]
21    pub mode: AirlockMode,
22
23    /// Anti-RCE pattern detection configuration
24    #[serde(default)]
25    pub rce: RceConfig,
26
27    /// Financial circuit breaker configuration
28    #[serde(default)]
29    pub velocity: VelocityConfig,
30
31    /// Data exfiltration shield configuration
32    #[serde(default)]
33    pub exfiltration: ExfiltrationConfig,
34
35    /// Schema-drift validation configuration
36    #[serde(default)]
37    pub schema_drift: SchemaDriftConfig,
38
39    /// Behavioral-drift (per-agent rolling z-score) configuration
40    #[serde(default)]
41    pub behavioral_drift: BehavioralDriftConfig,
42}
43
44impl Default for AirlockConfig {
45    fn default() -> Self {
46        Self {
47            mode: AirlockMode::Shadow,
48            rce: RceConfig::default(),
49            velocity: VelocityConfig::default(),
50            exfiltration: ExfiltrationConfig::default(),
51            schema_drift: SchemaDriftConfig::default(),
52            behavioral_drift: BehavioralDriftConfig::default(),
53        }
54    }
55}
56
57/// Anti-RCE pattern detection configuration
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct RceConfig {
60    /// Enable RCE pattern detection
61    #[serde(default = "default_true")]
62    pub enabled: bool,
63
64    /// Tools to apply RCE detection to
65    #[serde(default = "default_rce_tools")]
66    pub target_tools: Vec<String>,
67
68    /// Custom patterns to add (in addition to built-in)
69    #[serde(default)]
70    pub custom_patterns: Vec<String>,
71}
72
73impl Default for RceConfig {
74    fn default() -> Self {
75        Self {
76            enabled: true,
77            target_tools: default_rce_tools(),
78            custom_patterns: Vec::new(),
79        }
80    }
81}
82
83/// Financial circuit breaker configuration
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct VelocityConfig {
86    /// Enable velocity checking
87    #[serde(default = "default_true")]
88    pub enabled: bool,
89
90    /// Max cost in cents per time window
91    #[serde(default = "default_max_cost_cents")]
92    pub max_cost_cents: u64,
93
94    /// Time window in seconds
95    #[serde(default = "default_window_seconds")]
96    pub window_seconds: u64,
97
98    /// Max identical calls before loop detection triggers
99    #[serde(default = "default_loop_threshold")]
100    pub loop_threshold: u32,
101}
102
103impl Default for VelocityConfig {
104    fn default() -> Self {
105        Self {
106            enabled: true,
107            max_cost_cents: default_max_cost_cents(),
108            window_seconds: default_window_seconds(),
109            loop_threshold: default_loop_threshold(),
110        }
111    }
112}
113
114/// Data exfiltration shield configuration
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct ExfiltrationConfig {
117    /// Enable exfiltration detection
118    #[serde(default = "default_true")]
119    pub enabled: bool,
120
121    /// Network tools to inspect
122    #[serde(default = "default_network_tools")]
123    pub target_tools: Vec<String>,
124
125    /// Allowed domains (whitelist)
126    #[serde(default)]
127    pub allowed_domains: Vec<String>,
128
129    /// Block raw IP addresses (prevents direct C2 connections)
130    #[serde(default = "default_true")]
131    pub block_ip_addresses: bool,
132
133    /// Run the credential-DLP scanner on outbound payloads. Detects cloud
134    /// keys / tokens / Luhn-valid PANs / mod-97-valid IBANs.
135    #[serde(default = "default_true")]
136    pub credential_dlp_enabled: bool,
137
138    /// Maximum outbound bytes to a single domain per run before the shield
139    /// kills further dispatches. `None` disables the budget. The shield
140    /// estimates per-call body size from the serialised tool input.
141    #[serde(default)]
142    pub data_budget_per_domain_bytes: Option<u64>,
143}
144
145impl Default for ExfiltrationConfig {
146    fn default() -> Self {
147        Self {
148            enabled: true,
149            target_tools: default_network_tools(),
150            allowed_domains: Vec::new(),
151            block_ip_addresses: true,
152            credential_dlp_enabled: true,
153            data_budget_per_domain_bytes: None,
154        }
155    }
156}
157
158/// Schema-drift validation configuration
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct SchemaDriftConfig {
161    /// Enable schema-drift checking
162    #[serde(default = "default_true")]
163    pub enabled: bool,
164
165    /// Risk score (0-100) assigned to drift violations. Clamped to 100.
166    #[serde(default = "default_schema_drift_risk_score")]
167    pub risk_score: u8,
168}
169
170impl Default for SchemaDriftConfig {
171    fn default() -> Self {
172        Self {
173            enabled: true,
174            risk_score: default_schema_drift_risk_score(),
175        }
176    }
177}
178
179fn default_schema_drift_risk_score() -> u8 {
180    70
181}
182
183/// Behavioral-drift (per-agent rolling z-score) configuration
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct BehavioralDriftConfig {
186    /// Enable behavioral-drift checking
187    #[serde(default = "default_true")]
188    pub enabled: bool,
189
190    /// Max observations retained per agent per dimension.
191    #[serde(default = "default_behavioral_window")]
192    pub window_size: usize,
193
194    /// Minimum observations required before drift checks fire. z-score on a
195    /// small sample is meaningless — keep this honest.
196    #[serde(default = "default_behavioral_min_observations")]
197    pub min_observations: usize,
198
199    /// Number of standard deviations from the rolling mean that triggers a
200    /// violation. 3.0σ is roughly the 99.7th percentile under a normal
201    /// distribution.
202    #[serde(default = "default_behavioral_z_threshold")]
203    pub z_threshold: f32,
204
205    /// Risk score (0-100) assigned to behavioral-drift violations.
206    #[serde(default = "default_behavioral_risk_score")]
207    pub risk_score: u8,
208}
209
210impl Default for BehavioralDriftConfig {
211    fn default() -> Self {
212        Self {
213            enabled: true,
214            window_size: default_behavioral_window(),
215            min_observations: default_behavioral_min_observations(),
216            z_threshold: default_behavioral_z_threshold(),
217            risk_score: default_behavioral_risk_score(),
218        }
219    }
220}
221
222fn default_behavioral_window() -> usize {
223    100
224}
225
226fn default_behavioral_min_observations() -> usize {
227    20
228}
229
230fn default_behavioral_z_threshold() -> f32 {
231    3.0
232}
233
234fn default_behavioral_risk_score() -> u8 {
235    65
236}
237
238/// Coherence-divergence monitor configuration.
239///
240/// Unlike the per-call layers above, the coherence monitor is *sequential* —
241/// it consumes a run trajectory and flags a stated blocking fact followed by
242/// a contradicting closure action (see [`super::coherence`]). It is driven by
243/// its own [`super::coherence::CoherenceMonitor`] rather than by
244/// [`super::inspector::AirlockInspector::inspect`], so this config is consumed
245/// directly by that monitor and is intentionally not a field of
246/// [`AirlockConfig`].
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct CoherenceConfig {
249    /// Enable coherence-divergence checking.
250    #[serde(default = "default_true")]
251    pub enabled: bool,
252
253    /// Max trajectory events between a stated blocking fact and a
254    /// contradicting closure action for them to be paired. Beyond this the
255    /// fact is treated as stale and dropped.
256    #[serde(default = "default_coherence_lookahead")]
257    pub lookahead: usize,
258
259    /// Risk score (0-100) assigned to coherence-divergence violations.
260    /// Clamped to 100.
261    #[serde(default = "default_coherence_risk_score")]
262    pub risk_score: u8,
263
264    /// Minimum confidence in `[0, 1]` required to surface a divergence —
265    /// guards the false-positive rate.
266    #[serde(default = "default_coherence_min_confidence")]
267    pub min_confidence: f64,
268}
269
270impl Default for CoherenceConfig {
271    fn default() -> Self {
272        Self {
273            enabled: true,
274            lookahead: default_coherence_lookahead(),
275            risk_score: default_coherence_risk_score(),
276            min_confidence: default_coherence_min_confidence(),
277        }
278    }
279}
280
281fn default_coherence_lookahead() -> usize {
282    8
283}
284
285fn default_coherence_risk_score() -> u8 {
286    70
287}
288
289fn default_coherence_min_confidence() -> f64 {
290    0.5
291}
292
293// =============================================================================
294// Default value functions for serde
295// =============================================================================
296
297fn default_true() -> bool {
298    true
299}
300
301fn default_max_cost_cents() -> u64 {
302    100 // $1.00
303}
304
305fn default_window_seconds() -> u64 {
306    10
307}
308
309fn default_loop_threshold() -> u32 {
310    3
311}
312
313fn default_rce_tools() -> Vec<String> {
314    vec![
315        "write_file".to_string(),
316        "create_file".to_string(),
317        "create_or_update_file".to_string(),
318        "python_repl".to_string(),
319        "bash".to_string(),
320        "execute_command".to_string(),
321        "run_script".to_string(),
322        "shell".to_string(),
323    ]
324}
325
326fn default_network_tools() -> Vec<String> {
327    vec![
328        "http_get".to_string(),
329        "http_post".to_string(),
330        "http_request".to_string(),
331        "curl".to_string(),
332        "fetch".to_string(),
333        "requests".to_string(),
334        "webhook".to_string(),
335        "send_email".to_string(),
336    ]
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn test_default_config() {
345        let config = AirlockConfig::default();
346        assert_eq!(config.mode, AirlockMode::Shadow);
347        assert!(config.rce.enabled);
348        assert!(config.velocity.enabled);
349        assert!(config.exfiltration.enabled);
350    }
351
352    #[test]
353    fn test_default_rce_tools() {
354        let tools = default_rce_tools();
355        assert!(tools.contains(&"write_file".to_string()));
356        assert!(tools.contains(&"bash".to_string()));
357    }
358
359    #[test]
360    fn test_velocity_defaults() {
361        let config = VelocityConfig::default();
362        assert_eq!(config.max_cost_cents, 100);
363        assert_eq!(config.window_seconds, 10);
364        assert_eq!(config.loop_threshold, 3);
365    }
366}