Skip to main content

clawbox_types/
api.rs

1//! API request and response types for the clawbox HTTP interface.
2
3use serde::{Deserialize, Serialize};
4
5use crate::policy::{Capabilities, SandboxPolicy};
6
7/// Request to execute a single tool call in a WASM sandbox.
8///
9/// At minimum, specify the `tool` name and `params`. Optionally override
10/// the caller's default capabilities for this invocation.
11#[non_exhaustive]
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ExecuteRequest {
14    /// Tool name to invoke (must match a registered manifest).
15    pub tool: String,
16    /// JSON parameters passed to the tool's entry point.
17    #[serde(default)]
18    pub params: serde_json::Value,
19    /// Optional capability overrides for this execution.
20    /// When `None`, the server applies the tool's default capabilities.
21    #[serde(default)]
22    pub capabilities: Option<Capabilities>,
23}
24
25impl ExecuteRequest {
26    /// Create a new execution request for the given tool with the supplied parameters.
27    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    /// Override the default capabilities for this execution.
36    pub fn with_capabilities(mut self, capabilities: Capabilities) -> Self {
37        self.capabilities = Some(capabilities);
38        self
39    }
40}
41
42/// Response from a tool execution.
43///
44/// Contains the execution result, any errors, and metadata about
45/// resource consumption and sanitization.
46#[non_exhaustive]
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct ExecuteResponse {
49    /// Caller-assigned request ID for correlation. Echoed back if provided.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub request_id: Option<String>,
52    /// Overall execution outcome.
53    pub status: ExecutionStatus,
54    /// Tool output on success. `None` when `status` is not `Ok`.
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub output: Option<serde_json::Value>,
57    /// Human-readable error message on failure.
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub error: Option<String>,
60    /// Execution telemetry (timing, fuel, logs, sanitization).
61    pub metadata: ExecutionMetadata,
62}
63
64impl ExecuteResponse {
65    /// Create a successful response with the given output and metadata.
66    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    /// Create an error response with the given message and metadata.
77    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    /// Set the request ID for correlation.
88    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/// Outcome status of a tool execution.
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
96#[serde(rename_all = "snake_case")]
97#[non_exhaustive]
98pub enum ExecutionStatus {
99    /// Tool completed successfully.
100    Ok,
101    /// Tool returned an error.
102    Error,
103    /// Execution exceeded its time limit.
104    Timeout,
105    /// Execution was blocked by policy (e.g., disallowed network access).
106    Blocked,
107}
108
109/// Telemetry collected during a tool execution.
110#[non_exhaustive]
111#[derive(Debug, Clone, Serialize, Deserialize, Default)]
112pub struct ExecutionMetadata {
113    /// Wall-clock execution time in milliseconds.
114    pub execution_time_ms: u64,
115    /// WASM fuel (instruction budget) consumed. 0 for container-direct executions.
116    pub fuel_consumed: u64,
117    /// Structured log entries emitted by the tool during execution.
118    #[serde(default)]
119    pub logs: Vec<serde_json::Value>,
120    /// Output sanitization report (credential scrubbing, etc.).
121    pub sanitization: SanitizationReport,
122}
123
124impl ExecutionMetadata {
125    /// Create metadata with the given execution time and fuel consumption.
126    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/// Report of output sanitization actions taken after execution.
137///
138/// The clawbox proxy scans tool output for leaked credentials and other
139/// sensitive patterns, redacting them before returning results.
140#[must_use]
141#[non_exhaustive]
142#[derive(Debug, Clone, Serialize, Deserialize, Default)]
143pub struct SanitizationReport {
144    /// Number of sensitive patterns detected in the output.
145    pub issues_found: u32,
146    /// Human-readable descriptions of sanitization actions taken.
147    pub actions_taken: Vec<String>,
148}
149
150impl SanitizationReport {
151    /// Create a report with the given number of issues and actions.
152    pub fn new(issues_found: u32, actions_taken: Vec<String>) -> Self {
153        Self {
154            issues_found,
155            actions_taken,
156        }
157    }
158}
159
160/// Request to spawn a new agent container.
161#[non_exhaustive]
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct ContainerSpawnRequest {
164    /// Human-readable task description for this container.
165    pub task: String,
166    /// Docker image override. When `None`, uses the server default.
167    #[serde(default)]
168    pub image: Option<String>,
169    /// Sandbox policy for the container. Defaults to `WasmOnly`.
170    #[serde(default)]
171    pub policy: SandboxPolicy,
172    /// Capability requirements for the container.
173    #[serde(default)]
174    pub capabilities: Capabilities,
175    /// Extra environment variables injected into the container.
176    #[serde(default)]
177    pub env: std::collections::HashMap<String, String>,
178    /// Optional command to run in the container (overrides image CMD).
179    #[serde(default)]
180    pub command: Option<Vec<String>>,
181}
182
183impl ContainerSpawnRequest {
184    /// Create a container spawn request for the given task with specified capabilities.
185    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    /// Override the sandbox policy.
197    pub fn with_policy(mut self, policy: SandboxPolicy) -> Self {
198        self.policy = policy;
199        self
200    }
201
202    /// Set the Docker image.
203    pub fn with_image(mut self, image: impl Into<String>) -> Self {
204        self.image = Some(image.into());
205        self
206    }
207
208    /// Set the container command.
209    pub fn with_command(mut self, cmd: Vec<String>) -> Self {
210        self.command = Some(cmd);
211        self
212    }
213
214    /// Add an environment variable.
215    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/// Lifecycle status of a container.
222#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
223#[serde(rename_all = "snake_case")]
224#[non_exhaustive]
225pub enum ContainerStatus {
226    /// Container is being created.
227    Creating,
228    /// Container is running and accepting requests.
229    Running,
230    /// Container finished its task normally.
231    Completed,
232    /// Container exited with an error.
233    Failed,
234    /// Container exceeded its time limit.
235    TimedOut,
236    /// Container was explicitly killed.
237    Killed,
238}
239
240/// Runtime information about an active or recently-active container.
241#[non_exhaustive]
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct ContainerInfo {
244    /// Unique container identifier.
245    pub container_id: String,
246    /// Current lifecycle status.
247    pub status: ContainerStatus,
248    /// Sandbox policy in effect.
249    pub policy: SandboxPolicy,
250    /// When this container was created.
251    pub created_at: chrono::DateTime<chrono::Utc>,
252    /// Brief description of the container's task.
253    pub task_summary: String,
254    /// Unix socket path for the container proxy (in-container path).
255    pub proxy_socket: String,
256    /// Current resource usage snapshot. `None` if not yet available.
257    pub resource_usage: Option<ResourceUsage>,
258}
259
260impl ContainerInfo {
261    /// Create container info with the minimum required fields.
262    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/// Snapshot of a container's resource consumption.
282#[non_exhaustive]
283#[derive(Debug, Clone, Serialize, Deserialize, Default)]
284pub struct ResourceUsage {
285    /// Current memory usage in bytes.
286    pub memory_bytes: u64,
287    /// CPU utilization as a percentage (0.0–100.0).
288    pub cpu_percent: f64,
289    /// Total outbound network requests made.
290    pub network_requests: u32,
291    /// Wall-clock time the container has been running, in milliseconds.
292    pub duration_ms: u64,
293}
294
295impl ResourceUsage {
296    /// Create a new resource usage snapshot.
297    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/// Health check response from the clawbox server.
313#[non_exhaustive]
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct HealthResponse {
316    /// Overall health status (e.g., `"healthy"`, `"degraded"`).
317    pub status: String,
318    /// Server version string.
319    pub version: String,
320    /// Whether the Docker daemon is reachable.
321    pub docker_available: bool,
322    /// Whether the WASM execution engine is initialized.
323    pub wasm_engine_ready: bool,
324    /// Number of currently active containers.
325    pub active_containers: usize,
326    /// Server uptime in seconds.
327    pub uptime_seconds: u64,
328    /// Detailed per-component health. `None` if not requested.
329    #[serde(skip_serializing_if = "Option::is_none", default)]
330    pub components: Option<HealthComponents>,
331}
332
333impl HealthResponse {
334    /// Create a healthy response with the given version and uptime.
335    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/// Per-component health breakdown.
349#[non_exhaustive]
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct HealthComponents {
352    /// WASM engine health.
353    pub wasm_engine: ComponentHealth,
354    /// Docker daemon health.
355    pub docker: ComponentHealth,
356    /// Agent manager health.
357    pub agents: ComponentHealth,
358}
359
360impl HealthComponents {
361    /// Create a health components report.
362    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/// Health status of an individual component.
376#[non_exhaustive]
377#[derive(Debug, Clone, Serialize, Deserialize)]
378pub struct ComponentHealth {
379    /// Component status (e.g., `"ok"`, `"error"`, `"unavailable"`).
380    pub status: String,
381    /// Optional structured detail about the component's state.
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub detail: Option<serde_json::Value>,
384}
385
386impl ComponentHealth {
387    /// Create a healthy component status.
388    pub fn ok() -> Self {
389        Self {
390            status: "ok".into(),
391            detail: None,
392        }
393    }
394
395    /// Create an error component status with a detail message.
396    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/// Structured API error response.
405#[non_exhaustive]
406#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct ApiError {
408    /// Human-readable error message.
409    pub error: String,
410    /// Machine-readable error code (e.g., `"tool_not_found"`, `"policy_violation"`).
411    pub code: String,
412    /// Optional structured details about the error.
413    #[serde(skip_serializing_if = "Option::is_none")]
414    pub details: Option<serde_json::Value>,
415}
416
417impl ApiError {
418    /// Create an API error with the given message and code.
419    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    /// Attach structured details to this error.
428    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}