Skip to main content

smith_protocol/
lib.rs

1//! # Smith Protocol Library
2//!
3//! This crate defines the core message protocols and data structures used throughout
4//! the Smith AI-powered execution platform. It provides:
5//!
6//! - **Intent System**: Structured requests for capability execution with security signatures
7//! - **Result System**: Standardized responses with execution metadata and audit trails  
8//! - **Event System**: Real-time communication protocols for service coordination
9//! - **Capability Definitions**: Type-safe capability specifications and parameter schemas
10//! - **Security Context**: Comprehensive audit trails and sandbox isolation metadata
11//!
12//! ## Architecture Overview
13//!
14//! Smith follows a strict separation between intelligence (AI agents) and execution (secure runners).
15//! All communication flows through NATS JetStream using the protocols defined in this crate:
16//!
17//! ```text
18//! AI Agent → Intent → NATS → Policy Validation → Secure Executor → Result → AI Agent
19//! ```
20//!
21//! ## Key Design Principles
22//!
23//! - **Zero-Trust Security**: All intents are cryptographically signed and policy-validated
24//! - **Comprehensive Auditing**: Every action produces detailed audit trails for compliance
25//! - **Capability-Based Model**: Fine-grained permissions using versioned capability strings
26//! - **Type Safety**: Strongly typed parameters and results with JSON Schema validation
27//! - **Time-Based Ordering**: UUIDv7 identifiers provide distributed ordering guarantees
28//!
29//! ## Basic Usage
30//!
31//! ```rust,no_run
32//! use smith_protocol::{Intent, Capability, IntentResult, RunnerMetadata};
33//! use serde_json::json;
34//! use std::time::{SystemTime, UNIX_EPOCH};
35//!
36//! // Create a file read intent
37//! let intent = Intent::new(
38//!     Capability::FsReadV1,
39//!     "production".to_string(),
40//!     json!({"path": "/etc/hostname", "max_bytes": 1024}),
41//!     30000, // 30 second TTL
42//!     "client-public-key".to_string(),
43//! );
44//!
45//! // Results include comprehensive execution metadata
46//! let start_time_ns = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
47//! let end_time_ns = start_time_ns + 1_000_000; // 1ms later
48//! let runner_metadata = RunnerMetadata::empty();
49//!
50//! let result = IntentResult::success(
51//!     intent.id.clone(),
52//!     json!({"content": "server.example.com\n"}),
53//!     start_time_ns,
54//!     end_time_ns,
55//!     runner_metadata,
56//!     "audit-12345".to_string(),
57//! );
58//! ```
59//!
60//! ## Security Model
61//!
62//! The protocol implements a multi-layer security model:
63//!
64//! 1. **Cryptographic Signatures**: All intents are signed with Ed25519 keys
65//! 2. **Policy Validation**: CUE-based policies validate intents before execution  
66//! 3. **Sandbox Isolation**: Multiple isolation layers (Landlock, seccomp, cgroups)
67//! 4. **Resource Limits**: CPU, memory, and I/O constraints enforced per capability
68//! 5. **Audit Trails**: Complete execution history for compliance and forensics
69//!
70//! For implementation details, see the specific capability modules and the executor documentation.
71
72use anyhow::Context;
73use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
74use ed25519_dalek::{Signature, Verifier, VerifyingKey, PUBLIC_KEY_LENGTH};
75use serde::{Deserialize, Serialize};
76use std::collections::{BTreeMap, HashMap};
77use std::convert::TryInto;
78use uuid::Uuid;
79
80#[cfg(feature = "typescript")]
81use ts_rs::TS;
82
83/// Protocol version for capability negotiation and compatibility checking.
84///
85/// This version is incremented when breaking changes are made to the protocol.
86/// Clients and servers use this for compatibility verification during handshake.
87pub const PROTOCOL_VERSION: u32 = 0;
88
89pub mod benchmark;
90pub mod client;
91pub mod idempotency;
92pub mod negotiation;
93pub mod policy;
94pub mod policy_abi;
95pub mod reasoning;
96pub mod result_schema;
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
99#[cfg_attr(feature = "typescript", derive(TS))]
100#[cfg_attr(
101    feature = "typescript",
102    ts(export, export_to = "client/src/lib/smith-protocol/generated.ts")
103)]
104#[serde(tag = "type", content = "data")]
105/// Command sent to the core service
106pub enum Command {
107    /// Initialize service with client capabilities
108    Handshake {
109        version: u32,
110        capabilities: Vec<String>,
111    },
112    /// Plan execution request
113    Plan {
114        #[cfg_attr(feature = "typescript", ts(type = "string"))]
115        request_id: Uuid,
116        goal: String,
117        context: HashMap<String, String>,
118    },
119    /// Execute a tool call
120    ToolCall {
121        #[cfg_attr(feature = "typescript", ts(type = "string"))]
122        request_id: Uuid,
123        tool: String,
124        #[cfg_attr(feature = "typescript", ts(type = "unknown"))]
125        args: serde_json::Value,
126        timeout_ms: Option<u64>,
127    },
128    /// Load hook configuration
129    HookLoad {
130        #[cfg_attr(feature = "typescript", ts(type = "string"))]
131        request_id: Uuid,
132        hook_type: String,
133        script: String,
134    },
135    /// Shell command execution
136    ShellExec {
137        #[cfg_attr(feature = "typescript", ts(type = "string"))]
138        request_id: Uuid,
139        command: String,
140        shell: Option<String>,
141        cwd: Option<String>,
142        env: HashMap<String, String>,
143        timeout_ms: Option<u64>,
144    },
145    /// Request service shutdown
146    Shutdown,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
150#[cfg_attr(feature = "typescript", derive(TS))]
151#[cfg_attr(
152    feature = "typescript",
153    ts(export, export_to = "client/src/lib/smith-protocol/generated.ts")
154)]
155#[serde(tag = "type", content = "data")]
156/// Event emitted by the core service
157pub enum Event {
158    /// Service ready with capabilities
159    Ready {
160        version: u32,
161        capabilities: Vec<String>,
162        #[cfg_attr(feature = "typescript", ts(type = "string"))]
163        service_id: Uuid,
164    },
165    /// State change notification
166    StateChange {
167        #[cfg_attr(feature = "typescript", ts(type = "string | undefined"))]
168        request_id: Option<Uuid>,
169        state: ServiceState,
170        #[cfg_attr(feature = "typescript", ts(type = "bigint"))]
171        timestamp: u64,
172    },
173    /// Log message
174    Log {
175        level: LogLevel,
176        message: String,
177        #[cfg_attr(feature = "typescript", ts(type = "bigint"))]
178        timestamp: u64,
179        #[cfg_attr(feature = "typescript", ts(type = "string | undefined"))]
180        request_id: Option<Uuid>,
181    },
182    /// Token usage tracking
183    TokenUsage {
184        #[cfg_attr(feature = "typescript", ts(type = "string"))]
185        request_id: Uuid,
186        #[cfg_attr(feature = "typescript", ts(type = "bigint"))]
187        tokens_used: u64,
188        #[cfg_attr(feature = "typescript", ts(type = "bigint | undefined"))]
189        tokens_remaining: Option<u64>,
190    },
191    /// Shell command output
192    ShellOutput {
193        #[cfg_attr(feature = "typescript", ts(type = "string"))]
194        request_id: Uuid,
195        stdout: Option<String>,
196        stderr: Option<String>,
197        exit_code: Option<i32>,
198        finished: bool,
199    },
200    /// Tool call result
201    ToolResult {
202        #[cfg_attr(feature = "typescript", ts(type = "string"))]
203        request_id: Uuid,
204        success: bool,
205        #[cfg_attr(feature = "typescript", ts(type = "unknown"))]
206        result: serde_json::Value,
207        error: Option<String>,
208    },
209    /// Hook execution result
210    HookResult {
211        #[cfg_attr(feature = "typescript", ts(type = "string"))]
212        request_id: Uuid,
213        action: HookAction,
214    },
215    /// Graph/DAG state delta
216    GraphDelta {
217        #[cfg_attr(feature = "typescript", ts(type = "string | undefined"))]
218        request_id: Option<Uuid>,
219        nodes_added: Vec<GraphNode>,
220        edges_added: Vec<GraphEdge>,
221        #[cfg_attr(feature = "typescript", ts(type = "string[]"))]
222        nodes_removed: Vec<Uuid>,
223        #[cfg_attr(feature = "typescript", ts(type = "string[]"))]
224        edges_removed: Vec<Uuid>,
225    },
226    /// Service error
227    Error {
228        #[cfg_attr(feature = "typescript", ts(type = "string | undefined"))]
229        request_id: Option<Uuid>,
230        error_code: String,
231        message: String,
232        #[cfg_attr(feature = "typescript", ts(type = "bigint"))]
233        timestamp: u64,
234    },
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
238#[cfg_attr(feature = "typescript", derive(TS))]
239#[cfg_attr(
240    feature = "typescript",
241    ts(export, export_to = "client/src/lib/smith-protocol/generated.ts")
242)]
243/// Service operational state
244pub enum ServiceState {
245    Starting,
246    Ready,
247    Processing,
248    Error,
249    Shutting,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
253#[cfg_attr(feature = "typescript", derive(TS))]
254#[cfg_attr(
255    feature = "typescript",
256    ts(export, export_to = "client/src/lib/smith-protocol/generated.ts")
257)]
258/// Log levels matching standard conventions
259pub enum LogLevel {
260    Trace,
261    Debug,
262    Info,
263    Warn,
264    Error,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
268#[cfg_attr(feature = "typescript", derive(TS))]
269#[cfg_attr(
270    feature = "typescript",
271    ts(export, export_to = "client/src/lib/smith-protocol/generated.ts")
272)]
273#[serde(tag = "type", content = "data")]
274/// Hook action result from script execution
275pub enum HookAction {
276    Allow(serde_json::Value),
277    Block { reason: String },
278    Transform(serde_json::Value),
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
282#[cfg_attr(feature = "typescript", derive(TS))]
283#[cfg_attr(
284    feature = "typescript",
285    ts(export, export_to = "client/src/lib/smith-protocol/generated.ts")
286)]
287/// Graph node for DAG visualization
288pub struct GraphNode {
289    #[cfg_attr(feature = "typescript", ts(type = "string"))]
290    pub id: Uuid,
291    pub label: String,
292    pub node_type: String,
293    pub metadata: HashMap<String, String>,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
297#[cfg_attr(feature = "typescript", derive(TS))]
298#[cfg_attr(
299    feature = "typescript",
300    ts(export, export_to = "client/src/lib/smith-protocol/generated.ts")
301)]
302/// Graph edge for DAG visualization
303pub struct GraphEdge {
304    #[cfg_attr(feature = "typescript", ts(type = "string"))]
305    pub id: Uuid,
306    #[cfg_attr(feature = "typescript", ts(type = "string"))]
307    pub from: Uuid,
308    #[cfg_attr(feature = "typescript", ts(type = "string"))]
309    pub to: Uuid,
310    pub label: Option<String>,
311    pub edge_type: String,
312}
313
314/// Capability strings for handshake negotiation
315pub mod capabilities {
316    pub const SHELL_EXEC: &str = "shell_exec";
317    pub const HOOKS_JS: &str = "hooks_js";
318    pub const HOOKS_RUST: &str = "hooks_rust";
319    pub const REPLAY: &str = "replay";
320    pub const NATS: &str = "nats";
321    pub const TRACING: &str = "tracing";
322}
323
324// JetStream-native protocol definitions for Smith executor platform
325
326#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
327#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
328#[cfg_attr(feature = "typescript", derive(TS))]
329#[cfg_attr(
330    feature = "typescript",
331    ts(export, export_to = "client/src/lib/smith-protocol/generated.ts")
332)]
333#[serde(rename_all = "snake_case")]
334/// Intent capabilities enum for JetStream execution
335pub enum Capability {
336    #[serde(rename = "fs.read.v1")]
337    FsReadV1,
338    #[serde(rename = "http.fetch.v1")]
339    HttpFetchV1,
340    #[serde(rename = "fs.write.v1")]
341    FsWriteV1,
342    #[serde(rename = "git.clone.v1")]
343    GitCloneV1,
344    #[serde(rename = "archive.read.v1")]
345    ArchiveReadV1,
346    #[serde(rename = "sqlite.query.v1")]
347    SqliteQueryV1,
348    #[serde(rename = "bench.report.v1")]
349    BenchReportV1,
350    #[serde(rename = "shell.exec.v1")]
351    ShellExec,
352    /// Alias for HttpFetchV1 (for test compatibility)
353    HttpFetch,
354}
355
356impl std::fmt::Display for Capability {
357    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358        match self {
359            Capability::FsReadV1 => write!(f, "fs.read.v1"),
360            Capability::HttpFetchV1 => write!(f, "http.fetch.v1"),
361            Capability::FsWriteV1 => write!(f, "fs.write.v1"),
362            Capability::GitCloneV1 => write!(f, "git.clone.v1"),
363            Capability::ArchiveReadV1 => write!(f, "archive.read.v1"),
364            Capability::SqliteQueryV1 => write!(f, "sqlite.query.v1"),
365            Capability::BenchReportV1 => write!(f, "bench.report.v1"),
366            Capability::ShellExec => write!(f, "shell.exec.v1"),
367            Capability::HttpFetch => write!(f, "http.fetch.v1"),
368        }
369    }
370}
371
372impl std::str::FromStr for Capability {
373    type Err = anyhow::Error;
374
375    fn from_str(s: &str) -> Result<Self, Self::Err> {
376        Self::parse_capability_string(s)
377    }
378}
379
380impl Capability {
381    /// Parse a capability string into a Capability enum
382    fn parse_capability_string(s: &str) -> Result<Self, anyhow::Error> {
383        match s {
384            "fs.read.v1" => Ok(Capability::FsReadV1),
385            "http.fetch.v1" => Ok(Capability::HttpFetchV1),
386            "fs.write.v1" => Ok(Capability::FsWriteV1),
387            "git.clone.v1" => Ok(Capability::GitCloneV1),
388            "archive.read.v1" => Ok(Capability::ArchiveReadV1),
389            "sqlite.query.v1" => Ok(Capability::SqliteQueryV1),
390            "bench.report.v1" => Ok(Capability::BenchReportV1),
391            "shell.exec.v1" => Ok(Capability::ShellExec),
392            "http.fetch" => Ok(Capability::HttpFetch), // For test compatibility
393            _ => Err(anyhow::anyhow!("Unknown capability: {}", s)),
394        }
395    }
396
397    /// Get all available capabilities as strings
398    pub fn all_capabilities() -> Vec<&'static str> {
399        vec![
400            "fs.read.v1",
401            "http.fetch.v1",
402            "fs.write.v1",
403            "git.clone.v1",
404            "archive.read.v1",
405            "sqlite.query.v1",
406            "bench.report.v1",
407            "shell.exec.v1",
408        ]
409    }
410
411    /// Check if a capability string is valid
412    pub fn is_valid_capability(s: &str) -> bool {
413        Self::all_capabilities().contains(&s)
414    }
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize)]
418#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
419#[cfg_attr(feature = "typescript", derive(TS))]
420#[cfg_attr(
421    feature = "typescript",
422    ts(export, export_to = "client/src/lib/smith-protocol/generated.ts")
423)]
424/// Intent sent to executors via JetStream
425pub struct Intent {
426    /// Unique intent identifier (UUIDv7 for time ordering)
427    pub id: String,
428    /// Capability being requested
429    pub capability: Capability,
430    /// Routing domain/shard
431    pub domain: String,
432    /// Capability-specific parameters
433    #[cfg_attr(feature = "typescript", ts(type = "unknown"))]
434    pub params: serde_json::Value,
435    /// Creation timestamp in nanoseconds
436    #[cfg_attr(feature = "typescript", ts(type = "bigint"))]
437    pub created_at_ns: u128,
438    /// Time-to-live in milliseconds
439    pub ttl_ms: u32,
440    /// Nonce for deduplication and replay defense
441    pub nonce: String,
442    /// Public key of the signer (base64)
443    pub signer: String,
444    /// Detached signature of canonical intent body (base64)
445    pub signature_b64: String,
446    /// Additional metadata for the intent
447    #[cfg_attr(feature = "typescript", ts(type = "Record<string, unknown>"))]
448    pub metadata: std::collections::HashMap<String, serde_json::Value>,
449}
450
451impl Intent {
452    /// Verify the signature of this intent using the embedded signer public key.
453    pub fn verify_signature(&self) -> anyhow::Result<bool> {
454        if self.signature_b64.trim().is_empty() {
455            return Ok(false);
456        }
457
458        let signer_bytes = BASE64
459            .decode(self.signer.trim())
460            .context("Failed to decode signer public key from base64")?;
461
462        let signer_array: [u8; PUBLIC_KEY_LENGTH] = signer_bytes.try_into().map_err(|_| {
463            anyhow::anyhow!("Signer public key must be {PUBLIC_KEY_LENGTH} bytes after decoding")
464        })?;
465
466        let verifying_key = VerifyingKey::from_bytes(&signer_array)
467            .map_err(|err| anyhow::anyhow!("Invalid signer public key: {err}"))?;
468
469        self.verify_with_key(&verifying_key)
470    }
471
472    /// Verify the signature of this intent with a supplied verifying key.
473    pub fn verify_with_key(&self, verifying_key: &VerifyingKey) -> anyhow::Result<bool> {
474        if self.signature_b64.trim().is_empty() {
475            return Ok(false);
476        }
477
478        let signature_bytes = BASE64
479            .decode(self.signature_b64.trim())
480            .context("Failed to decode intent signature from base64")?;
481
482        let signature = Signature::from_slice(&signature_bytes)
483            .map_err(|err| anyhow::anyhow!("Invalid signature format: {err}"))?;
484
485        let canonical_json = self.canonical_json()?;
486
487        match verifying_key.verify(canonical_json.as_bytes(), &signature) {
488            Ok(_) => Ok(true),
489            Err(_) => Ok(false),
490        }
491    }
492}
493
494#[derive(Debug, Clone, Serialize, Deserialize)]
495#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
496#[cfg_attr(feature = "typescript", derive(TS))]
497#[cfg_attr(
498    feature = "typescript",
499    ts(export, export_to = "client/src/lib/smith-protocol/generated.ts")
500)]
501/// Result of intent execution
502pub struct IntentResult {
503    /// ID of the original intent
504    pub intent_id: String,
505    /// Execution status
506    pub status: ExecutionStatus,
507    /// Success output (if status is Ok)
508    #[cfg_attr(feature = "typescript", ts(type = "unknown | undefined"))]
509    pub output: Option<serde_json::Value>,
510    /// Error details (if status is Error/Denied/Timeout/Killed)
511    pub error: Option<ExecutionError>,
512    /// When execution started (nanoseconds)
513    #[cfg_attr(feature = "typescript", ts(type = "bigint"))]
514    pub started_at_ns: u128,
515    /// When execution finished (nanoseconds)
516    #[cfg_attr(feature = "typescript", ts(type = "bigint"))]
517    pub finished_at_ns: u128,
518    /// Runtime metadata from the executor
519    pub runner_meta: RunnerMetadata,
520    /// Reference to audit log entry
521    pub audit_ref: AuditRef,
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
525#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
526#[cfg_attr(feature = "typescript", derive(TS))]
527#[cfg_attr(
528    feature = "typescript",
529    ts(export, export_to = "client/src/lib/smith-protocol/generated.ts")
530)]
531/// Reference to an audit log entry
532pub struct AuditRef {
533    /// Audit log entry ID
534    pub id: String,
535    /// Timestamp of the audit entry
536    #[cfg_attr(feature = "typescript", ts(type = "bigint"))]
537    pub timestamp: u64,
538    /// Audit trail hash for integrity verification
539    pub hash: String,
540}
541
542#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
543#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
544#[cfg_attr(feature = "typescript", derive(TS))]
545#[cfg_attr(
546    feature = "typescript",
547    ts(export, export_to = "client/src/lib/smith-protocol/generated.ts")
548)]
549#[serde(rename_all = "lowercase")]
550/// Execution status enum
551pub enum ExecutionStatus {
552    Ok,
553    Error,
554    Denied,
555    Timeout,
556    Killed,
557    /// Alias for Ok (for test compatibility)
558    Success,
559    /// Alias for Error (for test compatibility)
560    Failed,
561}
562
563#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
564#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
565#[cfg_attr(feature = "typescript", derive(TS))]
566#[cfg_attr(
567    feature = "typescript",
568    ts(export, export_to = "client/src/lib/smith-protocol/generated.ts")
569)]
570/// Error details for failed executions
571pub struct ExecutionError {
572    /// Error code
573    pub code: String,
574    /// Human-readable error message
575    pub message: String,
576}
577
578#[derive(Debug, Clone, Serialize, Deserialize)]
579#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
580#[cfg_attr(feature = "typescript", derive(TS))]
581#[cfg_attr(
582    feature = "typescript",
583    ts(export, export_to = "client/src/lib/smith-protocol/generated.ts")
584)]
585/// Runtime metadata from executor
586pub struct RunnerMetadata {
587    /// Process ID of the runner
588    pub pid: u32,
589    /// CPU time used in milliseconds
590    pub cpu_ms: u32,
591    /// Maximum RSS in kilobytes
592    pub max_rss_kb: u32,
593    /// Capability digest used for bundle enforcement (optional for compatibility)
594    pub capability_digest: Option<String>,
595}
596
597impl RunnerMetadata {
598    /// Create empty metadata (used for denied/failed executions)
599    pub fn empty() -> Self {
600        Self {
601            pid: 0,
602            cpu_ms: 0,
603            max_rss_kb: 0,
604            capability_digest: None,
605        }
606    }
607}
608
609#[derive(Debug, Clone, Serialize, Deserialize)]
610#[cfg_attr(feature = "typescript", derive(TS))]
611#[cfg_attr(
612    feature = "typescript",
613    ts(export, export_to = "client/src/lib/smith-protocol/generated.ts")
614)]
615/// Audit log entry for compliance and debugging
616pub struct AuditEntry {
617    /// Intent that was processed
618    pub intent_id: String,
619    /// Execution result summary
620    pub result_status: ExecutionStatus,
621    /// Timestamp when audit was created
622    #[cfg_attr(feature = "typescript", ts(type = "bigint"))]
623    pub timestamp_ns: u128,
624    /// Executor instance that processed this intent
625    pub executor_id: String,
626    /// Policy decisions made during admission
627    pub policy_decisions: Vec<PolicyDecision>,
628    /// Security context
629    pub security_context: SecurityContext,
630}
631
632#[derive(Debug, Clone, Serialize, Deserialize)]
633#[cfg_attr(feature = "typescript", derive(TS))]
634#[cfg_attr(
635    feature = "typescript",
636    ts(export, export_to = "client/src/lib/smith-protocol/generated.ts")
637)]
638/// Policy decision made during admission
639pub struct PolicyDecision {
640    /// Policy rule that was evaluated
641    pub rule_name: String,
642    /// Result of the policy evaluation
643    pub decision: PolicyResult,
644    /// Human-readable reason for the decision
645    pub reason: String,
646}
647
648#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
649#[cfg_attr(feature = "typescript", derive(TS))]
650#[cfg_attr(
651    feature = "typescript",
652    ts(export, export_to = "client/src/lib/smith-protocol/generated.ts")
653)]
654#[serde(rename_all = "lowercase")]
655/// Policy evaluation result
656pub enum PolicyResult {
657    Allow,
658    Deny,
659    Transform,
660}
661
662#[derive(Debug, Clone, Serialize, Deserialize)]
663#[cfg_attr(feature = "typescript", derive(TS))]
664#[cfg_attr(
665    feature = "typescript",
666    ts(export, export_to = "client/src/lib/smith-protocol/generated.ts")
667)]
668/// Security context for audit trail
669pub struct SecurityContext {
670    /// Sandbox mode used for execution
671    pub sandbox_mode: Option<SandboxMode>,
672    /// User namespace ID
673    pub user_ns: Option<u32>,
674    /// Mount namespace ID
675    pub mount_ns: Option<u32>,
676    /// PID namespace ID
677    pub pid_ns: Option<u32>,
678    /// Network namespace ID
679    pub net_ns: Option<u32>,
680    /// Cgroup path
681    pub cgroup_path: Option<String>,
682    /// Landlock restrictions applied
683    pub landlock_enabled: bool,
684    /// Seccomp filter applied
685    pub seccomp_enabled: bool,
686    /// Allow-list entries accessed during execution
687    pub allowlist_hits: Option<Vec<AllowlistHit>>,
688}
689
690#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
691#[cfg_attr(feature = "typescript", derive(TS))]
692#[cfg_attr(
693    feature = "typescript",
694    ts(export, export_to = "client/src/lib/smith-protocol/generated.ts")
695)]
696#[serde(rename_all = "lowercase")]
697/// Sandbox execution modes
698pub enum SandboxMode {
699    /// Full sandbox with all security features enabled
700    Full,
701    /// Partial sandbox with limited features (development/testing)
702    Demo,
703    /// No sandboxing (unsafe, for compatibility only)
704    Unsafe,
705}
706
707#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
708#[cfg_attr(feature = "typescript", derive(TS))]
709#[cfg_attr(
710    feature = "typescript",
711    ts(export, export_to = "client/src/lib/smith-protocol/generated.ts")
712)]
713/// Allow-list access record
714pub struct AllowlistHit {
715    /// Type of resource accessed
716    pub resource_type: String,
717    /// Resource identifier (path, URL, etc.)
718    pub resource_id: String,
719    /// Access operation (read, write, execute, etc.)
720    pub operation: String,
721    /// Timestamp of access
722    #[cfg_attr(feature = "typescript", ts(type = "bigint"))]
723    pub timestamp_ns: u128,
724}
725
726/// Resource usage metrics for audit
727#[derive(Debug, Clone, Serialize, Deserialize)]
728pub struct ResourceUsage {
729    /// Peak memory usage in KB
730    pub peak_memory_kb: u32,
731    /// CPU time consumed in milliseconds
732    pub cpu_time_ms: u32,
733    /// Wall clock time for execution in milliseconds
734    pub wall_time_ms: u32,
735    /// Number of file descriptors used
736    pub fd_count: u32,
737    /// Number of bytes read from disk
738    pub disk_read_bytes: u64,
739    /// Number of bytes written to disk
740    pub disk_write_bytes: u64,
741    /// Number of bytes sent over network
742    pub network_tx_bytes: u64,
743    /// Number of bytes received over network
744    pub network_rx_bytes: u64,
745}
746
747/// Capability specification for discovery and documentation
748#[derive(Debug, Clone, Serialize, Deserialize)]
749#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
750pub struct CapabilitySpec {
751    /// Capability name (e.g., "fs.read.v1")
752    pub name: String,
753    /// Human-readable description
754    pub description: String,
755    /// JSON schema for parameter validation
756    pub params_schema: serde_json::Value,
757    /// Example parameters
758    pub example_params: serde_json::Value,
759    /// Resource requirements and limits
760    pub resource_requirements: ResourceRequirements,
761    /// Security implications and recommendations
762    pub security_notes: Vec<String>,
763}
764
765/// Resource requirements for a capability
766#[derive(Debug, Clone, Serialize, Deserialize)]
767#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
768pub struct ResourceRequirements {
769    /// Typical CPU time required (ms)
770    pub cpu_ms_typical: u32,
771    /// Maximum memory usage (KB)
772    pub memory_kb_max: u32,
773    /// Network access required
774    pub network_access: bool,
775    /// File system access required
776    pub filesystem_access: bool,
777    /// External command execution required
778    pub external_commands: bool,
779}
780
781/// Resource limits enforced during intent execution
782///
783/// These limits provide defense-in-depth resource isolation and are enforced
784/// by the secure executor's jailer. Violations result in execution termination.
785#[derive(Debug, Clone, Serialize, Deserialize)]
786#[cfg_attr(feature = "typescript", derive(TS))]
787#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
788pub struct ExecutionLimits {
789    /// CPU time limit per 100ms interval
790    pub cpu_ms_per_100ms: u32,
791    /// Memory limit in bytes
792    pub mem_bytes: u64,
793    /// I/O limit in bytes
794    pub io_bytes: u64,
795    /// Maximum number of processes
796    pub pids_max: u32,
797    /// Total timeout in milliseconds
798    pub timeout_ms: u64,
799}
800
801impl Default for ExecutionLimits {
802    fn default() -> Self {
803        Self {
804            cpu_ms_per_100ms: 50,         // 50% CPU usage
805            mem_bytes: 128 * 1024 * 1024, // 128MB
806            io_bytes: 10 * 1024 * 1024,   // 10MB
807            pids_max: 10,
808            timeout_ms: 30000, // 30 seconds
809        }
810    }
811}
812
813/// Enhanced audit event (replaces AuditEntry for better compatibility)
814#[derive(Debug, Clone, Serialize, Deserialize)]
815pub struct AuditEvent {
816    /// Intent that was processed
817    pub intent_id: String,
818    /// Execution result summary
819    pub result_status: ExecutionStatus,
820    /// Timestamp when audit was created
821    pub timestamp_ns: u128,
822    /// Executor instance that processed this intent
823    pub executor_id: String,
824    /// Policy decisions made during admission
825    pub policy_decisions: Vec<PolicyDecision>,
826    /// Security context
827    pub security_context: SecurityContext,
828    /// Resource usage metrics
829    pub resource_usage: Option<ResourceUsage>,
830}
831
832/// Capability-specific parameter types
833pub mod params {
834    use super::*;
835
836    /// Parameters for fs.read.v1 capability
837    #[derive(Debug, Clone, Serialize, Deserialize)]
838    #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
839    pub struct FsReadV1 {
840        /// File path to read
841        pub path: String,
842        /// Maximum bytes to read (optional)
843        pub max_bytes: Option<u64>,
844        /// Whether to follow symlinks
845        pub follow_symlinks: Option<bool>,
846    }
847
848    /// Parameters for http.fetch.v1 capability
849    #[derive(Debug, Clone, Serialize, Deserialize)]
850    #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
851    pub struct HttpFetchV1 {
852        /// URL to fetch
853        pub url: String,
854        /// HTTP method (default: GET)
855        pub method: Option<String>,
856        /// HTTP headers
857        pub headers: Option<HashMap<String, String>>,
858        /// Request body (for POST/PUT)
859        pub body: Option<String>,
860        /// Timeout in milliseconds
861        pub timeout_ms: Option<u32>,
862    }
863
864    /// Parameters for archive.read.v1 capability
865    #[derive(Debug, Clone, Serialize, Deserialize)]
866    #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
867    pub struct ArchiveReadV1 {
868        /// Archive file path to read
869        pub path: String,
870        /// Whether to extract file contents (default: false, metadata only)
871        pub extract_content: Option<bool>,
872    }
873
874    /// Parameters for sqlite.query.v1 capability
875    #[derive(Debug, Clone, Serialize, Deserialize)]
876    #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
877    pub struct SqliteQueryV1 {
878        /// SQLite database file path
879        pub database_path: String,
880        /// SQL query to execute (read-only)
881        pub query: String,
882        /// Query parameters for prepared statements
883        pub params: Option<Vec<serde_json::Value>>,
884        /// Maximum rows to return (default: 1000)
885        pub max_rows: Option<u32>,
886        /// Query timeout in milliseconds (default: 30000)
887        pub timeout_ms: Option<u32>,
888    }
889
890    /// Parameters for bench.report.v1 capability
891    #[derive(Debug, Clone, Serialize, Deserialize)]
892    #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
893    pub struct BenchReportV1 {
894        /// Benchmark name/identifier
895        pub benchmark_name: String,
896        /// Performance metrics to report
897        pub metrics: HashMap<String, f64>,
898        /// Benchmark metadata
899        pub metadata: Option<HashMap<String, serde_json::Value>>,
900        /// Historical data retention days (default: 30)
901        pub retention_days: Option<u32>,
902    }
903
904    /// Parameters for shell.exec.v1 capability
905    #[derive(Debug, Clone, Serialize, Deserialize)]
906    #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
907    pub struct ShellExecV1 {
908        /// Command to execute
909        pub command: String,
910        /// Command arguments
911        pub args: Option<Vec<String>>,
912        /// Environment variables
913        pub env: Option<HashMap<String, String>>,
914        /// Working directory
915        pub cwd: Option<String>,
916        /// Timeout in milliseconds (default: 30000, max: 600000)
917        pub timeout_ms: Option<u32>,
918        /// Data to write to stdin
919        pub stdin: Option<String>,
920    }
921}
922
923/// Intent creation and signing utilities
924impl Intent {
925    /// Create a new intent with the given parameters
926    pub fn new(
927        capability: Capability,
928        domain: String,
929        params: serde_json::Value,
930        ttl_ms: u32,
931        signer: String,
932    ) -> Self {
933        let id = Self::generate_intent_id();
934        let nonce = Self::generate_nonce();
935        let created_at_ns = Self::current_timestamp_ns();
936
937        Self {
938            id,
939            capability,
940            domain,
941            params,
942            created_at_ns,
943            ttl_ms,
944            nonce,
945            signer,
946            signature_b64: String::new(), // Will be set by signing process
947            metadata: std::collections::HashMap::new(),
948        }
949    }
950
951    /// Generate a time-ordered intent ID using UUIDv7
952    fn generate_intent_id() -> String {
953        uuid::Uuid::now_v7().to_string()
954    }
955
956    /// Generate a random nonce for deduplication
957    fn generate_nonce() -> String {
958        uuid::Uuid::new_v4().to_string()
959    }
960
961    /// Get current timestamp in nanoseconds since epoch
962    fn current_timestamp_ns() -> u128 {
963        std::time::SystemTime::now()
964            .duration_since(std::time::UNIX_EPOCH)
965            .unwrap()
966            .as_nanos()
967    }
968
969    /// Get canonical JSON representation for signing
970    pub fn canonical_json(&self) -> anyhow::Result<String> {
971        let canonical = self.create_unsigned_copy();
972        let sorted_json = Self::sort_json_keys(canonical)?;
973        Ok(serde_json::to_string(&sorted_json)?)
974    }
975
976    /// Create a copy of the intent without signature for canonical form
977    fn create_unsigned_copy(&self) -> Self {
978        let mut canonical = self.clone();
979        canonical.signature_b64 = String::new();
980        canonical
981    }
982
983    /// Sort JSON keys for deterministic output
984    fn sort_json_keys(value: impl Serialize) -> anyhow::Result<serde_json::Value> {
985        let mut json = serde_json::to_value(value)?;
986        if let serde_json::Value::Object(ref mut map) = json {
987            // Remove signature_b64 field completely for canonical form
988            map.remove("signature_b64");
989
990            let sorted: BTreeMap<_, _> = map.iter().collect();
991            *map = sorted
992                .into_iter()
993                .map(|(k, v)| (k.clone(), v.clone()))
994                .collect();
995        }
996        Ok(json)
997    }
998
999    /// Check if intent has expired based on current time
1000    pub fn is_expired(&self) -> bool {
1001        let now_ns = Self::current_timestamp_ns();
1002        let expiry_ns = self.created_at_ns + self.ttl_as_nanoseconds();
1003        now_ns > expiry_ns
1004    }
1005
1006    /// Convert TTL from milliseconds to nanoseconds
1007    fn ttl_as_nanoseconds(&self) -> u128 {
1008        (self.ttl_ms as u128) * 1_000_000
1009    }
1010
1011    /// Get the subject pattern for this intent
1012    pub fn subject(&self) -> String {
1013        smith_bus::builders::IntentSubject::with_domain(&self.capability.to_string(), &self.domain)
1014    }
1015
1016    /// Get the result subject for this intent
1017    pub fn result_subject(&self) -> String {
1018        smith_bus::builders::ResultSubject::for_intent(&self.id)
1019    }
1020}
1021
1022impl IntentResult {
1023    /// Create a successful result
1024    pub fn success(
1025        intent_id: String,
1026        output: serde_json::Value,
1027        started_at_ns: u128,
1028        finished_at_ns: u128,
1029        runner_meta: RunnerMetadata,
1030        audit_ref: String,
1031    ) -> Self {
1032        Self::create_result(
1033            intent_id,
1034            ExecutionStatus::Ok,
1035            Some(output),
1036            None,
1037            started_at_ns,
1038            finished_at_ns,
1039            runner_meta,
1040            audit_ref,
1041        )
1042    }
1043
1044    /// Create an error result
1045    pub fn error(
1046        intent_id: String,
1047        error_code: String,
1048        error_message: String,
1049        started_at_ns: u128,
1050        finished_at_ns: u128,
1051        runner_meta: RunnerMetadata,
1052        audit_ref: String,
1053    ) -> Self {
1054        let error = Some(ExecutionError {
1055            code: error_code,
1056            message: error_message,
1057        });
1058
1059        Self::create_result(
1060            intent_id,
1061            ExecutionStatus::Error,
1062            None,
1063            error,
1064            started_at_ns,
1065            finished_at_ns,
1066            runner_meta,
1067            audit_ref,
1068        )
1069    }
1070
1071    /// Create a denied result
1072    pub fn denied(intent_id: String, reason: String, audit_ref: String) -> Self {
1073        let now_ns = Intent::current_timestamp_ns();
1074        let error = Some(ExecutionError {
1075            code: "POLICY_DENIED".to_string(),
1076            message: reason,
1077        });
1078        let empty_metadata = RunnerMetadata::empty();
1079
1080        Self::create_result(
1081            intent_id,
1082            ExecutionStatus::Denied,
1083            None,
1084            error,
1085            now_ns,
1086            now_ns,
1087            empty_metadata,
1088            audit_ref,
1089        )
1090    }
1091
1092    /// Create a result with the given parameters
1093    #[allow(clippy::too_many_arguments)]
1094    fn create_result(
1095        intent_id: String,
1096        status: ExecutionStatus,
1097        output: Option<serde_json::Value>,
1098        error: Option<ExecutionError>,
1099        started_at_ns: u128,
1100        finished_at_ns: u128,
1101        runner_meta: RunnerMetadata,
1102        audit_ref: String,
1103    ) -> Self {
1104        Self {
1105            intent_id,
1106            status,
1107            output,
1108            error,
1109            started_at_ns,
1110            finished_at_ns,
1111            runner_meta,
1112            audit_ref: AuditRef {
1113                id: audit_ref,
1114                timestamp: std::time::SystemTime::now()
1115                    .duration_since(std::time::UNIX_EPOCH)
1116                    .unwrap_or_default()
1117                    .as_secs(),
1118                hash: "placeholder".to_string(),
1119            },
1120        }
1121    }
1122}
1123
1124#[cfg(test)]
1125mod tests {
1126    use super::*;
1127    use ed25519_dalek::Signer;
1128    use serde_json::json;
1129
1130    #[test]
1131    fn test_capability_serialization() {
1132        let cap = Capability::FsReadV1;
1133        let serialized = serde_json::to_string(&cap).unwrap();
1134        assert_eq!(serialized, r#""fs.read.v1""#);
1135
1136        let deserialized: Capability = serde_json::from_str(&serialized).unwrap();
1137        assert_eq!(deserialized, cap);
1138    }
1139
1140    #[test]
1141    fn test_capability_from_str() {
1142        assert_eq!(
1143            "fs.read.v1".parse::<Capability>().unwrap(),
1144            Capability::FsReadV1
1145        );
1146        assert_eq!(
1147            "http.fetch.v1".parse::<Capability>().unwrap(),
1148            Capability::HttpFetchV1
1149        );
1150        assert!("invalid".parse::<Capability>().is_err());
1151    }
1152
1153    #[test]
1154    fn test_intent_creation() {
1155        let intent = Intent::new(
1156            Capability::FsReadV1,
1157            "test".to_string(),
1158            json!({"path": "/etc/hostname"}),
1159            30000,
1160            "test-signer".to_string(),
1161        );
1162
1163        assert!(!intent.id.is_empty());
1164        assert_eq!(intent.capability, Capability::FsReadV1);
1165        assert_eq!(intent.domain, "test");
1166        assert_eq!(intent.ttl_ms, 30000);
1167        assert!(!intent.nonce.is_empty());
1168        assert_eq!(intent.signer, "test-signer");
1169    }
1170
1171    #[test]
1172    fn test_intent_subjects() {
1173        let intent = Intent::new(
1174            Capability::HttpFetchV1,
1175            "web".to_string(),
1176            json!({"url": "https://example.com"}),
1177            10000,
1178            "test-signer".to_string(),
1179        );
1180
1181        assert_eq!(intent.subject(), "smith.intents.http.fetch.v1.web");
1182        assert_eq!(
1183            intent.result_subject(),
1184            format!("smith.results.{}", intent.id)
1185        );
1186    }
1187
1188    #[test]
1189    fn test_intent_expiration() {
1190        let mut intent = Intent::new(
1191            Capability::FsReadV1,
1192            "test".to_string(),
1193            json!({"path": "/tmp/test"}),
1194            100, // 100ms TTL
1195            "test-signer".to_string(),
1196        );
1197
1198        // Should not be expired immediately
1199        assert!(!intent.is_expired());
1200
1201        // Simulate expired intent by setting old timestamp
1202        intent.created_at_ns = std::time::SystemTime::now()
1203            .duration_since(std::time::UNIX_EPOCH)
1204            .unwrap()
1205            .as_nanos()
1206            - 200_000_000; // 200ms ago
1207
1208        assert!(intent.is_expired());
1209    }
1210
1211    #[test]
1212    fn test_intent_canonical_json() {
1213        let intent = Intent::new(
1214            Capability::FsReadV1,
1215            "test".to_string(),
1216            json!({"path": "/etc/hostname", "max_bytes": 1024}),
1217            30000,
1218            "test-signer".to_string(),
1219        );
1220
1221        let canonical = intent.canonical_json().unwrap();
1222
1223        // Should be valid JSON
1224        let _: serde_json::Value = serde_json::from_str(&canonical).unwrap();
1225
1226        // Should not contain signature
1227        assert!(!canonical.contains("signature_b64"));
1228    }
1229
1230    #[test]
1231    fn test_intent_signature_verification() {
1232        use ed25519_dalek::SigningKey;
1233
1234        let signing_bytes = [42u8; 32];
1235        let signing_key = SigningKey::from_bytes(&signing_bytes);
1236        let verifying_key = signing_key.verifying_key();
1237
1238        let mut intent = Intent::new(
1239            Capability::FsReadV1,
1240            "test".to_string(),
1241            json!({"path": "/etc/hostname"}),
1242            30_000,
1243            BASE64.encode(verifying_key.to_bytes()),
1244        );
1245
1246        let canonical = intent.canonical_json().unwrap();
1247        let signature = signing_key.sign(canonical.as_bytes());
1248        intent.signature_b64 = BASE64.encode(signature.to_bytes());
1249
1250        assert!(intent.verify_signature().unwrap());
1251
1252        let mut invalid_signature = signature.to_bytes();
1253        invalid_signature[0] ^= 0xFF;
1254        intent.signature_b64 = BASE64.encode(invalid_signature);
1255        assert!(!intent.verify_signature().unwrap());
1256    }
1257
1258    #[test]
1259    fn test_intent_result_creation() {
1260        let intent_id = "test-intent-123".to_string();
1261        let audit_ref = "audit-ref-456".to_string();
1262        let runner_meta = RunnerMetadata {
1263            pid: 1234,
1264            cpu_ms: 500,
1265            max_rss_kb: 2048,
1266            capability_digest: Some("test-capability-digest".to_string()),
1267        };
1268
1269        // Test successful result
1270        let success_result = IntentResult::success(
1271            intent_id.clone(),
1272            json!({"content": "file contents"}),
1273            1640995200000000000,
1274            1640995201000000000,
1275            runner_meta.clone(),
1276            audit_ref.clone(),
1277        );
1278
1279        assert_eq!(success_result.intent_id, intent_id);
1280        assert!(matches!(success_result.status, ExecutionStatus::Ok));
1281        assert!(success_result.output.is_some());
1282        assert!(success_result.error.is_none());
1283
1284        // Test error result
1285        let error_result = IntentResult::error(
1286            intent_id.clone(),
1287            "FILE_NOT_FOUND".to_string(),
1288            "File does not exist".to_string(),
1289            1640995200000000000,
1290            1640995201000000000,
1291            runner_meta.clone(),
1292            audit_ref.clone(),
1293        );
1294
1295        assert_eq!(error_result.intent_id, intent_id);
1296        assert!(matches!(error_result.status, ExecutionStatus::Error));
1297        assert!(error_result.output.is_none());
1298        assert!(error_result.error.is_some());
1299        assert_eq!(error_result.error.as_ref().unwrap().code, "FILE_NOT_FOUND");
1300
1301        // Test denied result
1302        let denied_result = IntentResult::denied(
1303            intent_id.clone(),
1304            "Policy violation: unauthorized path".to_string(),
1305            audit_ref.clone(),
1306        );
1307
1308        assert_eq!(denied_result.intent_id, intent_id);
1309        assert!(matches!(denied_result.status, ExecutionStatus::Denied));
1310        assert!(denied_result.error.is_some());
1311        assert_eq!(denied_result.error.as_ref().unwrap().code, "POLICY_DENIED");
1312    }
1313
1314    #[test]
1315    fn test_fs_read_params() {
1316        let params = params::FsReadV1 {
1317            path: "/etc/hostname".to_string(),
1318            max_bytes: Some(1024),
1319            follow_symlinks: Some(false),
1320        };
1321
1322        let json = serde_json::to_value(&params).unwrap();
1323        let deserialized: params::FsReadV1 = serde_json::from_value(json).unwrap();
1324
1325        assert_eq!(deserialized.path, "/etc/hostname");
1326        assert_eq!(deserialized.max_bytes, Some(1024));
1327        assert_eq!(deserialized.follow_symlinks, Some(false));
1328    }
1329
1330    #[test]
1331    fn test_http_fetch_params() {
1332        let mut headers = HashMap::new();
1333        headers.insert("User-Agent".to_string(), "Smith/1.0".to_string());
1334
1335        let params = params::HttpFetchV1 {
1336            url: "https://api.example.com/data".to_string(),
1337            method: Some("POST".to_string()),
1338            headers: Some(headers.clone()),
1339            body: Some(r#"{"key":"value"}"#.to_string()),
1340            timeout_ms: Some(5000),
1341        };
1342
1343        let json = serde_json::to_value(&params).unwrap();
1344        let deserialized: params::HttpFetchV1 = serde_json::from_value(json).unwrap();
1345
1346        assert_eq!(deserialized.url, "https://api.example.com/data");
1347        assert_eq!(deserialized.method, Some("POST".to_string()));
1348        assert_eq!(deserialized.headers, Some(headers));
1349        assert_eq!(deserialized.timeout_ms, Some(5000));
1350    }
1351
1352    #[test]
1353    fn test_audit_entry_structure() {
1354        let policy_decision = PolicyDecision {
1355            rule_name: "path_allowlist".to_string(),
1356            decision: PolicyResult::Allow,
1357            reason: "Path is in allowed list".to_string(),
1358        };
1359
1360        let security_context = SecurityContext {
1361            sandbox_mode: Some(SandboxMode::Full),
1362            user_ns: Some(1001),
1363            mount_ns: Some(2002),
1364            pid_ns: Some(3003),
1365            net_ns: Some(4004),
1366            cgroup_path: Some("/sys/fs/cgroup/smith/executor-123".to_string()),
1367            landlock_enabled: true,
1368            seccomp_enabled: true,
1369            allowlist_hits: None,
1370        };
1371
1372        let audit_entry = AuditEntry {
1373            intent_id: "intent-123".to_string(),
1374            result_status: ExecutionStatus::Ok,
1375            timestamp_ns: 1640995200000000000,
1376            executor_id: "executor-001".to_string(),
1377            policy_decisions: vec![policy_decision],
1378            security_context,
1379        };
1380
1381        // Test serialization roundtrip
1382        let json = serde_json::to_string(&audit_entry).unwrap();
1383        let deserialized: AuditEntry = serde_json::from_str(&json).unwrap();
1384
1385        assert_eq!(deserialized.intent_id, "intent-123");
1386        assert_eq!(deserialized.executor_id, "executor-001");
1387        assert_eq!(deserialized.policy_decisions.len(), 1);
1388        assert!(deserialized.security_context.landlock_enabled);
1389    }
1390
1391    #[test]
1392    fn test_all_capability_variants() {
1393        // Test that all capability variants can be created and have proper string representations
1394        let capabilities = vec![
1395            (Capability::FsReadV1, "fs.read.v1"),
1396            (Capability::HttpFetchV1, "http.fetch.v1"),
1397            (Capability::FsWriteV1, "fs.write.v1"),
1398            (Capability::GitCloneV1, "git.clone.v1"),
1399            (Capability::ArchiveReadV1, "archive.read.v1"),
1400            (Capability::SqliteQueryV1, "sqlite.query.v1"),
1401            (Capability::BenchReportV1, "bench.report.v1"),
1402        ];
1403
1404        for (capability, expected_string) in capabilities {
1405            // Test Display trait
1406            assert_eq!(capability.to_string(), expected_string);
1407
1408            // Test FromStr trait
1409            let parsed: Capability = expected_string.parse().unwrap();
1410            assert_eq!(parsed, capability);
1411
1412            // Test serialization roundtrip
1413            let serialized = serde_json::to_string(&capability).unwrap();
1414            let deserialized: Capability = serde_json::from_str(&serialized).unwrap();
1415            assert_eq!(deserialized, capability);
1416        }
1417    }
1418
1419    #[test]
1420    fn test_capability_all_capabilities() {
1421        let all_caps = Capability::all_capabilities();
1422        assert_eq!(all_caps.len(), 8);
1423
1424        // Verify all expected capabilities are present
1425        assert!(all_caps.contains(&"fs.read.v1"));
1426        assert!(all_caps.contains(&"http.fetch.v1"));
1427        assert!(all_caps.contains(&"fs.write.v1"));
1428        assert!(all_caps.contains(&"git.clone.v1"));
1429        assert!(all_caps.contains(&"archive.read.v1"));
1430        assert!(all_caps.contains(&"sqlite.query.v1"));
1431        assert!(all_caps.contains(&"bench.report.v1"));
1432        assert!(all_caps.contains(&"shell.exec.v1"));
1433    }
1434
1435    #[test]
1436    fn test_capability_is_valid_capability() {
1437        // Test valid capabilities
1438        assert!(Capability::is_valid_capability("fs.read.v1"));
1439        assert!(Capability::is_valid_capability("http.fetch.v1"));
1440        assert!(Capability::is_valid_capability("sqlite.query.v1"));
1441
1442        // Test invalid capabilities
1443        assert!(!Capability::is_valid_capability("invalid.capability"));
1444        assert!(!Capability::is_valid_capability(""));
1445        assert!(!Capability::is_valid_capability("fs.read.v2")); // Wrong version
1446    }
1447
1448    #[test]
1449    fn test_execution_status_variants() {
1450        let statuses = vec![
1451            ExecutionStatus::Ok,
1452            ExecutionStatus::Error,
1453            ExecutionStatus::Denied,
1454            ExecutionStatus::Timeout,
1455            ExecutionStatus::Killed,
1456        ];
1457
1458        for status in statuses {
1459            // Test serialization roundtrip
1460            let serialized = serde_json::to_string(&status).unwrap();
1461            let deserialized: ExecutionStatus = serde_json::from_str(&serialized).unwrap();
1462            assert_eq!(deserialized, status);
1463
1464            // Test Debug trait
1465            let debug_str = format!("{:?}", status);
1466            assert!(!debug_str.is_empty());
1467        }
1468    }
1469
1470    #[test]
1471    fn test_execution_error_structure() {
1472        let error = ExecutionError {
1473            code: "FILE_NOT_FOUND".to_string(),
1474            message: "The specified file could not be found".to_string(),
1475        };
1476
1477        // Test serialization
1478        let json = serde_json::to_string(&error).unwrap();
1479        let deserialized: ExecutionError = serde_json::from_str(&json).unwrap();
1480
1481        assert_eq!(deserialized.code, "FILE_NOT_FOUND");
1482        assert_eq!(
1483            deserialized.message,
1484            "The specified file could not be found"
1485        );
1486
1487        // Test Clone trait
1488        let cloned = error.clone();
1489        assert_eq!(cloned.code, error.code);
1490        assert_eq!(cloned.message, error.message);
1491    }
1492
1493    #[test]
1494    fn test_runner_metadata() {
1495        let metadata = RunnerMetadata {
1496            pid: 12345,
1497            cpu_ms: 1500,
1498            max_rss_kb: 8192,
1499            capability_digest: Some("sha256:abcd1234".to_string()),
1500        };
1501
1502        // Test serialization roundtrip
1503        let json = serde_json::to_string(&metadata).unwrap();
1504        let deserialized: RunnerMetadata = serde_json::from_str(&json).unwrap();
1505
1506        assert_eq!(deserialized.pid, 12345);
1507        assert_eq!(deserialized.cpu_ms, 1500);
1508        assert_eq!(deserialized.max_rss_kb, 8192);
1509        assert_eq!(
1510            deserialized.capability_digest,
1511            Some("sha256:abcd1234".to_string())
1512        );
1513
1514        // Test empty metadata
1515        let empty = RunnerMetadata::empty();
1516        assert_eq!(empty.pid, 0);
1517        assert_eq!(empty.cpu_ms, 0);
1518        assert_eq!(empty.max_rss_kb, 0);
1519        assert_eq!(empty.capability_digest, None);
1520    }
1521
1522    #[test]
1523    fn test_sandbox_mode_variants() {
1524        let modes = vec![
1525            (SandboxMode::Full, "full"),
1526            (SandboxMode::Demo, "demo"),
1527            (SandboxMode::Unsafe, "unsafe"),
1528        ];
1529
1530        for (mode, expected_string) in modes {
1531            // Test serialization
1532            let serialized = serde_json::to_string(&mode).unwrap();
1533            assert_eq!(serialized, format!("\"{}\"", expected_string));
1534
1535            // Test deserialization
1536            let deserialized: SandboxMode = serde_json::from_str(&serialized).unwrap();
1537            assert_eq!(deserialized, mode);
1538        }
1539    }
1540
1541    #[test]
1542    fn test_policy_result_variants() {
1543        let results = vec![
1544            (PolicyResult::Allow, "allow"),
1545            (PolicyResult::Deny, "deny"),
1546            (PolicyResult::Transform, "transform"),
1547        ];
1548
1549        for (result, expected_string) in results {
1550            // Test serialization
1551            let serialized = serde_json::to_string(&result).unwrap();
1552            assert_eq!(serialized, format!("\"{}\"", expected_string));
1553
1554            // Test deserialization
1555            let deserialized: PolicyResult = serde_json::from_str(&serialized).unwrap();
1556            assert_eq!(deserialized, result);
1557        }
1558    }
1559
1560    #[test]
1561    fn test_policy_decision_structure() {
1562        let decision = PolicyDecision {
1563            rule_name: "file_access_check".to_string(),
1564            decision: PolicyResult::Allow,
1565            reason: "File is within allowed directory".to_string(),
1566        };
1567
1568        // Test serialization roundtrip
1569        let json = serde_json::to_string(&decision).unwrap();
1570        let deserialized: PolicyDecision = serde_json::from_str(&json).unwrap();
1571
1572        assert_eq!(deserialized.rule_name, "file_access_check");
1573        assert_eq!(deserialized.decision, PolicyResult::Allow);
1574        assert_eq!(deserialized.reason, "File is within allowed directory");
1575    }
1576
1577    #[test]
1578    fn test_security_context_comprehensive() {
1579        // Test full security context
1580        let full_context = SecurityContext {
1581            sandbox_mode: Some(SandboxMode::Full),
1582            user_ns: Some(1001),
1583            mount_ns: Some(2002),
1584            pid_ns: Some(3003),
1585            net_ns: Some(4004),
1586            cgroup_path: Some("/sys/fs/cgroup/smith/test-123".to_string()),
1587            landlock_enabled: true,
1588            seccomp_enabled: true,
1589            allowlist_hits: Some(vec![AllowlistHit {
1590                resource_type: "file".to_string(),
1591                resource_id: "/etc/hostname".to_string(),
1592                operation: "read".to_string(),
1593                timestamp_ns: 1640995200000000000,
1594            }]),
1595        };
1596
1597        // Test serialization roundtrip
1598        let json = serde_json::to_string(&full_context).unwrap();
1599        let deserialized: SecurityContext = serde_json::from_str(&json).unwrap();
1600
1601        assert_eq!(deserialized.sandbox_mode, Some(SandboxMode::Full));
1602        assert_eq!(deserialized.user_ns, Some(1001));
1603        assert!(deserialized.landlock_enabled);
1604        assert_eq!(deserialized.allowlist_hits.as_ref().unwrap().len(), 1);
1605
1606        // Test minimal security context
1607        let minimal_context = SecurityContext {
1608            sandbox_mode: None,
1609            user_ns: None,
1610            mount_ns: None,
1611            pid_ns: None,
1612            net_ns: None,
1613            cgroup_path: None,
1614            landlock_enabled: false,
1615            seccomp_enabled: false,
1616            allowlist_hits: None,
1617        };
1618
1619        let minimal_json = serde_json::to_string(&minimal_context).unwrap();
1620        let minimal_deserialized: SecurityContext = serde_json::from_str(&minimal_json).unwrap();
1621
1622        assert_eq!(minimal_deserialized.sandbox_mode, None);
1623        assert!(!minimal_deserialized.landlock_enabled);
1624        assert_eq!(minimal_deserialized.allowlist_hits, None);
1625    }
1626
1627    #[test]
1628    fn test_intent_result_timeout_and_killed() {
1629        let intent_id = "timeout-test-123".to_string();
1630        let audit_ref = "audit-timeout-456".to_string();
1631        let runner_meta = RunnerMetadata::empty();
1632
1633        // Test timeout result creation (utility function)
1634        let start_time = 1640995200000000000;
1635        let end_time = 1640995230000000000; // 30 seconds later
1636
1637        let timeout_result = IntentResult::create_result(
1638            intent_id.clone(),
1639            ExecutionStatus::Timeout,
1640            None,
1641            Some(ExecutionError {
1642                code: "EXECUTION_TIMEOUT".to_string(),
1643                message: "Execution exceeded maximum allowed time".to_string(),
1644            }),
1645            start_time,
1646            end_time,
1647            runner_meta.clone(),
1648            audit_ref.clone(),
1649        );
1650
1651        assert_eq!(timeout_result.status, ExecutionStatus::Timeout);
1652        assert!(timeout_result.error.is_some());
1653        assert_eq!(
1654            timeout_result.error.as_ref().unwrap().code,
1655            "EXECUTION_TIMEOUT"
1656        );
1657
1658        // Test killed result creation (utility function)
1659        let killed_result = IntentResult::create_result(
1660            intent_id.clone(),
1661            ExecutionStatus::Killed,
1662            None,
1663            Some(ExecutionError {
1664                code: "PROCESS_KILLED".to_string(),
1665                message: "Process was terminated by signal".to_string(),
1666            }),
1667            start_time,
1668            end_time,
1669            runner_meta.clone(),
1670            audit_ref.clone(),
1671        );
1672
1673        assert_eq!(killed_result.status, ExecutionStatus::Killed);
1674        assert!(killed_result.error.is_some());
1675        assert_eq!(killed_result.error.as_ref().unwrap().code, "PROCESS_KILLED");
1676    }
1677}