use anyhow::Result;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use rand_core::{OsRng, RngCore};
use serde_json::Value;
use std::path::Path;
use std::sync::Arc;
use tracing::{debug, info, warn};
use uuid::Uuid;
pub mod analysis_performance;
pub mod analysis_system;
pub mod fs_read;
pub mod fs_write;
pub mod http_fetch;
pub mod implementation_execute;
pub mod planner_exec;
pub mod shell_exec;
pub mod test_simulation;
pub mod validation_test;
use crate::vm::MicroVmManager;
use smith_protocol::{ExecutionLimits, ExecutionStatus};
#[derive(Debug, Clone)]
pub struct ExecContext {
pub workdir: std::path::PathBuf,
pub limits: ExecutionLimits,
pub scope: Scope,
pub creds: Option<EphemeralCreds>,
pub netns: Option<NetNamespaceHandle>,
pub trace_id: String,
pub session: Option<SessionContext>,
}
#[derive(Debug, Clone)]
pub struct SessionContext {
pub session_id: Uuid,
pub domain: Option<String>,
pub vm_profile: Option<String>,
}
pub type ExecutionContext = ExecContext;
#[derive(Debug, Clone)]
pub struct Scope {
pub paths: Vec<String>,
pub urls: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct EphemeralCreds {
pub access_token: String,
pub expires_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone)]
pub struct NetNamespaceHandle {
pub fd: i32, }
pub trait OutputSink: Send + Sync {
fn write_stdout(&mut self, data: &[u8]) -> Result<()>;
fn write_stderr(&mut self, data: &[u8]) -> Result<()>;
fn write_log(&mut self, level: &str, message: &str) -> Result<()>;
}
pub struct MemoryOutputSink {
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
pub logs: Vec<String>,
}
impl MemoryOutputSink {
pub fn new() -> Self {
Self {
stdout: Vec::new(),
stderr: Vec::new(),
logs: Vec::new(),
}
}
}
impl OutputSink for MemoryOutputSink {
fn write_stdout(&mut self, data: &[u8]) -> Result<()> {
self.stdout.extend_from_slice(data);
Ok(())
}
fn write_stderr(&mut self, data: &[u8]) -> Result<()> {
self.stderr.extend_from_slice(data);
Ok(())
}
fn write_log(&mut self, level: &str, message: &str) -> Result<()> {
self.logs.push(format!("[{}] {}", level, message));
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct ExecutionResult {
pub status: ExecutionStatus,
pub exit_code: Option<i32>,
pub artifacts: Vec<Artifact>,
pub duration_ms: u64,
pub stdout_bytes: u64,
pub stderr_bytes: u64,
}
#[derive(Debug, Clone)]
pub struct Artifact {
pub name: String,
pub path: std::path::PathBuf,
pub size: u64,
pub sha256: String,
}
#[async_trait]
pub trait Runner: Send + Sync {
fn digest(&self) -> String;
fn validate_params(&self, params: &Value) -> Result<()>;
async fn execute(
&self,
ctx: &ExecContext,
params: Value,
out: &mut dyn OutputSink,
) -> Result<ExecutionResult>;
}
pub struct RunnerRegistry {
runners: std::collections::HashMap<String, Box<dyn Runner>>,
}
impl RunnerRegistry {
pub fn new(vm_manager: Option<Arc<MicroVmManager>>) -> Self {
let mut registry = Self {
runners: std::collections::HashMap::new(),
};
registry.register("fs.read", Box::new(fs_read::FsReadRunner::new()));
registry.register("fs.read.v1", Box::new(fs_read::FsReadRunner::new()));
registry.register("fs.write", Box::new(fs_write::FsWriteRunner::new()));
registry.register("fs.write.v1", Box::new(fs_write::FsWriteRunner::new()));
registry.register(
"git.clone",
Box::new(test_simulation::UnsupportedCapabilityRunner::new(
"git-clone-disabled-runner-v1",
"git.clone.v1 is deprecated; use shell.exec.v1 with the git CLI instead",
)),
);
registry.register(
"git.clone.v1",
Box::new(test_simulation::UnsupportedCapabilityRunner::new(
"git-clone-disabled-runner-v1",
"git.clone.v1 is deprecated; use shell.exec.v1 with the git CLI instead",
)),
);
registry.register(
"planner.exec",
Box::new(planner_exec::PlannerExecRunner::new()),
);
registry.register(
"analysis.system.v1",
Box::new(analysis_system::AnalysisSystemRunner::new()),
);
registry.register(
"analysis.performance.v1",
Box::new(analysis_performance::AnalysisPerformanceRunner::new()),
);
registry.register(
"analysis.security.v1",
Box::new(test_simulation::NoopSuccessRunner::new(
"analysis-security-runner-v1",
"Security analysis complete",
)),
);
registry.register(
"analysis.concurrent.v1",
Box::new(test_simulation::NoopSuccessRunner::new(
"analysis-concurrent-runner-v1",
"Concurrent analysis completed",
)),
);
registry.register(
"implementation.execute.v1",
Box::new(implementation_execute::ImplementationExecuteRunner::new()),
);
registry.register(
"implementation.prepare.v1",
Box::new(test_simulation::NoopSuccessRunner::new(
"implementation-prepare-runner-v1",
"Implementation preparation complete",
)),
);
registry.register(
"validation.test.v1",
Box::new(validation_test::ValidationTestRunner::new()),
);
registry.register(
"validation.functional.v1",
Box::new(test_simulation::NoopSuccessRunner::new(
"validation-functional-runner-v1",
"Functional validation complete",
)),
);
registry.register(
"validation.performance.v1",
Box::new(test_simulation::NoopSuccessRunner::new(
"validation-performance-runner-v1",
"Performance validation complete",
)),
);
registry.register(
"validation.final.v1",
Box::new(test_simulation::NoopSuccessRunner::new(
"validation-final-runner-v1",
"Final validation successful",
)),
);
registry.register(
"test.failure.v1",
Box::new(test_simulation::RandomFailureRunner::new()),
);
registry.register(
"test.always_fail.v1",
Box::new(test_simulation::AlwaysFailRunner::new()),
);
let shell_runner = Box::new(shell_exec::ShellExecRunner::new(vm_manager.clone()));
registry.register("shell.exec", shell_runner);
registry.register(
"shell.exec.v1",
Box::new(shell_exec::ShellExecRunner::new(vm_manager)),
);
info!(
"Runner registry initialized with {} runners",
registry.runners.len()
);
registry
}
pub fn register(&mut self, capability: &str, runner: Box<dyn Runner>) {
self.runners.insert(capability.to_string(), runner);
info!("Registered runner for capability: {}", capability);
}
pub fn get_runner(&self, capability: &str) -> Option<&dyn Runner> {
self.runners.get(capability).map(|r| r.as_ref())
}
pub fn capabilities(&self) -> Vec<String> {
self.runners.keys().cloned().collect()
}
}
fn generate_ephemeral_credentials() -> Option<EphemeralCreds> {
let mut token_bytes = [0u8; 32];
OsRng.fill_bytes(&mut token_bytes);
let access_token = format!("exec-{}", Uuid::new_v4());
let expires_at = Utc::now() + chrono::Duration::hours(1);
debug!(
"Generated ephemeral credentials, expires at: {}",
expires_at
);
Some(EphemeralCreds {
access_token,
expires_at,
})
}
fn create_network_namespace() -> Option<NetNamespaceHandle> {
let current_uid = unsafe { libc::geteuid() };
if current_uid != 0 {
debug!(
current_uid,
"Skipping network namespace creation (requires root capabilities)"
);
return None;
}
#[cfg(target_os = "linux")]
{
match create_netns_linux() {
Ok(fd) => {
debug!("Created network namespace with fd: {}", fd);
Some(NetNamespaceHandle { fd })
}
Err(e) => {
warn!("Failed to create network namespace: {}", e);
None
}
}
}
#[cfg(not(target_os = "linux"))]
{
debug!("Network namespace creation not supported on this platform");
None
}
}
#[cfg(target_os = "linux")]
fn create_netns_linux() -> Result<i32> {
use nix::sched::{unshare, CloneFlags};
use std::os::unix::io::AsRawFd;
unshare(CloneFlags::CLONE_NEWNET)
.map_err(|e| anyhow::anyhow!("Failed to create network namespace: {}", e))?;
let ns_fd = std::fs::File::open("/proc/self/ns/net")
.map_err(|e| anyhow::anyhow!("Failed to open network namespace: {}", e))?;
Ok(ns_fd.as_raw_fd())
}
pub fn create_exec_context(
workdir: &Path,
limits: ExecutionLimits,
scope: Scope,
trace_id: String,
) -> ExecContext {
ExecContext {
workdir: workdir.to_path_buf(),
limits,
scope,
creds: generate_ephemeral_credentials(),
netns: create_network_namespace(),
trace_id,
session: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use tempfile::TempDir;
#[test]
fn test_runner_registry() {
let registry = RunnerRegistry::new(None);
assert!(registry.get_runner("fs.read").is_some());
assert!(registry.get_runner("nonexistent").is_none());
assert!(registry.get_runner("fs.write").is_some());
assert!(registry.get_runner("git.clone").is_some());
let capabilities = registry.capabilities();
assert!(capabilities.contains(&"fs.read".to_string()));
assert!(capabilities.contains(&"fs.write".to_string()));
assert!(capabilities.contains(&"git.clone".to_string()));
}
#[test]
fn test_runner_registry_v1_runners() {
let registry = RunnerRegistry::new(None);
assert!(registry.get_runner("fs.read.v1").is_some());
assert!(registry.get_runner("fs.write.v1").is_some());
assert!(registry.get_runner("git.clone.v1").is_some());
assert!(registry.get_runner("shell.exec.v1").is_some());
assert!(registry.get_runner("analysis.system.v1").is_some());
assert!(registry.get_runner("analysis.performance.v1").is_some());
assert!(registry.get_runner("analysis.security.v1").is_some());
assert!(registry.get_runner("implementation.execute.v1").is_some());
assert!(registry.get_runner("validation.test.v1").is_some());
}
#[test]
fn test_runner_registry_test_runners() {
let registry = RunnerRegistry::new(None);
assert!(registry.get_runner("test.failure.v1").is_some());
assert!(registry.get_runner("test.always_fail.v1").is_some());
}
#[test]
fn test_runner_registry_shell_exec() {
let registry = RunnerRegistry::new(None);
assert!(registry.get_runner("shell.exec").is_some());
assert!(registry.get_runner("shell.exec.v1").is_some());
}
#[test]
fn test_runner_registry_capabilities_count() {
let registry = RunnerRegistry::new(None);
let capabilities = registry.capabilities();
assert!(
capabilities.len() >= 10,
"Expected at least 10 registered capabilities, got {}",
capabilities.len()
);
}
#[test]
fn test_memory_output_sink() {
let mut sink = MemoryOutputSink::new();
sink.write_stdout(b"hello").unwrap();
sink.write_stderr(b"error").unwrap();
sink.write_log("INFO", "test message").unwrap();
assert_eq!(sink.stdout, b"hello");
assert_eq!(sink.stderr, b"error");
assert_eq!(sink.logs, vec!["[INFO] test message"]);
}
#[test]
fn test_memory_output_sink_new() {
let sink = MemoryOutputSink::new();
assert!(sink.stdout.is_empty());
assert!(sink.stderr.is_empty());
assert!(sink.logs.is_empty());
}
#[test]
fn test_memory_output_sink_multiple_writes() {
let mut sink = MemoryOutputSink::new();
sink.write_stdout(b"hello ").unwrap();
sink.write_stdout(b"world").unwrap();
assert_eq!(sink.stdout, b"hello world");
}
#[test]
fn test_memory_output_sink_multiple_logs() {
let mut sink = MemoryOutputSink::new();
sink.write_log("INFO", "message 1").unwrap();
sink.write_log("ERROR", "message 2").unwrap();
sink.write_log("DEBUG", "message 3").unwrap();
assert_eq!(sink.logs.len(), 3);
assert_eq!(sink.logs[0], "[INFO] message 1");
assert_eq!(sink.logs[1], "[ERROR] message 2");
assert_eq!(sink.logs[2], "[DEBUG] message 3");
}
#[test]
fn test_memory_output_sink_binary_data() {
let mut sink = MemoryOutputSink::new();
let binary_data: Vec<u8> = vec![0x00, 0x01, 0x02, 0xFF, 0xFE];
sink.write_stdout(&binary_data).unwrap();
assert_eq!(sink.stdout, binary_data);
}
#[test]
fn test_exec_context_creation() {
let ctx = ExecContext {
workdir: PathBuf::from("/tmp/test"),
limits: ExecutionLimits::default(),
scope: Scope {
paths: vec!["/allowed".to_string()],
urls: vec!["https://example.com".to_string()],
},
creds: None,
netns: None,
trace_id: "trace-123".to_string(),
session: None,
};
assert_eq!(ctx.workdir, PathBuf::from("/tmp/test"));
assert_eq!(ctx.trace_id, "trace-123");
assert!(ctx.creds.is_none());
assert!(ctx.netns.is_none());
assert!(ctx.session.is_none());
}
#[test]
fn test_exec_context_with_session() {
let session = SessionContext {
session_id: Uuid::new_v4(),
domain: Some("test-domain".to_string()),
vm_profile: Some("standard".to_string()),
};
let ctx = ExecContext {
workdir: PathBuf::from("/tmp"),
limits: ExecutionLimits::default(),
scope: Scope {
paths: vec![],
urls: vec![],
},
creds: None,
netns: None,
trace_id: "trace-456".to_string(),
session: Some(session.clone()),
};
assert!(ctx.session.is_some());
let s = ctx.session.unwrap();
assert_eq!(s.domain, Some("test-domain".to_string()));
assert_eq!(s.vm_profile, Some("standard".to_string()));
}
#[test]
fn test_session_context_creation() {
let session_id = Uuid::new_v4();
let session = SessionContext {
session_id,
domain: Some("my-domain".to_string()),
vm_profile: None,
};
assert_eq!(session.session_id, session_id);
assert_eq!(session.domain, Some("my-domain".to_string()));
assert!(session.vm_profile.is_none());
}
#[test]
fn test_session_context_clone() {
let session = SessionContext {
session_id: Uuid::new_v4(),
domain: Some("domain".to_string()),
vm_profile: Some("profile".to_string()),
};
let cloned = session.clone();
assert_eq!(cloned.session_id, session.session_id);
assert_eq!(cloned.domain, session.domain);
assert_eq!(cloned.vm_profile, session.vm_profile);
}
#[test]
fn test_scope_creation() {
let scope = Scope {
paths: vec!["/etc".to_string(), "/var/log".to_string()],
urls: vec!["https://api.example.com".to_string()],
};
assert_eq!(scope.paths.len(), 2);
assert_eq!(scope.urls.len(), 1);
}
#[test]
fn test_scope_empty() {
let scope = Scope {
paths: vec![],
urls: vec![],
};
assert!(scope.paths.is_empty());
assert!(scope.urls.is_empty());
}
#[test]
fn test_ephemeral_creds_creation() {
let creds = EphemeralCreds {
access_token: "token-123".to_string(),
expires_at: Utc::now() + chrono::Duration::hours(1),
};
assert_eq!(creds.access_token, "token-123");
assert!(creds.expires_at > Utc::now());
}
#[test]
fn test_net_namespace_handle_creation() {
let handle = NetNamespaceHandle { fd: 42 };
assert_eq!(handle.fd, 42);
}
#[test]
fn test_execution_result_creation() {
let result = ExecutionResult {
status: ExecutionStatus::Ok,
exit_code: Some(0),
artifacts: vec![],
duration_ms: 100,
stdout_bytes: 50,
stderr_bytes: 0,
};
assert_eq!(result.status, ExecutionStatus::Ok);
assert_eq!(result.exit_code, Some(0));
assert!(result.artifacts.is_empty());
assert_eq!(result.duration_ms, 100);
}
#[test]
fn test_execution_result_with_error() {
let result = ExecutionResult {
status: ExecutionStatus::Error,
exit_code: Some(1),
artifacts: vec![],
duration_ms: 50,
stdout_bytes: 0,
stderr_bytes: 100,
};
assert_eq!(result.status, ExecutionStatus::Error);
assert_eq!(result.exit_code, Some(1));
}
#[test]
fn test_execution_result_timeout() {
let result = ExecutionResult {
status: ExecutionStatus::Timeout,
exit_code: None,
artifacts: vec![],
duration_ms: 30000,
stdout_bytes: 0,
stderr_bytes: 0,
};
assert_eq!(result.status, ExecutionStatus::Timeout);
assert!(result.exit_code.is_none());
}
#[test]
fn test_artifact_creation() {
let artifact = Artifact {
name: "output.txt".to_string(),
path: PathBuf::from("/tmp/output.txt"),
size: 1024,
sha256: "abc123".to_string(),
};
assert_eq!(artifact.name, "output.txt");
assert_eq!(artifact.size, 1024);
}
#[test]
fn test_create_exec_context() {
let temp_dir = TempDir::new().unwrap();
let limits = ExecutionLimits::default();
let scope = Scope {
paths: vec!["/etc".to_string()],
urls: vec![],
};
let ctx = create_exec_context(temp_dir.path(), limits, scope, "trace-999".to_string());
assert_eq!(ctx.workdir, temp_dir.path());
assert_eq!(ctx.trace_id, "trace-999");
assert_eq!(ctx.scope.paths, vec!["/etc".to_string()]);
assert!(ctx.creds.is_some());
assert!(ctx.session.is_none());
}
#[test]
fn test_create_exec_context_generates_creds() {
let temp_dir = TempDir::new().unwrap();
let ctx = create_exec_context(
temp_dir.path(),
ExecutionLimits::default(),
Scope {
paths: vec![],
urls: vec![],
},
"trace".to_string(),
);
let creds = ctx.creds.unwrap();
assert!(creds.access_token.starts_with("exec-"));
assert!(creds.expires_at > Utc::now());
}
#[test]
fn test_generate_ephemeral_credentials() {
let creds = generate_ephemeral_credentials();
assert!(creds.is_some());
let creds = creds.unwrap();
assert!(creds.access_token.starts_with("exec-"));
let one_hour = chrono::Duration::hours(1);
let expected_expiry = Utc::now() + one_hour;
assert!(creds.expires_at > Utc::now());
assert!(creds.expires_at < expected_expiry + chrono::Duration::minutes(1));
}
#[test]
fn test_generate_ephemeral_credentials_unique() {
let creds1 = generate_ephemeral_credentials().unwrap();
let creds2 = generate_ephemeral_credentials().unwrap();
assert_ne!(creds1.access_token, creds2.access_token);
}
}
#[cfg(test)]
mod capability_tests;