1use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue, USER_AGENT};
2use std::time::Duration;
3
4use crate::browser::{BROWSER_SETUP_CMD, BrowserSession};
5use crate::error::{Error, Result, error_from_status};
6use crate::types::*;
7
8const SDK_VERSION: &str = env!("CARGO_PKG_VERSION");
9const DEFAULT_BASE_URL: &str = "http://localhost:18888";
10const DEFAULT_TIMEOUT_SECS: u64 = 30;
11
12pub struct AgentKernelBuilder {
14 base_url: String,
15 api_key: Option<String>,
16 timeout: Duration,
17}
18
19impl AgentKernelBuilder {
20 pub fn base_url(mut self, url: impl Into<String>) -> Self {
22 self.base_url = url.into();
23 self
24 }
25
26 pub fn api_key(mut self, key: impl Into<String>) -> Self {
28 self.api_key = Some(key.into());
29 self
30 }
31
32 pub fn timeout(mut self, timeout: Duration) -> Self {
34 self.timeout = timeout;
35 self
36 }
37
38 pub fn build(self) -> Result<AgentKernel> {
40 let mut headers = HeaderMap::new();
41 headers.insert(
42 USER_AGENT,
43 HeaderValue::from_str(&format!("agentkernel-rust-sdk/{SDK_VERSION}")).unwrap(),
44 );
45 if let Some(ref key) = self.api_key {
46 headers.insert(
47 AUTHORIZATION,
48 HeaderValue::from_str(&format!("Bearer {key}"))
49 .map_err(|e| Error::Auth(e.to_string()))?,
50 );
51 }
52
53 let http = reqwest::Client::builder()
54 .default_headers(headers)
55 .timeout(self.timeout)
56 .build()?;
57
58 Ok(AgentKernel {
59 base_url: self.base_url.trim_end_matches('/').to_string(),
60 http,
61 })
62 }
63}
64
65#[derive(Clone)]
77pub struct AgentKernel {
78 base_url: String,
79 http: reqwest::Client,
80}
81
82impl AgentKernel {
83 pub fn builder() -> AgentKernelBuilder {
85 AgentKernelBuilder {
86 base_url: std::env::var("AGENTKERNEL_BASE_URL")
87 .unwrap_or_else(|_| DEFAULT_BASE_URL.to_string()),
88 api_key: std::env::var("AGENTKERNEL_API_KEY").ok(),
89 timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS),
90 }
91 }
92
93 pub async fn health(&self) -> Result<String> {
95 self.request::<String>(reqwest::Method::GET, "/health", None::<&()>)
96 .await
97 }
98
99 pub async fn run(&self, command: &[&str], opts: Option<RunOptions>) -> Result<RunOutput> {
101 let opts = opts.unwrap_or_default();
102 let body = RunRequest {
103 command: command.iter().map(|s| s.to_string()).collect(),
104 image: opts.image,
105 profile: opts.profile,
106 fast: opts.fast.unwrap_or(true),
107 };
108 self.request(reqwest::Method::POST, "/run", Some(&body))
109 .await
110 }
111
112 pub async fn list_sandboxes(&self) -> Result<Vec<SandboxInfo>> {
114 self.request(reqwest::Method::GET, "/sandboxes", None::<&()>)
115 .await
116 }
117
118 pub async fn create_sandbox(
120 &self,
121 name: &str,
122 opts: Option<CreateSandboxOptions>,
123 ) -> Result<SandboxInfo> {
124 let opts = opts.unwrap_or_default();
125 let body = CreateRequest {
126 name: name.to_string(),
127 image: opts.image,
128 vcpus: opts.vcpus,
129 memory_mb: opts.memory_mb,
130 profile: opts.profile,
131 source_url: opts.source_url,
132 source_ref: opts.source_ref,
133 volumes: opts.volumes,
134 secrets: opts.secrets,
135 secret_files: opts.secret_files,
136 };
137 self.request(reqwest::Method::POST, "/sandboxes", Some(&body))
138 .await
139 }
140
141 pub async fn get_sandbox(&self, name: &str) -> Result<SandboxInfo> {
143 self.request(
144 reqwest::Method::GET,
145 &format!("/sandboxes/{name}"),
146 None::<&()>,
147 )
148 .await
149 }
150
151 pub async fn remove_sandbox(&self, name: &str) -> Result<()> {
153 let _: String = self
154 .request(
155 reqwest::Method::DELETE,
156 &format!("/sandboxes/{name}"),
157 None::<&()>,
158 )
159 .await?;
160 Ok(())
161 }
162
163 pub async fn exec_in_sandbox(
165 &self,
166 name: &str,
167 command: &[&str],
168 opts: Option<ExecOptions>,
169 ) -> Result<RunOutput> {
170 let opts = opts.unwrap_or_default();
171 let body = ExecRequest {
172 command: command.iter().map(|s| s.to_string()).collect(),
173 env: opts.env,
174 workdir: opts.workdir,
175 sudo: opts.sudo,
176 };
177 self.request(
178 reqwest::Method::POST,
179 &format!("/sandboxes/{name}/exec"),
180 Some(&body),
181 )
182 .await
183 }
184
185 pub async fn with_sandbox<F, Fut, T>(
189 &self,
190 name: &str,
191 opts: Option<CreateSandboxOptions>,
192 f: F,
193 ) -> Result<T>
194 where
195 F: FnOnce(SandboxHandle) -> Fut,
196 Fut: std::future::Future<Output = Result<T>>,
197 {
198 self.create_sandbox(name, opts).await?;
199 let handle = SandboxHandle {
200 name: name.to_string(),
201 client: self.clone(),
202 };
203 let result = f(handle).await;
204 let _ = self.remove_sandbox(name).await;
206 result
207 }
208
209 pub async fn browser(&self, name: &str, memory_mb: Option<u64>) -> Result<BrowserSession> {
227 let opts = CreateSandboxOptions {
228 image: Some("python:3.12-slim".to_string()),
229 memory_mb: Some(memory_mb.unwrap_or(2048)),
230 profile: Some(SecurityProfile::Moderate),
231 ..Default::default()
232 };
233 self.create_sandbox(name, Some(opts)).await?;
234
235 self.exec_in_sandbox(name, BROWSER_SETUP_CMD, None).await?;
237
238 Ok(BrowserSession::new(name.to_string(), self.clone()))
239 }
240
241 pub async fn write_files(
243 &self,
244 name: &str,
245 files: std::collections::HashMap<String, String>,
246 ) -> Result<BatchFileWriteResponse> {
247 let body = BatchFileWriteRequest { files };
248 self.request(
249 reqwest::Method::POST,
250 &format!("/sandboxes/{name}/files"),
251 Some(&body),
252 )
253 .await
254 }
255
256 pub async fn read_file(&self, name: &str, path: &str) -> Result<FileReadResponse> {
258 self.request(
259 reqwest::Method::GET,
260 &format!("/sandboxes/{name}/files/{path}"),
261 None::<&()>,
262 )
263 .await
264 }
265
266 pub async fn write_file(
268 &self,
269 name: &str,
270 path: &str,
271 content: &str,
272 encoding: Option<&str>,
273 ) -> Result<String> {
274 let body = FileWriteRequest {
275 content: content.to_string(),
276 encoding: encoding.map(String::from),
277 };
278 self.request(
279 reqwest::Method::PUT,
280 &format!("/sandboxes/{name}/files/{path}"),
281 Some(&body),
282 )
283 .await
284 }
285
286 pub async fn delete_file(&self, name: &str, path: &str) -> Result<String> {
288 self.request(
289 reqwest::Method::DELETE,
290 &format!("/sandboxes/{name}/files/{path}"),
291 None::<&()>,
292 )
293 .await
294 }
295
296 pub async fn get_sandbox_logs(&self, name: &str) -> Result<Vec<serde_json::Value>> {
298 self.request(
299 reqwest::Method::GET,
300 &format!("/sandboxes/{name}/logs"),
301 None::<&()>,
302 )
303 .await
304 }
305
306 pub async fn exec_detached(
308 &self,
309 name: &str,
310 command: &[&str],
311 opts: Option<ExecOptions>,
312 ) -> Result<DetachedCommand> {
313 let opts = opts.unwrap_or_default();
314 let body = ExecRequest {
315 command: command.iter().map(|s| s.to_string()).collect(),
316 env: opts.env,
317 workdir: opts.workdir,
318 sudo: opts.sudo,
319 };
320 self.request(
321 reqwest::Method::POST,
322 &format!("/sandboxes/{name}/exec/detach"),
323 Some(&body),
324 )
325 .await
326 }
327
328 pub async fn detached_status(&self, name: &str, cmd_id: &str) -> Result<DetachedCommand> {
330 self.request(
331 reqwest::Method::GET,
332 &format!("/sandboxes/{name}/exec/detached/{cmd_id}"),
333 None::<&()>,
334 )
335 .await
336 }
337
338 pub async fn detached_logs(
340 &self,
341 name: &str,
342 cmd_id: &str,
343 stream: Option<&str>,
344 ) -> Result<DetachedLogsResponse> {
345 let query = match stream {
346 Some(s) => format!("?stream={s}"),
347 None => String::new(),
348 };
349 self.request(
350 reqwest::Method::GET,
351 &format!("/sandboxes/{name}/exec/detached/{cmd_id}/logs{query}"),
352 None::<&()>,
353 )
354 .await
355 }
356
357 pub async fn detached_kill(&self, name: &str, cmd_id: &str) -> Result<String> {
359 self.request(
360 reqwest::Method::DELETE,
361 &format!("/sandboxes/{name}/exec/detached/{cmd_id}"),
362 None::<&()>,
363 )
364 .await
365 }
366
367 pub async fn detached_list(&self, name: &str) -> Result<Vec<DetachedCommand>> {
369 self.request(
370 reqwest::Method::GET,
371 &format!("/sandboxes/{name}/exec/detached"),
372 None::<&()>,
373 )
374 .await
375 }
376
377 pub async fn batch_run(&self, commands: Vec<BatchCommand>) -> Result<BatchRunResponse> {
379 let body = BatchRunRequest { commands };
380 self.request(reqwest::Method::POST, "/batch/run", Some(&body))
381 .await
382 }
383
384 pub async fn extend_ttl(&self, name: &str, by: &str) -> Result<ExtendTtlResponse> {
386 let body = ExtendTtlRequest { by: by.to_string() };
387 self.request(
388 reqwest::Method::POST,
389 &format!("/sandboxes/{name}/extend"),
390 Some(&body),
391 )
392 .await
393 }
394
395 pub async fn list_snapshots(&self) -> Result<Vec<SnapshotMeta>> {
397 self.request(reqwest::Method::GET, "/snapshots", None::<&()>)
398 .await
399 }
400
401 pub async fn take_snapshot(&self, opts: TakeSnapshotOptions) -> Result<SnapshotMeta> {
403 self.request(reqwest::Method::POST, "/snapshots", Some(&opts))
404 .await
405 }
406
407 pub async fn get_snapshot(&self, name: &str) -> Result<SnapshotMeta> {
409 self.request(
410 reqwest::Method::GET,
411 &format!("/snapshots/{name}"),
412 None::<&()>,
413 )
414 .await
415 }
416
417 pub async fn delete_snapshot(&self, name: &str) -> Result<()> {
419 let _: String = self
420 .request(
421 reqwest::Method::DELETE,
422 &format!("/snapshots/{name}"),
423 None::<&()>,
424 )
425 .await?;
426 Ok(())
427 }
428
429 pub async fn restore_snapshot(&self, name: &str) -> Result<SandboxInfo> {
431 self.request(
432 reqwest::Method::POST,
433 &format!("/snapshots/{name}/restore"),
434 None::<&()>,
435 )
436 .await
437 }
438
439 async fn request<T: serde::de::DeserializeOwned>(
442 &self,
443 method: reqwest::Method,
444 path: &str,
445 body: Option<&(impl serde::Serialize + ?Sized)>,
446 ) -> Result<T> {
447 let url = format!("{}{path}", self.base_url);
448 let mut req = self.http.request(method, &url);
449 if let Some(b) = body {
450 req = req.header(CONTENT_TYPE, "application/json").json(b);
451 }
452
453 let response = req.send().await?;
454 let status = response.status().as_u16();
455 let text = response.text().await?;
456
457 if status >= 400 {
458 return Err(error_from_status(status, &text));
459 }
460
461 let parsed: ApiResponse<T> = serde_json::from_str(&text)?;
462 if !parsed.success {
463 return Err(Error::Server(
464 parsed.error.unwrap_or_else(|| "Unknown error".to_string()),
465 ));
466 }
467 parsed
468 .data
469 .ok_or_else(|| Error::Server("Missing data field".to_string()))
470 }
471}
472
473pub struct SandboxHandle {
477 name: String,
478 client: AgentKernel,
479}
480
481impl SandboxHandle {
482 pub fn name(&self) -> &str {
484 &self.name
485 }
486
487 pub async fn run(&self, command: &[&str]) -> Result<RunOutput> {
489 self.client.exec_in_sandbox(&self.name, command, None).await
490 }
491
492 pub async fn run_with_options(&self, command: &[&str], opts: ExecOptions) -> Result<RunOutput> {
494 self.client
495 .exec_in_sandbox(&self.name, command, Some(opts))
496 .await
497 }
498
499 pub async fn info(&self) -> Result<SandboxInfo> {
501 self.client.get_sandbox(&self.name).await
502 }
503
504 pub async fn read_file(&self, path: &str) -> Result<FileReadResponse> {
506 self.client.read_file(&self.name, path).await
507 }
508
509 pub async fn write_file(
511 &self,
512 path: &str,
513 content: &str,
514 encoding: Option<&str>,
515 ) -> Result<String> {
516 self.client
517 .write_file(&self.name, path, content, encoding)
518 .await
519 }
520
521 pub async fn write_files(
523 &self,
524 files: std::collections::HashMap<String, String>,
525 ) -> Result<BatchFileWriteResponse> {
526 self.client.write_files(&self.name, files).await
527 }
528
529 pub async fn delete_file(&self, path: &str) -> Result<String> {
531 self.client.delete_file(&self.name, path).await
532 }
533}