Skip to main content

icebox/core/
executor.rs

1use tracing::info;
2
3use crate::core::module::{LoadedModule, ModuleError, ModuleResult};
4use crate::core::safety::{
5    make_config_policy, now_secs, Charter, ConfigPolicy, DecisionRecord, Evidence,
6    MemoryEntry, MemoryKind, PolicyContext, PolicyDecision, PolicyEngine, PolicySet, Preflight,
7    PreflightError, ReasoningTrace, RiskLevel, ScopeManager,
8};
9
10const MAX_DECISIONS: usize = 1000;
11const MAX_EVIDENCE: usize = 2000;
12const MAX_TRACES: usize = 1000;
13const MAX_MEMORIES: usize = 1000;
14
15#[derive(Debug, thiserror::Error)]
16pub enum ExecutorError {
17    #[error(transparent)]
18    Preflight(#[from] PreflightError),
19    #[error(transparent)]
20    Module(#[from] ModuleError),
21    #[error("sandbox error: {0}")]
22    Sandbox(String),
23}
24
25#[derive(Debug)]
26pub struct ModuleExecutor {
27    pub charter: Charter,
28    pub scope: ScopeManager,
29    pub max_risk: RiskLevel,
30    pub policy_set: PolicySet,
31    pub sandbox_required: bool,
32    pub tier: crate::core::safety::Tier,
33    pub decisions: Vec<DecisionRecord>,
34    pub evidence: Vec<Evidence>,
35    pub traces: Vec<ReasoningTrace>,
36    pub memories: Vec<MemoryEntry>,
37}
38
39impl ModuleExecutor {
40    pub fn new(charter: Charter, scope: ScopeManager, max_risk: RiskLevel) -> Self {
41        ModuleExecutor {
42            charter,
43            scope,
44            max_risk,
45            policy_set: PolicySet::default(),
46            sandbox_required: false,
47            tier: crate::core::safety::Tier::Fridge,
48            decisions: Vec::new(),
49            evidence: Vec::new(),
50            traces: Vec::new(),
51            memories: Vec::new(),
52        }
53    }
54
55    pub fn policy(&self, context: PolicyContext) -> ConfigPolicy {
56        let mut policy = make_config_policy(self.max_risk, context, &self.policy_set);
57        if let Some(thr) = self.tier.cvss_threshold() {
58            policy.rules.add_rule(crate::core::safety::PolicyRule::DenyIfCvssAbove(thr));
59        }
60        policy
61    }
62
63    pub fn recent_traces(&self, n: usize) -> Vec<ReasoningTrace> {
64        let end = self.traces.len();
65        let start = end.saturating_sub(n);
66        self.traces[start..].to_vec()
67    }
68
69    pub fn recent_memories(&self, n: usize) -> Vec<MemoryEntry> {
70        let end = self.memories.len();
71        let start = end.saturating_sub(n);
72        self.memories[start..].to_vec()
73    }
74
75    pub fn remember(&mut self, kind: MemoryKind, text: impl Into<String>) {
76        self.memories.push(MemoryEntry {
77            at: now_secs(),
78            kind,
79            text: text.into(),
80        });
81        let overflow = self.memories.len().saturating_sub(MAX_MEMORIES);
82        if overflow > 0 {
83            self.memories.drain(..overflow);
84        }
85    }
86
87    pub fn record_trace(&mut self, trace: ReasoningTrace) {
88        self.traces.push(trace);
89        let overflow = self.traces.len().saturating_sub(MAX_TRACES);
90        if overflow > 0 {
91            self.traces.drain(..overflow);
92        }
93    }
94
95    pub fn recent_decisions(&self, n: usize) -> Vec<DecisionRecord> {
96        let end = self.decisions.len();
97        let start = end.saturating_sub(n);
98        self.decisions[start..].to_vec()
99    }
100
101    pub fn recent_evidence(&self, n: usize) -> Vec<Evidence> {
102        let end = self.evidence.len();
103        let start = end.saturating_sub(n);
104        self.evidence[start..].to_vec()
105    }
106
107    pub async fn preflight(
108        &self,
109        loaded: &LoadedModule,
110        target: &str,
111        destructive_override: Option<bool>,
112        approved: bool,
113        context: PolicyContext,
114    ) -> Preflight {
115        use crate::core::module::Intent;
116        let destructive = destructive_override.unwrap_or_else(|| {
117            loaded.info.effective_intents().contains(&Intent::Modify)
118        });
119        
120
121
122        Preflight {
123            target: target.to_string(),
124            charter_accepted: self.charter.accepted,
125            in_scope: self.scope.is_in_scope(target),
126            risk: loaded.info.effective_impact(),
127            destructive,
128            approved,
129            capabilities: loaded.info.capabilities.clone(),
130            intents: loaded.info.effective_intents(),
131            context,
132            cvss: None,
133        }
134    }
135
136    #[allow(clippy::too_many_arguments)]
137    pub async fn execute(
138        &mut self,
139        loaded: &mut LoadedModule,
140        target: &str,
141        destructive_override: Option<bool>,
142        approved: bool,
143        context: PolicyContext,
144        job_id: Option<u64>,
145        sandbox: bool,
146        engine: Option<crate::core::sandbox::SandboxEngineType>,
147    ) -> Result<ModuleResult, ExecutorError> {
148        let pf = self.preflight(loaded, target, destructive_override, approved, context).await;
149        let policy = self.policy(context);
150        let decision = policy.evaluate(&pf.to_request());
151        self.record_decision(&loaded.info.name, &pf, &decision);
152        pf.check(&policy)?;
153
154        if (self.sandbox_required || self.tier.requires_sandbox()) && !sandbox {
155            let reason = format!("operational tier {} requires sandbox isolation", self.tier);
156            self.record_decision(&loaded.info.name, &pf, &PolicyDecision::Deny(reason.clone()));
157            return Err(ExecutorError::Preflight(PreflightError::Denied(reason)));
158        }
159
160        if self.tier.requires_explicit_approval() && !approved {
161            let reason = format!("operational tier {} requires explicit operator approval", self.tier);
162            self.record_decision(
163                &loaded.info.name,
164                &pf,
165                &PolicyDecision::RequireApproval(reason.clone()),
166            );
167            return Err(ExecutorError::Preflight(PreflightError::ApprovalRequired));
168        }
169
170        info!(
171            target = %target,
172            module = %loaded.info.name,
173            risk = %pf.risk.as_str(),
174            destructive = pf.destructive,
175            sandbox = sandbox,
176            "executor: preflight passed"
177        );
178        let result = if sandbox {
179            self.run_sandboxed(
180                loaded,
181                target,
182                engine.unwrap_or(crate::core::sandbox::SandboxEngineType::Docker),
183                context,
184            )
185            .await?
186        } else {
187            match loaded.module.run().await {
188                Ok(r) => r,
189                Err(e) => {
190                    let reason = format!("module execution failed: {e}");
191                    self.record_decision(
192                        &loaded.info.name,
193                        &pf,
194                        &PolicyDecision::Deny(reason.clone()),
195                    );
196                    return Err(ExecutorError::Module(e));
197                }
198            }
199        };
200
201        let denied = policy.denied_payload(&result);
202        if !denied.is_empty() {
203            let reason = format!("payload matched denied pattern: {denied}");
204            self.record_decision(&loaded.info.name, &pf, &PolicyDecision::Deny(reason.clone()));
205            let mut blocked = result;
206            blocked.success = false;
207            blocked.evidence.push(format!("[BLOCKED:payload] {denied}"));
208            blocked.data = serde_json::Value::Null;
209            self.record_evidence(&loaded.info.name, target, &blocked, job_id);
210            return Ok(blocked);
211        }
212
213        self.record_evidence(&loaded.info.name, target, &result, job_id);
214        Ok(result)
215    }
216
217    fn record_decision(
218        &mut self,
219        module: &str,
220        pf: &Preflight,
221        decision: &crate::core::safety::PolicyDecision,
222    ) {
223        self.decisions.push(DecisionRecord {
224            at: now_secs(),
225            target: pf.target.clone(),
226            module: module.to_string(),
227            capabilities: pf.capabilities.clone(),
228            intents: pf.intents.clone(),
229            impact: pf.risk,
230            context: pf.context,
231            decision: decision.clone(),
232        });
233        let overflow = self.decisions.len().saturating_sub(MAX_DECISIONS);
234        if overflow > 0 {
235            self.decisions.drain(..overflow);
236        }
237    }
238
239    fn record_evidence(
240        &mut self,
241        module: &str,
242        target: &str,
243        result: &ModuleResult,
244        job_id: Option<u64>,
245    ) {
246        let seq_start = self.evidence.len();
247        for (i, content) in result.evidence.iter().enumerate() {
248            self.evidence.push(Evidence::new(
249                module,
250                target,
251                content,
252                job_id,
253                seq_start + i,
254            ));
255        }
256        let overflow = self.evidence.len().saturating_sub(MAX_EVIDENCE);
257        if overflow > 0 {
258            self.evidence.drain(..overflow);
259        }
260    }
261    async fn run_sandboxed(
262        &mut self,
263        loaded: &crate::core::module::LoadedModule,
264        target: &str,
265        engine: crate::core::sandbox::SandboxEngineType,
266        context: PolicyContext,
267    ) -> Result<ModuleResult, ExecutorError> {
268        use crate::core::sandbox::Sandbox;
269        let image = "icebox-sandbox:latest".to_string();
270        let module_name = loaded.info.name.clone();
271        match Sandbox::freeze(engine, target, &image).await {
272            Ok(sandbox) => {
273                info!(
274                    container = %sandbox.container_id(),
275                    "[SANDBOX] container frozen"
276                );
277                let options = loaded.module.options_json();
278                let mut result = match sandbox
279                    .exec_module(&loaded.info.name, target, &options)
280                    .await
281                {
282                    Ok(r) => r,
283                    Err(e) => ModuleResult {
284                        error: Some(format!("sandbox module exec failed: {e}")),
285                        ..Default::default()
286                    },
287                };
288                let logs = sandbox.capture_logs().await;
289                result.evidence.extend(logs);
290                result.evidence.push(format!(
291                    "[SANDBOX] Container melted: {}",
292                    sandbox.container_id()
293                ));
294                if let Err(e) = sandbox.melt().await {
295                    result
296                        .evidence
297                        .push(format!("[SANDBOX] Teardown warning: {e}"));
298                }
299                if result.error.is_some() {
300                    self.record_failure(
301                        &module_name,
302                        target,
303                        result.error.as_deref().unwrap_or("sandbox failure"),
304                        context,
305                    );
306                }
307                Ok(result)
308            }
309            Err(e) => {
310                let reason = format!(
311                    "Sandbox initialization failed: {e}. Isolation is mandatory."
312                );
313                self.record_failure(&module_name, target, &reason, context);
314                Err(ExecutorError::Sandbox(reason))
315            }
316        }
317    }
318
319    fn record_failure(
320        &mut self,
321        module: &str,
322        target: &str,
323        reason: &str,
324        context: PolicyContext,
325    ) {
326        self.decisions.push(DecisionRecord {
327            at: now_secs(),
328            target: target.to_string(),
329            module: module.to_string(),
330            capabilities: Vec::new(),
331            intents: Vec::new(),
332            impact: RiskLevel::None,
333            context,
334            decision: PolicyDecision::Deny(reason.to_string()),
335        });
336        let overflow = self.decisions.len().saturating_sub(MAX_DECISIONS);
337        if overflow > 0 {
338            self.decisions.drain(..overflow);
339        }
340    }
341}