icebox-gov 0.2.4

Runtime governance for autonomous offensive security — the single seam every human operator, REST client, and LLM agent must pass through before anything touches a target.
Documentation
use tracing::info;

use crate::core::module::{LoadedModule, ModuleError, ModuleResult};
use crate::core::safety::{
    make_config_policy, now_secs, Charter, ConfigPolicy, DecisionRecord, Evidence,
    MemoryEntry, MemoryKind, PolicyContext, PolicyDecision, PolicyEngine, PolicySet, Preflight,
    PreflightError, ReasoningTrace, RiskLevel, ScopeManager,
};

const MAX_DECISIONS: usize = 1000;
const MAX_EVIDENCE: usize = 2000;
const MAX_TRACES: usize = 1000;
const MAX_MEMORIES: usize = 1000;

#[derive(Debug, thiserror::Error)]
pub enum ExecutorError {
    #[error(transparent)]
    Preflight(#[from] PreflightError),
    #[error(transparent)]
    Module(#[from] ModuleError),
    #[error("sandbox error: {0}")]
    Sandbox(String),
}

#[derive(Debug)]
pub struct ModuleExecutor {
    pub charter: Charter,
    pub scope: ScopeManager,
    pub max_risk: RiskLevel,
    pub policy_set: PolicySet,
    pub sandbox_required: bool,
    pub tier: crate::core::safety::Tier,
    pub decisions: Vec<DecisionRecord>,
    pub evidence: Vec<Evidence>,
    pub traces: Vec<ReasoningTrace>,
    pub memories: Vec<MemoryEntry>,
}

impl ModuleExecutor {
    pub fn new(charter: Charter, scope: ScopeManager, max_risk: RiskLevel) -> Self {
        ModuleExecutor {
            charter,
            scope,
            max_risk,
            policy_set: PolicySet::default(),
            sandbox_required: false,
            tier: crate::core::safety::Tier::Fridge,
            decisions: Vec::new(),
            evidence: Vec::new(),
            traces: Vec::new(),
            memories: Vec::new(),
        }
    }

    pub fn policy(&self, context: PolicyContext) -> ConfigPolicy {
        let mut policy = make_config_policy(self.max_risk, context, &self.policy_set);
        if let Some(thr) = self.tier.cvss_threshold() {
            policy.rules.add_rule(crate::core::safety::PolicyRule::DenyIfCvssAbove(thr));
        }
        policy
    }

    pub fn recent_traces(&self, n: usize) -> Vec<ReasoningTrace> {
        let end = self.traces.len();
        let start = end.saturating_sub(n);
        self.traces[start..].to_vec()
    }

    pub fn recent_memories(&self, n: usize) -> Vec<MemoryEntry> {
        let end = self.memories.len();
        let start = end.saturating_sub(n);
        self.memories[start..].to_vec()
    }

    pub fn remember(&mut self, kind: MemoryKind, text: impl Into<String>) {
        self.memories.push(MemoryEntry {
            at: now_secs(),
            kind,
            text: text.into(),
        });
        let overflow = self.memories.len().saturating_sub(MAX_MEMORIES);
        if overflow > 0 {
            self.memories.drain(..overflow);
        }
    }

    pub fn record_trace(&mut self, trace: ReasoningTrace) {
        self.traces.push(trace);
        let overflow = self.traces.len().saturating_sub(MAX_TRACES);
        if overflow > 0 {
            self.traces.drain(..overflow);
        }
    }

    pub fn recent_decisions(&self, n: usize) -> Vec<DecisionRecord> {
        let end = self.decisions.len();
        let start = end.saturating_sub(n);
        self.decisions[start..].to_vec()
    }

    pub fn recent_evidence(&self, n: usize) -> Vec<Evidence> {
        let end = self.evidence.len();
        let start = end.saturating_sub(n);
        self.evidence[start..].to_vec()
    }

    pub async fn preflight(
        &self,
        loaded: &LoadedModule,
        target: &str,
        destructive_override: Option<bool>,
        approved: bool,
        context: PolicyContext,
    ) -> Preflight {
        use crate::core::module::Intent;
        let destructive = destructive_override.unwrap_or_else(|| {
            loaded.info.effective_intents().contains(&Intent::Modify)
        });
        


        Preflight {
            target: target.to_string(),
            charter_accepted: self.charter.accepted,
            in_scope: self.scope.is_in_scope(target),
            risk: loaded.info.effective_impact(),
            destructive,
            approved,
            capabilities: loaded.info.capabilities.clone(),
            intents: loaded.info.effective_intents(),
            context,
            cvss: None,
        }
    }

    #[allow(clippy::too_many_arguments)]
    pub async fn execute(
        &mut self,
        loaded: &mut LoadedModule,
        target: &str,
        destructive_override: Option<bool>,
        approved: bool,
        context: PolicyContext,
        job_id: Option<u64>,
        sandbox: bool,
        engine: Option<crate::core::sandbox::SandboxEngineType>,
    ) -> Result<ModuleResult, ExecutorError> {
        let pf = self.preflight(loaded, target, destructive_override, approved, context).await;
        let policy = self.policy(context);
        let decision = policy.evaluate(&pf.to_request());
        self.record_decision(&loaded.info.name, &pf, &decision);
        pf.check(&policy)?;

        if (self.sandbox_required || self.tier.requires_sandbox()) && !sandbox {
            let reason = format!("operational tier {} requires sandbox isolation", self.tier);
            self.record_decision(&loaded.info.name, &pf, &PolicyDecision::Deny(reason.clone()));
            return Err(ExecutorError::Preflight(PreflightError::Denied(reason)));
        }

        if self.tier.requires_explicit_approval() && !approved {
            let reason = format!("operational tier {} requires explicit operator approval", self.tier);
            self.record_decision(
                &loaded.info.name,
                &pf,
                &PolicyDecision::RequireApproval(reason.clone()),
            );
            return Err(ExecutorError::Preflight(PreflightError::ApprovalRequired));
        }

        info!(
            target = %target,
            module = %loaded.info.name,
            risk = %pf.risk.as_str(),
            destructive = pf.destructive,
            sandbox = sandbox,
            "executor: preflight passed"
        );
        let result = if sandbox {
            self.run_sandboxed(
                loaded,
                target,
                engine.unwrap_or(crate::core::sandbox::SandboxEngineType::Docker),
                context,
            )
            .await?
        } else {
            match loaded.module.run().await {
                Ok(r) => r,
                Err(e) => {
                    let reason = format!("module execution failed: {e}");
                    self.record_decision(
                        &loaded.info.name,
                        &pf,
                        &PolicyDecision::Deny(reason.clone()),
                    );
                    return Err(ExecutorError::Module(e));
                }
            }
        };

        let denied = policy.denied_payload(&result);
        if !denied.is_empty() {
            let reason = format!("payload matched denied pattern: {denied}");
            self.record_decision(&loaded.info.name, &pf, &PolicyDecision::Deny(reason.clone()));
            let mut blocked = result;
            blocked.success = false;
            blocked.evidence.push(format!("[BLOCKED:payload] {denied}"));
            blocked.data = serde_json::Value::Null;
            self.record_evidence(&loaded.info.name, target, &blocked, job_id);
            return Ok(blocked);
        }

        self.record_evidence(&loaded.info.name, target, &result, job_id);
        Ok(result)
    }

    fn record_decision(
        &mut self,
        module: &str,
        pf: &Preflight,
        decision: &crate::core::safety::PolicyDecision,
    ) {
        self.decisions.push(DecisionRecord {
            at: now_secs(),
            target: pf.target.clone(),
            module: module.to_string(),
            capabilities: pf.capabilities.clone(),
            intents: pf.intents.clone(),
            impact: pf.risk,
            context: pf.context,
            decision: decision.clone(),
        });
        let overflow = self.decisions.len().saturating_sub(MAX_DECISIONS);
        if overflow > 0 {
            self.decisions.drain(..overflow);
        }
    }

    fn record_evidence(
        &mut self,
        module: &str,
        target: &str,
        result: &ModuleResult,
        job_id: Option<u64>,
    ) {
        let seq_start = self.evidence.len();
        for (i, content) in result.evidence.iter().enumerate() {
            self.evidence.push(Evidence::new(
                module,
                target,
                content,
                job_id,
                seq_start + i,
            ));
        }
        let overflow = self.evidence.len().saturating_sub(MAX_EVIDENCE);
        if overflow > 0 {
            self.evidence.drain(..overflow);
        }
    }
    async fn run_sandboxed(
        &mut self,
        loaded: &crate::core::module::LoadedModule,
        target: &str,
        engine: crate::core::sandbox::SandboxEngineType,
        context: PolicyContext,
    ) -> Result<ModuleResult, ExecutorError> {
        use crate::core::sandbox::Sandbox;
        let image = "icebox-sandbox:latest".to_string();
        let module_name = loaded.info.name.clone();
        match Sandbox::freeze(engine, target, &image).await {
            Ok(sandbox) => {
                info!(
                    container = %sandbox.container_id(),
                    "[SANDBOX] container frozen"
                );
                let options = loaded.module.options_json();
                let mut result = match sandbox
                    .exec_module(&loaded.info.name, target, &options)
                    .await
                {
                    Ok(r) => r,
                    Err(e) => ModuleResult {
                        error: Some(format!("sandbox module exec failed: {e}")),
                        ..Default::default()
                    },
                };
                let logs = sandbox.capture_logs().await;
                result.evidence.extend(logs);
                result.evidence.push(format!(
                    "[SANDBOX] Container melted: {}",
                    sandbox.container_id()
                ));
                if let Err(e) = sandbox.melt().await {
                    result
                        .evidence
                        .push(format!("[SANDBOX] Teardown warning: {e}"));
                }
                if result.error.is_some() {
                    self.record_failure(
                        &module_name,
                        target,
                        result.error.as_deref().unwrap_or("sandbox failure"),
                        context,
                    );
                }
                Ok(result)
            }
            Err(e) => {
                let reason = format!(
                    "Sandbox initialization failed: {e}. Isolation is mandatory."
                );
                self.record_failure(&module_name, target, &reason, context);
                Err(ExecutorError::Sandbox(reason))
            }
        }
    }

    fn record_failure(
        &mut self,
        module: &str,
        target: &str,
        reason: &str,
        context: PolicyContext,
    ) {
        self.decisions.push(DecisionRecord {
            at: now_secs(),
            target: target.to_string(),
            module: module.to_string(),
            capabilities: Vec::new(),
            intents: Vec::new(),
            impact: RiskLevel::None,
            context,
            decision: PolicyDecision::Deny(reason.to_string()),
        });
        let overflow = self.decisions.len().saturating_sub(MAX_DECISIONS);
        if overflow > 0 {
            self.decisions.drain(..overflow);
        }
    }
}