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}