use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Command {
pub program: String,
pub args: Vec<String>,
pub workdir: Option<PathBuf>,
pub env: HashMap<String, String>,
pub inherit_env: bool,
pub stdin: Option<Vec<u8>>,
pub timeout: Option<Duration>,
}
impl Command {
pub fn new(program: &str) -> Self {
Self {
program: program.to_string(),
args: vec![],
workdir: None,
env: HashMap::new(),
inherit_env: true,
stdin: None,
timeout: None,
}
}
pub fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.args
.extend(args.into_iter().map(|s| s.as_ref().to_string()));
self
}
pub fn workdir(mut self, dir: PathBuf) -> Self {
self.workdir = Some(dir);
self
}
pub fn env(mut self, key: &str, value: &str) -> Self {
self.env.insert(key.to_string(), value.to_string());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn stdin(mut self, input: Vec<u8>) -> Self {
self.stdin = Some(input);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IntentRequest {
pub id: Uuid,
pub capability: String,
pub version: u32,
pub params: serde_json::Value,
pub constraints: RequestConstraints,
pub metadata: RequestMetadata,
pub sandbox_prefs: SandboxPreferences,
}
impl IntentRequest {
pub fn new(capability: &str, params: serde_json::Value) -> Self {
Self {
id: Uuid::new_v4(),
capability: capability.to_string(),
version: 1,
params,
constraints: RequestConstraints::default(),
metadata: RequestMetadata::default(),
sandbox_prefs: SandboxPreferences::default(),
}
}
pub fn with_id(mut self, id: Uuid) -> Self {
self.id = id;
self
}
pub fn with_constraints(mut self, constraints: RequestConstraints) -> Self {
self.constraints = constraints;
self
}
pub fn with_sandbox_prefs(mut self, prefs: SandboxPreferences) -> Self {
self.sandbox_prefs = prefs;
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RequestConstraints {
pub max_duration_ms: Option<u64>,
pub max_output_bytes: Option<u64>,
pub max_memory_bytes: Option<u64>,
pub allow_network: Option<bool>,
pub allow_writes: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RequestMetadata {
pub trace_id: Option<String>,
pub span_id: Option<String>,
pub timestamp_ms: Option<u64>,
pub idempotency_key: Option<String>,
pub priority: Option<u8>,
pub custom: HashMap<String, String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SandboxPreferences {
pub sandbox_id: Option<String>,
pub require_fresh: bool,
pub profile: Option<String>,
pub persist: bool,
pub backend: Option<String>,
pub labels: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IntentResponse {
pub request_id: Uuid,
pub status: IntentStatus,
pub code: String,
pub message: String,
pub result: Option<ExecutionResult>,
pub error: Option<ErrorDetails>,
pub timing: ResponseTiming,
pub sandbox_info: Option<SandboxInfo>,
}
impl IntentResponse {
pub fn success(request_id: Uuid, result: ExecutionResult) -> Self {
Self {
request_id,
status: IntentStatus::Ok,
code: "OK".to_string(),
message: "Execution completed successfully".to_string(),
result: Some(result),
error: None,
timing: ResponseTiming::default(),
sandbox_info: None,
}
}
pub fn error(request_id: Uuid, code: &str, message: &str) -> Self {
Self {
request_id,
status: IntentStatus::Error,
code: code.to_string(),
message: message.to_string(),
result: None,
error: Some(ErrorDetails {
code: code.to_string(),
message: message.to_string(),
details: None,
retryable: false,
retry_after_ms: None,
}),
timing: ResponseTiming::default(),
sandbox_info: None,
}
}
pub fn denied(request_id: Uuid, reason: &str) -> Self {
Self {
request_id,
status: IntentStatus::Denied,
code: "DENIED".to_string(),
message: reason.to_string(),
result: None,
error: None,
timing: ResponseTiming::default(),
sandbox_info: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum IntentStatus {
Ok,
Denied,
Error,
Expired,
Cancelled,
Pending,
}
impl IntentStatus {
pub fn is_terminal(&self) -> bool {
!matches!(self, IntentStatus::Pending)
}
pub fn is_success(&self) -> bool {
matches!(self, IntentStatus::Ok)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionResult {
pub exit_code: i32,
pub stdout: Option<String>,
pub stdout_bytes: Option<Vec<u8>>,
pub stderr: Option<String>,
pub output: Option<serde_json::Value>,
pub artifacts: Vec<Artifact>,
pub resource_usage: Option<ResourceUsageStats>,
}
impl Default for ExecutionResult {
fn default() -> Self {
Self {
exit_code: 0,
stdout: None,
stdout_bytes: None,
stderr: None,
output: None,
artifacts: vec![],
resource_usage: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Artifact {
pub name: String,
pub content_type: String,
pub size: u64,
pub sha256: String,
pub uri: Option<String>,
pub content: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ResourceUsageStats {
pub peak_memory_bytes: u64,
pub cpu_time_ms: u64,
pub wall_time_ms: u64,
pub disk_write_bytes: u64,
pub disk_read_bytes: u64,
pub network_tx_bytes: u64,
pub network_rx_bytes: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorDetails {
pub code: String,
pub message: String,
pub details: Option<serde_json::Value>,
pub retryable: bool,
pub retry_after_ms: Option<u64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ResponseTiming {
pub received_at_ms: u64,
pub started_at_ms: u64,
pub completed_at_ms: u64,
pub queue_time_ms: u64,
pub setup_time_ms: u64,
pub exec_time_ms: u64,
pub total_time_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxInfo {
pub sandbox_id: String,
pub backend: String,
pub profile: String,
pub newly_created: bool,
pub capabilities: SandboxCapabilitiesInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxCapabilitiesInfo {
pub can_write: bool,
pub has_network: bool,
pub readable_paths: Vec<String>,
pub writable_paths: Vec<String>,
pub limits: ResourceLimitsInfo,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ResourceLimitsInfo {
pub max_memory_bytes: Option<u64>,
pub max_cpu_ms: Option<u64>,
pub max_wall_ms: Option<u64>,
pub max_output_bytes: Option<u64>,
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_command_new() {
let cmd = Command::new("ls");
assert_eq!(cmd.program, "ls");
assert!(cmd.args.is_empty());
assert!(cmd.workdir.is_none());
assert!(cmd.env.is_empty());
assert!(cmd.inherit_env);
assert!(cmd.stdin.is_none());
assert!(cmd.timeout.is_none());
}
#[test]
fn test_command_args() {
let cmd = Command::new("ls").args(["-la", "/tmp"]);
assert_eq!(cmd.args, vec!["-la", "/tmp"]);
}
#[test]
fn test_command_workdir() {
let cmd = Command::new("ls").workdir(PathBuf::from("/home/user"));
assert_eq!(cmd.workdir, Some(PathBuf::from("/home/user")));
}
#[test]
fn test_command_env() {
let cmd = Command::new("bash")
.env("PATH", "/usr/bin")
.env("HOME", "/home/user");
assert_eq!(cmd.env.get("PATH"), Some(&"/usr/bin".to_string()));
assert_eq!(cmd.env.get("HOME"), Some(&"/home/user".to_string()));
}
#[test]
fn test_command_timeout() {
let cmd = Command::new("sleep").timeout(Duration::from_secs(30));
assert_eq!(cmd.timeout, Some(Duration::from_secs(30)));
}
#[test]
fn test_command_stdin() {
let cmd = Command::new("cat").stdin(b"hello world".to_vec());
assert_eq!(cmd.stdin, Some(b"hello world".to_vec()));
}
#[test]
fn test_command_builder_chain() {
let cmd = Command::new("python")
.args(["script.py", "--verbose"])
.workdir(PathBuf::from("/app"))
.env("PYTHONPATH", "/lib")
.timeout(Duration::from_secs(60))
.stdin(b"input data".to_vec());
assert_eq!(cmd.program, "python");
assert_eq!(cmd.args.len(), 2);
assert_eq!(cmd.workdir, Some(PathBuf::from("/app")));
assert_eq!(cmd.env.get("PYTHONPATH"), Some(&"/lib".to_string()));
assert_eq!(cmd.timeout, Some(Duration::from_secs(60)));
assert!(cmd.stdin.is_some());
}
#[test]
fn test_intent_request_new() {
let params = serde_json::json!({"path": "/etc/passwd"});
let req = IntentRequest::new("fs.read.v1", params.clone());
assert_eq!(req.capability, "fs.read.v1");
assert_eq!(req.version, 1);
assert_eq!(req.params, params);
}
#[test]
fn test_intent_request_with_id() {
let id = Uuid::new_v4();
let req = IntentRequest::new("fs.read.v1", serde_json::json!({})).with_id(id);
assert_eq!(req.id, id);
}
#[test]
fn test_intent_request_with_constraints() {
let constraints = RequestConstraints {
max_duration_ms: Some(5000),
max_output_bytes: Some(1024),
max_memory_bytes: Some(1024 * 1024),
allow_network: Some(false),
allow_writes: Some(true),
};
let req = IntentRequest::new("shell.exec.v1", serde_json::json!({}))
.with_constraints(constraints);
assert_eq!(req.constraints.max_duration_ms, Some(5000));
assert_eq!(req.constraints.max_output_bytes, Some(1024));
assert_eq!(req.constraints.allow_network, Some(false));
}
#[test]
fn test_intent_request_with_sandbox_prefs() {
let prefs = SandboxPreferences {
sandbox_id: Some("sb-123".to_string()),
require_fresh: true,
profile: Some("high-security".to_string()),
persist: false,
backend: Some("landlock".to_string()),
labels: HashMap::new(),
};
let req = IntentRequest::new("fs.read.v1", serde_json::json!({})).with_sandbox_prefs(prefs);
assert_eq!(req.sandbox_prefs.sandbox_id, Some("sb-123".to_string()));
assert!(req.sandbox_prefs.require_fresh);
}
#[test]
fn test_request_constraints_default() {
let constraints = RequestConstraints::default();
assert!(constraints.max_duration_ms.is_none());
assert!(constraints.max_output_bytes.is_none());
assert!(constraints.max_memory_bytes.is_none());
assert!(constraints.allow_network.is_none());
assert!(constraints.allow_writes.is_none());
}
#[test]
fn test_request_metadata_default() {
let metadata = RequestMetadata::default();
assert!(metadata.trace_id.is_none());
assert!(metadata.span_id.is_none());
assert!(metadata.timestamp_ms.is_none());
assert!(metadata.idempotency_key.is_none());
assert!(metadata.priority.is_none());
assert!(metadata.custom.is_empty());
}
#[test]
fn test_request_metadata_with_values() {
let metadata = RequestMetadata {
trace_id: Some("trace-123".to_string()),
span_id: Some("span-456".to_string()),
timestamp_ms: Some(1234567890),
idempotency_key: Some("idem-key".to_string()),
priority: Some(5),
custom: HashMap::from([("env".to_string(), "prod".to_string())]),
};
assert_eq!(metadata.trace_id, Some("trace-123".to_string()));
assert_eq!(metadata.priority, Some(5));
}
#[test]
fn test_sandbox_preferences_default() {
let prefs = SandboxPreferences::default();
assert!(prefs.sandbox_id.is_none());
assert!(!prefs.require_fresh);
assert!(prefs.profile.is_none());
assert!(!prefs.persist);
assert!(prefs.backend.is_none());
assert!(prefs.labels.is_empty());
}
#[test]
fn test_intent_response_success() {
let id = Uuid::new_v4();
let result = ExecutionResult {
exit_code: 0,
stdout: Some("output".to_string()),
..Default::default()
};
let response = IntentResponse::success(id, result);
assert_eq!(response.request_id, id);
assert_eq!(response.status, IntentStatus::Ok);
assert_eq!(response.code, "OK");
assert!(response.result.is_some());
assert!(response.error.is_none());
}
#[test]
fn test_intent_response_error() {
let id = Uuid::new_v4();
let response = IntentResponse::error(id, "EXEC_FAILED", "Command failed");
assert_eq!(response.request_id, id);
assert_eq!(response.status, IntentStatus::Error);
assert_eq!(response.code, "EXEC_FAILED");
assert_eq!(response.message, "Command failed");
assert!(response.result.is_none());
assert!(response.error.is_some());
let error = response.error.unwrap();
assert_eq!(error.code, "EXEC_FAILED");
assert!(!error.retryable);
}
#[test]
fn test_intent_response_denied() {
let id = Uuid::new_v4();
let response = IntentResponse::denied(id, "Policy violation");
assert_eq!(response.request_id, id);
assert_eq!(response.status, IntentStatus::Denied);
assert_eq!(response.code, "DENIED");
assert_eq!(response.message, "Policy violation");
}
#[test]
fn test_intent_status_is_terminal() {
assert!(IntentStatus::Ok.is_terminal());
assert!(IntentStatus::Denied.is_terminal());
assert!(IntentStatus::Error.is_terminal());
assert!(IntentStatus::Expired.is_terminal());
assert!(IntentStatus::Cancelled.is_terminal());
assert!(!IntentStatus::Pending.is_terminal());
}
#[test]
fn test_intent_status_is_success() {
assert!(IntentStatus::Ok.is_success());
assert!(!IntentStatus::Denied.is_success());
assert!(!IntentStatus::Error.is_success());
assert!(!IntentStatus::Expired.is_success());
assert!(!IntentStatus::Cancelled.is_success());
assert!(!IntentStatus::Pending.is_success());
}
#[test]
fn test_execution_result_default() {
let result = ExecutionResult::default();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.is_none());
assert!(result.stdout_bytes.is_none());
assert!(result.stderr.is_none());
assert!(result.output.is_none());
assert!(result.artifacts.is_empty());
assert!(result.resource_usage.is_none());
}
#[test]
fn test_execution_result_with_values() {
let result = ExecutionResult {
exit_code: 1,
stdout: Some("output".to_string()),
stderr: Some("error".to_string()),
output: Some(serde_json::json!({"key": "value"})),
artifacts: vec![Artifact {
name: "output.txt".to_string(),
content_type: "text/plain".to_string(),
size: 100,
sha256: "abc123".to_string(),
uri: None,
content: Some(b"content".to_vec()),
}],
..Default::default()
};
assert_eq!(result.exit_code, 1);
assert_eq!(result.stdout, Some("output".to_string()));
assert_eq!(result.artifacts.len(), 1);
}
#[test]
fn test_artifact_creation() {
let artifact = Artifact {
name: "report.pdf".to_string(),
content_type: "application/pdf".to_string(),
size: 1024,
sha256: "sha256hash".to_string(),
uri: Some("s3://bucket/report.pdf".to_string()),
content: None,
};
assert_eq!(artifact.name, "report.pdf");
assert_eq!(artifact.content_type, "application/pdf");
assert_eq!(artifact.size, 1024);
assert!(artifact.uri.is_some());
assert!(artifact.content.is_none());
}
#[test]
fn test_resource_usage_stats_default() {
let stats = ResourceUsageStats::default();
assert_eq!(stats.peak_memory_bytes, 0);
assert_eq!(stats.cpu_time_ms, 0);
assert_eq!(stats.wall_time_ms, 0);
assert_eq!(stats.disk_write_bytes, 0);
assert_eq!(stats.disk_read_bytes, 0);
assert_eq!(stats.network_tx_bytes, 0);
assert_eq!(stats.network_rx_bytes, 0);
}
#[test]
fn test_error_details_creation() {
let error = ErrorDetails {
code: "TIMEOUT".to_string(),
message: "Execution timed out".to_string(),
details: Some(serde_json::json!({"timeout_ms": 5000})),
retryable: true,
retry_after_ms: Some(1000),
};
assert_eq!(error.code, "TIMEOUT");
assert!(error.retryable);
assert_eq!(error.retry_after_ms, Some(1000));
}
#[test]
fn test_response_timing_default() {
let timing = ResponseTiming::default();
assert_eq!(timing.received_at_ms, 0);
assert_eq!(timing.started_at_ms, 0);
assert_eq!(timing.completed_at_ms, 0);
assert_eq!(timing.queue_time_ms, 0);
assert_eq!(timing.setup_time_ms, 0);
assert_eq!(timing.exec_time_ms, 0);
assert_eq!(timing.total_time_ms, 0);
}
#[test]
fn test_sandbox_info_creation() {
let info = SandboxInfo {
sandbox_id: "sb-123".to_string(),
backend: "landlock".to_string(),
profile: "standard".to_string(),
newly_created: true,
capabilities: SandboxCapabilitiesInfo {
can_write: false,
has_network: false,
readable_paths: vec!["/etc".to_string()],
writable_paths: vec![],
limits: ResourceLimitsInfo::default(),
},
};
assert_eq!(info.sandbox_id, "sb-123");
assert!(info.newly_created);
assert!(!info.capabilities.can_write);
}
#[test]
fn test_command_serialization() {
let cmd = Command::new("ls").args(["-la"]).env("PATH", "/usr/bin");
let json = serde_json::to_string(&cmd).unwrap();
let deserialized: Command = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.program, "ls");
assert_eq!(deserialized.args, vec!["-la"]);
}
#[test]
fn test_intent_request_serialization() {
let req = IntentRequest::new("fs.read.v1", serde_json::json!({"path": "/tmp"}));
let json = serde_json::to_string(&req).unwrap();
let deserialized: IntentRequest = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.capability, "fs.read.v1");
}
#[test]
fn test_intent_response_serialization() {
let id = Uuid::new_v4();
let response = IntentResponse::error(id, "ERROR", "Test error");
let json = serde_json::to_string(&response).unwrap();
let deserialized: IntentResponse = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.request_id, id);
assert_eq!(deserialized.status, IntentStatus::Error);
}
#[test]
fn test_intent_status_serialization() {
let status = IntentStatus::Pending;
let json = serde_json::to_string(&status).unwrap();
assert_eq!(json, "\"pending\"");
let deserialized: IntentStatus = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, IntentStatus::Pending);
}
#[test]
fn test_all_intent_statuses_serialization() {
let statuses = vec![
IntentStatus::Ok,
IntentStatus::Denied,
IntentStatus::Error,
IntentStatus::Expired,
IntentStatus::Cancelled,
IntentStatus::Pending,
];
for status in statuses {
let json = serde_json::to_string(&status).unwrap();
let deserialized: IntentStatus = serde_json::from_str(&json).unwrap();
assert_eq!(status, deserialized);
}
}
}