1use 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
83pub 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")]
105pub enum Command {
107 Handshake {
109 version: u32,
110 capabilities: Vec<String>,
111 },
112 Plan {
114 #[cfg_attr(feature = "typescript", ts(type = "string"))]
115 request_id: Uuid,
116 goal: String,
117 context: HashMap<String, String>,
118 },
119 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 HookLoad {
130 #[cfg_attr(feature = "typescript", ts(type = "string"))]
131 request_id: Uuid,
132 hook_type: String,
133 script: String,
134 },
135 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 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")]
156pub enum Event {
158 Ready {
160 version: u32,
161 capabilities: Vec<String>,
162 #[cfg_attr(feature = "typescript", ts(type = "string"))]
163 service_id: Uuid,
164 },
165 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 {
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 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 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 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 HookResult {
211 #[cfg_attr(feature = "typescript", ts(type = "string"))]
212 request_id: Uuid,
213 action: HookAction,
214 },
215 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 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)]
243pub 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)]
258pub 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")]
274pub 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)]
287pub 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)]
302pub 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
314pub 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#[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")]
334pub 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 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 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), _ => Err(anyhow::anyhow!("Unknown capability: {}", s)),
394 }
395 }
396
397 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 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)]
424pub struct Intent {
426 pub id: String,
428 pub capability: Capability,
430 pub domain: String,
432 #[cfg_attr(feature = "typescript", ts(type = "unknown"))]
434 pub params: serde_json::Value,
435 #[cfg_attr(feature = "typescript", ts(type = "bigint"))]
437 pub created_at_ns: u128,
438 pub ttl_ms: u32,
440 pub nonce: String,
442 pub signer: String,
444 pub signature_b64: String,
446 #[cfg_attr(feature = "typescript", ts(type = "Record<string, unknown>"))]
448 pub metadata: std::collections::HashMap<String, serde_json::Value>,
449}
450
451impl Intent {
452 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 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)]
501pub struct IntentResult {
503 pub intent_id: String,
505 pub status: ExecutionStatus,
507 #[cfg_attr(feature = "typescript", ts(type = "unknown | undefined"))]
509 pub output: Option<serde_json::Value>,
510 pub error: Option<ExecutionError>,
512 #[cfg_attr(feature = "typescript", ts(type = "bigint"))]
514 pub started_at_ns: u128,
515 #[cfg_attr(feature = "typescript", ts(type = "bigint"))]
517 pub finished_at_ns: u128,
518 pub runner_meta: RunnerMetadata,
520 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)]
531pub struct AuditRef {
533 pub id: String,
535 #[cfg_attr(feature = "typescript", ts(type = "bigint"))]
537 pub timestamp: u64,
538 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")]
550pub enum ExecutionStatus {
552 Ok,
553 Error,
554 Denied,
555 Timeout,
556 Killed,
557 Success,
559 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)]
570pub struct ExecutionError {
572 pub code: String,
574 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)]
585pub struct RunnerMetadata {
587 pub pid: u32,
589 pub cpu_ms: u32,
591 pub max_rss_kb: u32,
593 pub capability_digest: Option<String>,
595}
596
597impl RunnerMetadata {
598 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)]
615pub struct AuditEntry {
617 pub intent_id: String,
619 pub result_status: ExecutionStatus,
621 #[cfg_attr(feature = "typescript", ts(type = "bigint"))]
623 pub timestamp_ns: u128,
624 pub executor_id: String,
626 pub policy_decisions: Vec<PolicyDecision>,
628 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)]
638pub struct PolicyDecision {
640 pub rule_name: String,
642 pub decision: PolicyResult,
644 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")]
655pub 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)]
668pub struct SecurityContext {
670 pub sandbox_mode: Option<SandboxMode>,
672 pub user_ns: Option<u32>,
674 pub mount_ns: Option<u32>,
676 pub pid_ns: Option<u32>,
678 pub net_ns: Option<u32>,
680 pub cgroup_path: Option<String>,
682 pub landlock_enabled: bool,
684 pub seccomp_enabled: bool,
686 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")]
697pub enum SandboxMode {
699 Full,
701 Demo,
703 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)]
713pub struct AllowlistHit {
715 pub resource_type: String,
717 pub resource_id: String,
719 pub operation: String,
721 #[cfg_attr(feature = "typescript", ts(type = "bigint"))]
723 pub timestamp_ns: u128,
724}
725
726#[derive(Debug, Clone, Serialize, Deserialize)]
728pub struct ResourceUsage {
729 pub peak_memory_kb: u32,
731 pub cpu_time_ms: u32,
733 pub wall_time_ms: u32,
735 pub fd_count: u32,
737 pub disk_read_bytes: u64,
739 pub disk_write_bytes: u64,
741 pub network_tx_bytes: u64,
743 pub network_rx_bytes: u64,
745}
746
747#[derive(Debug, Clone, Serialize, Deserialize)]
749#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
750pub struct CapabilitySpec {
751 pub name: String,
753 pub description: String,
755 pub params_schema: serde_json::Value,
757 pub example_params: serde_json::Value,
759 pub resource_requirements: ResourceRequirements,
761 pub security_notes: Vec<String>,
763}
764
765#[derive(Debug, Clone, Serialize, Deserialize)]
767#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
768pub struct ResourceRequirements {
769 pub cpu_ms_typical: u32,
771 pub memory_kb_max: u32,
773 pub network_access: bool,
775 pub filesystem_access: bool,
777 pub external_commands: bool,
779}
780
781#[derive(Debug, Clone, Serialize, Deserialize)]
786#[cfg_attr(feature = "typescript", derive(TS))]
787#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
788pub struct ExecutionLimits {
789 pub cpu_ms_per_100ms: u32,
791 pub mem_bytes: u64,
793 pub io_bytes: u64,
795 pub pids_max: u32,
797 pub timeout_ms: u64,
799}
800
801impl Default for ExecutionLimits {
802 fn default() -> Self {
803 Self {
804 cpu_ms_per_100ms: 50, mem_bytes: 128 * 1024 * 1024, io_bytes: 10 * 1024 * 1024, pids_max: 10,
808 timeout_ms: 30000, }
810 }
811}
812
813#[derive(Debug, Clone, Serialize, Deserialize)]
815pub struct AuditEvent {
816 pub intent_id: String,
818 pub result_status: ExecutionStatus,
820 pub timestamp_ns: u128,
822 pub executor_id: String,
824 pub policy_decisions: Vec<PolicyDecision>,
826 pub security_context: SecurityContext,
828 pub resource_usage: Option<ResourceUsage>,
830}
831
832pub mod params {
834 use super::*;
835
836 #[derive(Debug, Clone, Serialize, Deserialize)]
838 #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
839 pub struct FsReadV1 {
840 pub path: String,
842 pub max_bytes: Option<u64>,
844 pub follow_symlinks: Option<bool>,
846 }
847
848 #[derive(Debug, Clone, Serialize, Deserialize)]
850 #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
851 pub struct HttpFetchV1 {
852 pub url: String,
854 pub method: Option<String>,
856 pub headers: Option<HashMap<String, String>>,
858 pub body: Option<String>,
860 pub timeout_ms: Option<u32>,
862 }
863
864 #[derive(Debug, Clone, Serialize, Deserialize)]
866 #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
867 pub struct ArchiveReadV1 {
868 pub path: String,
870 pub extract_content: Option<bool>,
872 }
873
874 #[derive(Debug, Clone, Serialize, Deserialize)]
876 #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
877 pub struct SqliteQueryV1 {
878 pub database_path: String,
880 pub query: String,
882 pub params: Option<Vec<serde_json::Value>>,
884 pub max_rows: Option<u32>,
886 pub timeout_ms: Option<u32>,
888 }
889
890 #[derive(Debug, Clone, Serialize, Deserialize)]
892 #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
893 pub struct BenchReportV1 {
894 pub benchmark_name: String,
896 pub metrics: HashMap<String, f64>,
898 pub metadata: Option<HashMap<String, serde_json::Value>>,
900 pub retention_days: Option<u32>,
902 }
903
904 #[derive(Debug, Clone, Serialize, Deserialize)]
906 #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
907 pub struct ShellExecV1 {
908 pub command: String,
910 pub args: Option<Vec<String>>,
912 pub env: Option<HashMap<String, String>>,
914 pub cwd: Option<String>,
916 pub timeout_ms: Option<u32>,
918 pub stdin: Option<String>,
920 }
921}
922
923impl Intent {
925 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(), metadata: std::collections::HashMap::new(),
948 }
949 }
950
951 fn generate_intent_id() -> String {
953 uuid::Uuid::now_v7().to_string()
954 }
955
956 fn generate_nonce() -> String {
958 uuid::Uuid::new_v4().to_string()
959 }
960
961 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 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 fn create_unsigned_copy(&self) -> Self {
978 let mut canonical = self.clone();
979 canonical.signature_b64 = String::new();
980 canonical
981 }
982
983 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 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 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 fn ttl_as_nanoseconds(&self) -> u128 {
1008 (self.ttl_ms as u128) * 1_000_000
1009 }
1010
1011 pub fn subject(&self) -> String {
1013 smith_bus::builders::IntentSubject::with_domain(&self.capability.to_string(), &self.domain)
1014 }
1015
1016 pub fn result_subject(&self) -> String {
1018 smith_bus::builders::ResultSubject::for_intent(&self.id)
1019 }
1020}
1021
1022impl IntentResult {
1023 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 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 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 #[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, "test-signer".to_string(),
1196 );
1197
1198 assert!(!intent.is_expired());
1200
1201 intent.created_at_ns = std::time::SystemTime::now()
1203 .duration_since(std::time::UNIX_EPOCH)
1204 .unwrap()
1205 .as_nanos()
1206 - 200_000_000; 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 let _: serde_json::Value = serde_json::from_str(&canonical).unwrap();
1225
1226 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 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 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 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(¶ms).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(¶ms).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 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 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 assert_eq!(capability.to_string(), expected_string);
1407
1408 let parsed: Capability = expected_string.parse().unwrap();
1410 assert_eq!(parsed, capability);
1411
1412 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 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 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 assert!(!Capability::is_valid_capability("invalid.capability"));
1444 assert!(!Capability::is_valid_capability(""));
1445 assert!(!Capability::is_valid_capability("fs.read.v2")); }
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 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 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 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 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 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 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 let serialized = serde_json::to_string(&mode).unwrap();
1533 assert_eq!(serialized, format!("\"{}\"", expected_string));
1534
1535 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 let serialized = serde_json::to_string(&result).unwrap();
1552 assert_eq!(serialized, format!("\"{}\"", expected_string));
1553
1554 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 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 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 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 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 let start_time = 1640995200000000000;
1635 let end_time = 1640995230000000000; 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 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}