use anyhow::Result;
use async_trait::async_trait;
use smith_protocol::{CapabilitySpec, ExecutionError, ExecutionLimits, Intent};
use std::collections::HashMap;
#[async_trait]
pub trait Capability: Send + Sync {
fn name(&self) -> &'static str;
fn validate(&self, intent: &Intent) -> Result<(), ExecutionError>;
async fn execute(
&self,
intent: Intent,
ctx: ExecCtx,
) -> Result<CapabilityResult, ExecutionError>;
fn describe(&self) -> CapabilitySpec;
}
#[derive(Debug, Clone)]
pub struct ExecCtx {
pub workdir: std::path::PathBuf,
pub limits: ExecutionLimits,
pub scope: ExecutionScope,
pub trace_id: String,
pub sandbox: SandboxConfig,
}
#[derive(Debug, Clone)]
pub struct ExecutionScope {
pub paths: Vec<String>,
pub urls: Vec<String>,
pub env_vars: Vec<String>,
pub custom: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct SandboxConfig {
pub mode: smith_protocol::SandboxMode,
pub landlock_enabled: bool,
pub seccomp_enabled: bool,
pub cgroups_enabled: bool,
pub namespaces_enabled: bool,
}
#[derive(Debug, Clone)]
pub struct CapabilityResult {
pub status: smith_protocol::ExecutionStatus,
pub output: Option<serde_json::Value>,
pub error: Option<ExecutionError>,
pub metadata: ExecutionMetadata,
pub resource_usage: smith_protocol::ResourceUsage,
}
#[derive(Debug, Clone)]
pub struct ExecutionMetadata {
pub pid: u32,
pub exit_code: Option<i32>,
pub duration_ms: u64,
pub stdout_bytes: u64,
pub stderr_bytes: u64,
pub artifacts: Vec<Artifact>,
}
#[derive(Debug, Clone)]
pub struct Artifact {
pub name: String,
pub path: std::path::PathBuf,
pub size: u64,
pub sha256: String,
}
pub struct CapabilityRegistry {
capabilities: HashMap<String, Box<dyn Capability>>,
}
impl CapabilityRegistry {
pub fn new() -> Self {
Self {
capabilities: HashMap::new(),
}
}
pub fn register(&mut self, capability: Box<dyn Capability>) {
let name = capability.name().to_string();
self.capabilities.insert(name.clone(), capability);
tracing::info!("Registered capability: {}", name);
}
pub fn get(&self, name: &str) -> Option<&dyn Capability> {
self.capabilities.get(name).map(|c| c.as_ref())
}
pub fn list(&self) -> Vec<String> {
self.capabilities.keys().cloned().collect()
}
pub fn describe_all(&self) -> Vec<CapabilitySpec> {
self.capabilities.values().map(|c| c.describe()).collect()
}
pub fn validate_intent(&self, intent: &Intent) -> Result<(), ExecutionError> {
let capability_name = intent.capability.to_string();
match self.get(&capability_name) {
Some(capability) => capability.validate(intent),
None => Err(ExecutionError {
code: "CAPABILITY_NOT_FOUND".to_string(),
message: format!("No capability found for: {}", capability_name),
}),
}
}
pub async fn execute_intent(
&self,
intent: Intent,
ctx: ExecCtx,
) -> Result<CapabilityResult, ExecutionError> {
let capability_name = intent.capability.to_string();
match self.get(&capability_name) {
Some(capability) => capability.execute(intent, ctx).await,
None => Err(ExecutionError {
code: "CAPABILITY_NOT_FOUND".to_string(),
message: format!("No capability found for: {}", capability_name),
}),
}
}
}
impl Default for ExecutionScope {
fn default() -> Self {
Self {
paths: vec![],
urls: vec![],
env_vars: vec![],
custom: HashMap::new(),
}
}
}
impl Default for SandboxConfig {
fn default() -> Self {
Self {
mode: smith_protocol::SandboxMode::Full,
landlock_enabled: true,
seccomp_enabled: true,
cgroups_enabled: true,
namespaces_enabled: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use smith_protocol::ResourceRequirements;
struct TestCapability;
#[async_trait]
impl Capability for TestCapability {
fn name(&self) -> &'static str {
"test.v1"
}
fn validate(&self, _intent: &Intent) -> Result<(), ExecutionError> {
Ok(())
}
async fn execute(
&self,
_intent: Intent,
_ctx: ExecCtx,
) -> Result<CapabilityResult, ExecutionError> {
Ok(CapabilityResult {
status: smith_protocol::ExecutionStatus::Ok,
output: Some(json!({"result": "success"})),
error: None,
metadata: ExecutionMetadata {
pid: 12345,
exit_code: Some(0),
duration_ms: 100,
stdout_bytes: 10,
stderr_bytes: 0,
artifacts: vec![],
},
resource_usage: smith_protocol::ResourceUsage {
peak_memory_kb: 1024,
cpu_time_ms: 50,
wall_time_ms: 100,
fd_count: 3,
disk_read_bytes: 0,
disk_write_bytes: 0,
network_tx_bytes: 0,
network_rx_bytes: 0,
},
})
}
fn describe(&self) -> CapabilitySpec {
CapabilitySpec {
name: self.name().to_string(),
description: "Test capability for unit tests".to_string(),
params_schema: json!({
"type": "object",
"properties": {
"test_param": {"type": "string"}
}
}),
example_params: json!({"test_param": "example"}),
resource_requirements: ResourceRequirements {
cpu_ms_typical: 50,
memory_kb_max: 1024,
network_access: false,
filesystem_access: false,
external_commands: false,
},
security_notes: vec!["Safe test capability with no external access".to_string()],
}
}
}
#[test]
fn test_capability_registry() {
let mut registry = CapabilityRegistry::new();
registry.register(Box::new(TestCapability));
assert!(registry.get("test.v1").is_some());
assert!(registry.get("nonexistent").is_none());
let capabilities = registry.list();
assert_eq!(capabilities.len(), 1);
assert!(capabilities.contains(&"test.v1".to_string()));
}
#[test]
fn test_execution_limits_default() {
let limits = ExecutionLimits::default();
assert_eq!(limits.cpu_ms_per_100ms, 50);
assert_eq!(limits.mem_bytes, 128 * 1024 * 1024);
assert_eq!(limits.timeout_ms, 30000);
}
#[test]
fn test_sandbox_config_default() {
let config = SandboxConfig::default();
assert_eq!(config.mode, smith_protocol::SandboxMode::Full);
assert!(config.landlock_enabled);
assert!(config.seccomp_enabled);
assert!(config.cgroups_enabled);
}
}