Skip to main content

agentkernel_sdk/
client.rs

1use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE, USER_AGENT};
2use std::time::Duration;
3
4use crate::error::{error_from_status, Error, Result};
5use crate::types::*;
6
7const SDK_VERSION: &str = env!("CARGO_PKG_VERSION");
8const DEFAULT_BASE_URL: &str = "http://localhost:18888";
9const DEFAULT_TIMEOUT_SECS: u64 = 30;
10
11/// Builder for constructing an [`AgentKernel`] client.
12pub struct AgentKernelBuilder {
13    base_url: String,
14    api_key: Option<String>,
15    timeout: Duration,
16}
17
18impl AgentKernelBuilder {
19    /// Set the base URL.
20    pub fn base_url(mut self, url: impl Into<String>) -> Self {
21        self.base_url = url.into();
22        self
23    }
24
25    /// Set the API key for Bearer authentication.
26    pub fn api_key(mut self, key: impl Into<String>) -> Self {
27        self.api_key = Some(key.into());
28        self
29    }
30
31    /// Set the request timeout.
32    pub fn timeout(mut self, timeout: Duration) -> Self {
33        self.timeout = timeout;
34        self
35    }
36
37    /// Build the client.
38    pub fn build(self) -> Result<AgentKernel> {
39        let mut headers = HeaderMap::new();
40        headers.insert(
41            USER_AGENT,
42            HeaderValue::from_str(&format!("agentkernel-rust-sdk/{SDK_VERSION}")).unwrap(),
43        );
44        if let Some(ref key) = self.api_key {
45            headers.insert(
46                AUTHORIZATION,
47                HeaderValue::from_str(&format!("Bearer {key}"))
48                    .map_err(|e| Error::Auth(e.to_string()))?,
49            );
50        }
51
52        let http = reqwest::Client::builder()
53            .default_headers(headers)
54            .timeout(self.timeout)
55            .build()?;
56
57        Ok(AgentKernel {
58            base_url: self.base_url.trim_end_matches('/').to_string(),
59            http,
60        })
61    }
62}
63
64/// Client for the agentkernel HTTP API.
65///
66/// # Example
67/// ```no_run
68/// # async fn example() -> agentkernel_sdk::Result<()> {
69/// let client = agentkernel_sdk::AgentKernel::builder().build()?;
70/// let output = client.run(&["echo", "hello"], None).await?;
71/// println!("{}", output.output);
72/// # Ok(())
73/// # }
74/// ```
75#[derive(Clone)]
76pub struct AgentKernel {
77    base_url: String,
78    http: reqwest::Client,
79}
80
81impl AgentKernel {
82    /// Create a new builder with defaults resolved from env vars.
83    pub fn builder() -> AgentKernelBuilder {
84        AgentKernelBuilder {
85            base_url: std::env::var("AGENTKERNEL_BASE_URL")
86                .unwrap_or_else(|_| DEFAULT_BASE_URL.to_string()),
87            api_key: std::env::var("AGENTKERNEL_API_KEY").ok(),
88            timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS),
89        }
90    }
91
92    /// Health check. Returns `"ok"`.
93    pub async fn health(&self) -> Result<String> {
94        self.request::<String>(reqwest::Method::GET, "/health", None::<&()>)
95            .await
96    }
97
98    /// Run a command in a temporary sandbox.
99    pub async fn run(&self, command: &[&str], opts: Option<RunOptions>) -> Result<RunOutput> {
100        let opts = opts.unwrap_or_default();
101        let body = RunRequest {
102            command: command.iter().map(|s| s.to_string()).collect(),
103            image: opts.image,
104            profile: opts.profile,
105            fast: opts.fast.unwrap_or(true),
106        };
107        self.request(reqwest::Method::POST, "/run", Some(&body))
108            .await
109    }
110
111    /// List all sandboxes.
112    pub async fn list_sandboxes(&self) -> Result<Vec<SandboxInfo>> {
113        self.request(reqwest::Method::GET, "/sandboxes", None::<&()>)
114            .await
115    }
116
117    /// Create a new sandbox with optional resource limits.
118    pub async fn create_sandbox(
119        &self,
120        name: &str,
121        image: Option<&str>,
122        vcpus: Option<u32>,
123        memory_mb: Option<u64>,
124        profile: Option<SecurityProfile>,
125    ) -> Result<SandboxInfo> {
126        let body = CreateRequest {
127            name: name.to_string(),
128            image: image.map(String::from),
129            vcpus,
130            memory_mb,
131            profile,
132        };
133        self.request(reqwest::Method::POST, "/sandboxes", Some(&body))
134            .await
135    }
136
137    /// Get info about a sandbox.
138    pub async fn get_sandbox(&self, name: &str) -> Result<SandboxInfo> {
139        self.request(
140            reqwest::Method::GET,
141            &format!("/sandboxes/{name}"),
142            None::<&()>,
143        )
144        .await
145    }
146
147    /// Remove a sandbox.
148    pub async fn remove_sandbox(&self, name: &str) -> Result<()> {
149        let _: String = self
150            .request(
151                reqwest::Method::DELETE,
152                &format!("/sandboxes/{name}"),
153                None::<&()>,
154            )
155            .await?;
156        Ok(())
157    }
158
159    /// Run a command in an existing sandbox.
160    pub async fn exec_in_sandbox(&self, name: &str, command: &[&str]) -> Result<RunOutput> {
161        let body = ExecRequest {
162            command: command.iter().map(|s| s.to_string()).collect(),
163        };
164        self.request(
165            reqwest::Method::POST,
166            &format!("/sandboxes/{name}/exec"),
167            Some(&body),
168        )
169        .await
170    }
171
172    /// Create a sandbox and return a guard that removes it on drop.
173    ///
174    /// Use `with_sandbox` for guaranteed cleanup via a closure.
175    pub async fn with_sandbox<F, Fut, T>(&self, name: &str, image: Option<&str>, f: F) -> Result<T>
176    where
177        F: FnOnce(SandboxHandle) -> Fut,
178        Fut: std::future::Future<Output = Result<T>>,
179    {
180        self.create_sandbox(name, image, None, None, None).await?;
181        let handle = SandboxHandle {
182            name: name.to_string(),
183            client: self.clone(),
184        };
185        let result = f(handle).await;
186        // Always clean up
187        let _ = self.remove_sandbox(name).await;
188        result
189    }
190
191    /// Read a file from a sandbox.
192    pub async fn read_file(&self, name: &str, path: &str) -> Result<FileReadResponse> {
193        self.request(
194            reqwest::Method::GET,
195            &format!("/sandboxes/{name}/files/{path}"),
196            None::<&()>,
197        )
198        .await
199    }
200
201    /// Write a file to a sandbox.
202    pub async fn write_file(
203        &self,
204        name: &str,
205        path: &str,
206        content: &str,
207        encoding: Option<&str>,
208    ) -> Result<String> {
209        let body = FileWriteRequest {
210            content: content.to_string(),
211            encoding: encoding.map(String::from),
212        };
213        self.request(
214            reqwest::Method::PUT,
215            &format!("/sandboxes/{name}/files/{path}"),
216            Some(&body),
217        )
218        .await
219    }
220
221    /// Delete a file from a sandbox.
222    pub async fn delete_file(&self, name: &str, path: &str) -> Result<String> {
223        self.request(
224            reqwest::Method::DELETE,
225            &format!("/sandboxes/{name}/files/{path}"),
226            None::<&()>,
227        )
228        .await
229    }
230
231    /// Get audit log entries for a sandbox.
232    pub async fn get_sandbox_logs(&self, name: &str) -> Result<Vec<serde_json::Value>> {
233        self.request(
234            reqwest::Method::GET,
235            &format!("/sandboxes/{name}/logs"),
236            None::<&()>,
237        )
238        .await
239    }
240
241    /// Run multiple commands in parallel.
242    pub async fn batch_run(&self, commands: Vec<BatchCommand>) -> Result<BatchRunResponse> {
243        let body = BatchRunRequest { commands };
244        self.request(reqwest::Method::POST, "/batch/run", Some(&body))
245            .await
246    }
247
248    // -- Internal --
249
250    async fn request<T: serde::de::DeserializeOwned>(
251        &self,
252        method: reqwest::Method,
253        path: &str,
254        body: Option<&(impl serde::Serialize + ?Sized)>,
255    ) -> Result<T> {
256        let url = format!("{}{path}", self.base_url);
257        let mut req = self.http.request(method, &url);
258        if let Some(b) = body {
259            req = req.header(CONTENT_TYPE, "application/json").json(b);
260        }
261
262        let response = req.send().await?;
263        let status = response.status().as_u16();
264        let text = response.text().await?;
265
266        if status >= 400 {
267            return Err(error_from_status(status, &text));
268        }
269
270        let parsed: ApiResponse<T> = serde_json::from_str(&text)?;
271        if !parsed.success {
272            return Err(Error::Server(
273                parsed.error.unwrap_or_else(|| "Unknown error".to_string()),
274            ));
275        }
276        parsed
277            .data
278            .ok_or_else(|| Error::Server("Missing data field".to_string()))
279    }
280}
281
282/// Handle to a sandbox within a `with_sandbox` closure.
283///
284/// Owns a clone of the client (cheap — `reqwest::Client` is `Arc`-backed).
285pub struct SandboxHandle {
286    name: String,
287    client: AgentKernel,
288}
289
290impl SandboxHandle {
291    /// The sandbox name.
292    pub fn name(&self) -> &str {
293        &self.name
294    }
295
296    /// Run a command in this sandbox.
297    pub async fn run(&self, command: &[&str]) -> Result<RunOutput> {
298        self.client.exec_in_sandbox(&self.name, command).await
299    }
300
301    /// Get sandbox info.
302    pub async fn info(&self) -> Result<SandboxInfo> {
303        self.client.get_sandbox(&self.name).await
304    }
305
306    /// Read a file from this sandbox.
307    pub async fn read_file(&self, path: &str) -> Result<FileReadResponse> {
308        self.client.read_file(&self.name, path).await
309    }
310
311    /// Write a file to this sandbox.
312    pub async fn write_file(&self, path: &str, content: &str, encoding: Option<&str>) -> Result<String> {
313        self.client.write_file(&self.name, path, content, encoding).await
314    }
315
316    /// Delete a file from this sandbox.
317    pub async fn delete_file(&self, path: &str) -> Result<String> {
318        self.client.delete_file(&self.name, path).await
319    }
320}