1use serde::{Deserialize, Serialize};
4
5use crate::policy::{Capabilities, SandboxPolicy};
6
7#[non_exhaustive]
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ExecuteRequest {
14 pub tool: String,
16 #[serde(default)]
18 pub params: serde_json::Value,
19 #[serde(default)]
22 pub capabilities: Option<Capabilities>,
23}
24
25impl ExecuteRequest {
26 pub fn new(tool: impl Into<String>, params: serde_json::Value) -> Self {
28 Self {
29 tool: tool.into(),
30 params,
31 capabilities: None,
32 }
33 }
34
35 pub fn with_capabilities(mut self, capabilities: Capabilities) -> Self {
37 self.capabilities = Some(capabilities);
38 self
39 }
40}
41
42#[non_exhaustive]
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct ExecuteResponse {
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub request_id: Option<String>,
52 pub status: ExecutionStatus,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub output: Option<serde_json::Value>,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub error: Option<String>,
60 pub metadata: ExecutionMetadata,
62}
63
64impl ExecuteResponse {
65 pub fn ok(output: serde_json::Value, metadata: ExecutionMetadata) -> Self {
67 Self {
68 request_id: None,
69 status: ExecutionStatus::Ok,
70 output: Some(output),
71 error: None,
72 metadata,
73 }
74 }
75
76 pub fn error(error: impl Into<String>, metadata: ExecutionMetadata) -> Self {
78 Self {
79 request_id: None,
80 status: ExecutionStatus::Error,
81 output: None,
82 error: Some(error.into()),
83 metadata,
84 }
85 }
86
87 pub fn with_request_id(mut self, id: impl Into<String>) -> Self {
89 self.request_id = Some(id.into());
90 self
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
96#[serde(rename_all = "snake_case")]
97#[non_exhaustive]
98pub enum ExecutionStatus {
99 Ok,
101 Error,
103 Timeout,
105 Blocked,
107}
108
109#[non_exhaustive]
111#[derive(Debug, Clone, Serialize, Deserialize, Default)]
112pub struct ExecutionMetadata {
113 pub execution_time_ms: u64,
115 pub fuel_consumed: u64,
117 #[serde(default)]
119 pub logs: Vec<serde_json::Value>,
120 pub sanitization: SanitizationReport,
122}
123
124impl ExecutionMetadata {
125 pub fn new(execution_time_ms: u64, fuel_consumed: u64) -> Self {
127 Self {
128 execution_time_ms,
129 fuel_consumed,
130 logs: Vec::new(),
131 sanitization: SanitizationReport::default(),
132 }
133 }
134}
135
136#[must_use]
141#[non_exhaustive]
142#[derive(Debug, Clone, Serialize, Deserialize, Default)]
143pub struct SanitizationReport {
144 pub issues_found: u32,
146 pub actions_taken: Vec<String>,
148}
149
150impl SanitizationReport {
151 pub fn new(issues_found: u32, actions_taken: Vec<String>) -> Self {
153 Self {
154 issues_found,
155 actions_taken,
156 }
157 }
158}
159
160#[non_exhaustive]
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct ContainerSpawnRequest {
164 pub task: String,
166 #[serde(default)]
168 pub image: Option<String>,
169 #[serde(default)]
171 pub policy: SandboxPolicy,
172 #[serde(default)]
174 pub capabilities: Capabilities,
175 #[serde(default)]
177 pub env: std::collections::HashMap<String, String>,
178 #[serde(default)]
180 pub command: Option<Vec<String>>,
181}
182
183impl ContainerSpawnRequest {
184 pub fn new(task: impl Into<String>, capabilities: Capabilities) -> Self {
186 Self {
187 task: task.into(),
188 image: None,
189 policy: SandboxPolicy::default(),
190 capabilities,
191 env: std::collections::HashMap::new(),
192 command: None,
193 }
194 }
195
196 pub fn with_policy(mut self, policy: SandboxPolicy) -> Self {
198 self.policy = policy;
199 self
200 }
201
202 pub fn with_image(mut self, image: impl Into<String>) -> Self {
204 self.image = Some(image.into());
205 self
206 }
207
208 pub fn with_command(mut self, cmd: Vec<String>) -> Self {
210 self.command = Some(cmd);
211 self
212 }
213
214 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
216 self.env.insert(key.into(), value.into());
217 self
218 }
219}
220
221#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
223#[serde(rename_all = "snake_case")]
224#[non_exhaustive]
225pub enum ContainerStatus {
226 Creating,
228 Running,
230 Completed,
232 Failed,
234 TimedOut,
236 Killed,
238}
239
240#[non_exhaustive]
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct ContainerInfo {
244 pub container_id: String,
246 pub status: ContainerStatus,
248 pub policy: SandboxPolicy,
250 pub created_at: chrono::DateTime<chrono::Utc>,
252 pub task_summary: String,
254 pub proxy_socket: String,
256 pub resource_usage: Option<ResourceUsage>,
258}
259
260impl ContainerInfo {
261 pub fn new(
263 container_id: impl Into<String>,
264 status: ContainerStatus,
265 policy: SandboxPolicy,
266 task_summary: impl Into<String>,
267 proxy_socket: impl Into<String>,
268 ) -> Self {
269 Self {
270 container_id: container_id.into(),
271 status,
272 policy,
273 created_at: chrono::Utc::now(),
274 task_summary: task_summary.into(),
275 proxy_socket: proxy_socket.into(),
276 resource_usage: None,
277 }
278 }
279}
280
281#[non_exhaustive]
283#[derive(Debug, Clone, Serialize, Deserialize, Default)]
284pub struct ResourceUsage {
285 pub memory_bytes: u64,
287 pub cpu_percent: f64,
289 pub network_requests: u32,
291 pub duration_ms: u64,
293}
294
295impl ResourceUsage {
296 pub fn new(
298 memory_bytes: u64,
299 cpu_percent: f64,
300 network_requests: u32,
301 duration_ms: u64,
302 ) -> Self {
303 Self {
304 memory_bytes,
305 cpu_percent,
306 network_requests,
307 duration_ms,
308 }
309 }
310}
311
312#[non_exhaustive]
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct HealthResponse {
316 pub status: String,
318 pub version: String,
320 pub docker_available: bool,
322 pub wasm_engine_ready: bool,
324 pub active_containers: usize,
326 pub uptime_seconds: u64,
328 #[serde(skip_serializing_if = "Option::is_none", default)]
330 pub components: Option<HealthComponents>,
331}
332
333impl HealthResponse {
334 pub fn healthy(version: impl Into<String>, uptime_seconds: u64) -> Self {
336 Self {
337 status: "healthy".into(),
338 version: version.into(),
339 docker_available: true,
340 wasm_engine_ready: true,
341 active_containers: 0,
342 uptime_seconds,
343 components: None,
344 }
345 }
346}
347
348#[non_exhaustive]
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct HealthComponents {
352 pub wasm_engine: ComponentHealth,
354 pub docker: ComponentHealth,
356 pub agents: ComponentHealth,
358}
359
360impl HealthComponents {
361 pub fn new(
363 wasm_engine: ComponentHealth,
364 docker: ComponentHealth,
365 agents: ComponentHealth,
366 ) -> Self {
367 Self {
368 wasm_engine,
369 docker,
370 agents,
371 }
372 }
373}
374
375#[non_exhaustive]
377#[derive(Debug, Clone, Serialize, Deserialize)]
378pub struct ComponentHealth {
379 pub status: String,
381 #[serde(skip_serializing_if = "Option::is_none")]
383 pub detail: Option<serde_json::Value>,
384}
385
386impl ComponentHealth {
387 pub fn ok() -> Self {
389 Self {
390 status: "ok".into(),
391 detail: None,
392 }
393 }
394
395 pub fn error(detail: impl Into<String>) -> Self {
397 Self {
398 status: "error".into(),
399 detail: Some(serde_json::Value::String(detail.into())),
400 }
401 }
402}
403
404#[non_exhaustive]
406#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct ApiError {
408 pub error: String,
410 pub code: String,
412 #[serde(skip_serializing_if = "Option::is_none")]
414 pub details: Option<serde_json::Value>,
415}
416
417impl ApiError {
418 pub fn new(error: impl Into<String>, code: impl Into<String>) -> Self {
420 Self {
421 error: error.into(),
422 code: code.into(),
423 details: None,
424 }
425 }
426
427 pub fn with_details(mut self, details: serde_json::Value) -> Self {
429 self.details = Some(details);
430 self
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437
438 #[test]
439 fn test_execute_response_serde_roundtrip() {
440 let resp = ExecuteResponse::ok(
441 serde_json::json!({"result": 42}),
442 ExecutionMetadata::new(150, 10000),
443 )
444 .with_request_id("req-123");
445
446 let json = serde_json::to_string(&resp).unwrap();
447 let deser: ExecuteResponse = serde_json::from_str(&json).unwrap();
448 assert_eq!(deser.status, ExecutionStatus::Ok);
449 assert_eq!(deser.request_id.as_deref(), Some("req-123"));
450 assert_eq!(deser.metadata.fuel_consumed, 10000);
451 }
452
453 #[test]
454 fn test_execute_request_builder() {
455 let req = ExecuteRequest::new("my_tool", serde_json::json!({"key": "value"}))
456 .with_capabilities(Capabilities::default());
457 assert_eq!(req.tool, "my_tool");
458 assert!(req.capabilities.is_some());
459 }
460
461 #[test]
462 fn test_execute_response_error() {
463 let meta = ExecutionMetadata::new(100, 5000);
464 let resp = ExecuteResponse::error("something broke", meta);
465 assert_eq!(resp.status, ExecutionStatus::Error);
466 assert_eq!(resp.error.as_deref(), Some("something broke"));
467 assert!(resp.output.is_none());
468 }
469
470 #[test]
471 fn test_execution_metadata_default() {
472 let meta = ExecutionMetadata::default();
473 assert_eq!(meta.execution_time_ms, 0);
474 assert_eq!(meta.fuel_consumed, 0);
475 assert!(meta.logs.is_empty());
476 }
477
478 #[test]
479 fn test_sanitization_report_new() {
480 let report = SanitizationReport::new(3, vec!["redacted".into()]);
481 assert_eq!(report.issues_found, 3);
482 assert_eq!(report.actions_taken.len(), 1);
483 }
484
485 #[test]
486 fn test_sanitization_report_default() {
487 let report = SanitizationReport::default();
488 assert_eq!(report.issues_found, 0);
489 assert!(report.actions_taken.is_empty());
490 }
491
492 #[test]
493 fn test_container_spawn_request_builder() {
494 let req = ContainerSpawnRequest::new("my task", Capabilities::default())
495 .with_policy(SandboxPolicy::Container)
496 .with_image("alpine:latest")
497 .with_env("FOO", "bar");
498 assert_eq!(req.task, "my task");
499 assert_eq!(req.policy, SandboxPolicy::Container);
500 assert_eq!(req.image.as_deref(), Some("alpine:latest"));
501 assert_eq!(req.env.get("FOO").unwrap(), "bar");
502 }
503
504 #[test]
505 fn test_container_info_new() {
506 let info = ContainerInfo::new(
507 "clawbox-123",
508 ContainerStatus::Running,
509 SandboxPolicy::Container,
510 "test task",
511 "/run/clawbox/proxy.sock",
512 );
513 assert_eq!(info.container_id, "clawbox-123");
514 assert_eq!(info.status, ContainerStatus::Running);
515 assert_eq!(info.proxy_socket, "/run/clawbox/proxy.sock");
516 assert!(info.resource_usage.is_none());
517 }
518
519 #[test]
520 fn test_resource_usage_new() {
521 let usage = ResourceUsage::new(1024, 50.0, 10, 5000);
522 assert_eq!(usage.memory_bytes, 1024);
523 assert_eq!(usage.network_requests, 10);
524 }
525
526 #[test]
527 fn test_resource_usage_default() {
528 let usage = ResourceUsage::default();
529 assert_eq!(usage.memory_bytes, 0);
530 }
531
532 #[test]
533 fn test_health_response_healthy() {
534 let health = HealthResponse::healthy("1.0.0", 3600);
535 assert_eq!(health.status, "healthy");
536 assert!(health.docker_available);
537 }
538
539 #[test]
540 fn test_component_health_ok_and_error() {
541 let ok = ComponentHealth::ok();
542 assert_eq!(ok.status, "ok");
543 let err = ComponentHealth::error("bad");
544 assert_eq!(err.status, "error");
545 assert!(err.detail.is_some());
546 }
547
548 #[test]
549 fn test_api_error_with_details() {
550 let err =
551 ApiError::new("not found", "not_found").with_details(serde_json::json!({"id": "abc"}));
552 assert_eq!(err.code, "not_found");
553 assert!(err.details.is_some());
554 }
555}