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 get_sandbox_by_uuid(&self, uuid: &str) -> Result<SandboxInfo> {
153 self.request(
154 reqwest::Method::GET,
155 &format!("/sandboxes/by-uuid/{uuid}"),
156 None::<&()>,
157 )
158 .await
159 }
160
161 pub async fn remove_sandbox(&self, name: &str) -> Result<()> {
163 let _: String = self
164 .request(
165 reqwest::Method::DELETE,
166 &format!("/sandboxes/{name}"),
167 None::<&()>,
168 )
169 .await?;
170 Ok(())
171 }
172
173 pub async fn exec_in_sandbox(
175 &self,
176 name: &str,
177 command: &[&str],
178 opts: Option<ExecOptions>,
179 ) -> Result<RunOutput> {
180 let opts = opts.unwrap_or_default();
181 let body = ExecRequest {
182 command: command.iter().map(|s| s.to_string()).collect(),
183 env: opts.env,
184 workdir: opts.workdir,
185 sudo: opts.sudo,
186 };
187 self.request(
188 reqwest::Method::POST,
189 &format!("/sandboxes/{name}/exec"),
190 Some(&body),
191 )
192 .await
193 }
194
195 pub async fn with_sandbox<F, Fut, T>(
199 &self,
200 name: &str,
201 opts: Option<CreateSandboxOptions>,
202 f: F,
203 ) -> Result<T>
204 where
205 F: FnOnce(SandboxHandle) -> Fut,
206 Fut: std::future::Future<Output = Result<T>>,
207 {
208 self.create_sandbox(name, opts).await?;
209 let handle = SandboxHandle {
210 name: name.to_string(),
211 client: self.clone(),
212 };
213 let result = f(handle).await;
214 let _ = self.remove_sandbox(name).await;
216 result
217 }
218
219 pub async fn browser(&self, name: &str, memory_mb: Option<u64>) -> Result<BrowserSession> {
237 let opts = CreateSandboxOptions {
238 image: Some("python:3.12-slim".to_string()),
239 memory_mb: Some(memory_mb.unwrap_or(2048)),
240 profile: Some(SecurityProfile::Moderate),
241 ..Default::default()
242 };
243 self.create_sandbox(name, Some(opts)).await?;
244
245 self.exec_in_sandbox(name, BROWSER_SETUP_CMD, None).await?;
247
248 Ok(BrowserSession::new(name.to_string(), self.clone()))
249 }
250
251 pub async fn write_files(
253 &self,
254 name: &str,
255 files: std::collections::HashMap<String, String>,
256 ) -> Result<BatchFileWriteResponse> {
257 let body = BatchFileWriteRequest { files };
258 self.request(
259 reqwest::Method::POST,
260 &format!("/sandboxes/{name}/files"),
261 Some(&body),
262 )
263 .await
264 }
265
266 pub async fn read_file(&self, name: &str, path: &str) -> Result<FileReadResponse> {
268 self.request(
269 reqwest::Method::GET,
270 &format!("/sandboxes/{name}/files/{path}"),
271 None::<&()>,
272 )
273 .await
274 }
275
276 pub async fn write_file(
278 &self,
279 name: &str,
280 path: &str,
281 content: &str,
282 encoding: Option<&str>,
283 ) -> Result<String> {
284 let body = FileWriteRequest {
285 content: content.to_string(),
286 encoding: encoding.map(String::from),
287 };
288 self.request(
289 reqwest::Method::PUT,
290 &format!("/sandboxes/{name}/files/{path}"),
291 Some(&body),
292 )
293 .await
294 }
295
296 pub async fn delete_file(&self, name: &str, path: &str) -> Result<String> {
298 self.request(
299 reqwest::Method::DELETE,
300 &format!("/sandboxes/{name}/files/{path}"),
301 None::<&()>,
302 )
303 .await
304 }
305
306 pub async fn get_sandbox_logs(&self, name: &str) -> Result<Vec<serde_json::Value>> {
308 self.request(
309 reqwest::Method::GET,
310 &format!("/sandboxes/{name}/logs"),
311 None::<&()>,
312 )
313 .await
314 }
315
316 pub async fn exec_detached(
318 &self,
319 name: &str,
320 command: &[&str],
321 opts: Option<ExecOptions>,
322 ) -> Result<DetachedCommand> {
323 let opts = opts.unwrap_or_default();
324 let body = ExecRequest {
325 command: command.iter().map(|s| s.to_string()).collect(),
326 env: opts.env,
327 workdir: opts.workdir,
328 sudo: opts.sudo,
329 };
330 self.request(
331 reqwest::Method::POST,
332 &format!("/sandboxes/{name}/exec/detach"),
333 Some(&body),
334 )
335 .await
336 }
337
338 pub async fn detached_status(&self, name: &str, cmd_id: &str) -> Result<DetachedCommand> {
340 self.request(
341 reqwest::Method::GET,
342 &format!("/sandboxes/{name}/exec/detached/{cmd_id}"),
343 None::<&()>,
344 )
345 .await
346 }
347
348 pub async fn detached_logs(
350 &self,
351 name: &str,
352 cmd_id: &str,
353 stream: Option<&str>,
354 ) -> Result<DetachedLogsResponse> {
355 let query = match stream {
356 Some(s) => format!("?stream={s}"),
357 None => String::new(),
358 };
359 self.request(
360 reqwest::Method::GET,
361 &format!("/sandboxes/{name}/exec/detached/{cmd_id}/logs{query}"),
362 None::<&()>,
363 )
364 .await
365 }
366
367 pub async fn detached_kill(&self, name: &str, cmd_id: &str) -> Result<String> {
369 self.request(
370 reqwest::Method::DELETE,
371 &format!("/sandboxes/{name}/exec/detached/{cmd_id}"),
372 None::<&()>,
373 )
374 .await
375 }
376
377 pub async fn detached_list(&self, name: &str) -> Result<Vec<DetachedCommand>> {
379 self.request(
380 reqwest::Method::GET,
381 &format!("/sandboxes/{name}/exec/detached"),
382 None::<&()>,
383 )
384 .await
385 }
386
387 pub async fn batch_run(&self, commands: Vec<BatchCommand>) -> Result<BatchRunResponse> {
389 let body = BatchRunRequest { commands };
390 self.request(reqwest::Method::POST, "/batch/run", Some(&body))
391 .await
392 }
393
394 pub async fn list_orchestrations(&self) -> Result<Vec<Orchestration>> {
396 self.request(reqwest::Method::GET, "/orchestrations", None::<&()>)
397 .await
398 }
399
400 pub async fn create_orchestration(
402 &self,
403 payload: OrchestrationCreateRequest,
404 ) -> Result<Orchestration> {
405 self.request(reqwest::Method::POST, "/orchestrations", Some(&payload))
406 .await
407 }
408
409 pub async fn get_orchestration(&self, id: &str) -> Result<Orchestration> {
411 self.request(
412 reqwest::Method::GET,
413 &format!("/orchestrations/{id}"),
414 None::<&()>,
415 )
416 .await
417 }
418
419 pub async fn signal_orchestration(
421 &self,
422 id: &str,
423 payload: serde_json::Value,
424 ) -> Result<Orchestration> {
425 self.request(
426 reqwest::Method::POST,
427 &format!("/orchestrations/{id}/events"),
428 Some(&payload),
429 )
430 .await
431 }
432
433 pub async fn terminate_orchestration(
435 &self,
436 id: &str,
437 payload: Option<serde_json::Value>,
438 ) -> Result<Orchestration> {
439 let body = payload.unwrap_or_else(|| serde_json::json!({}));
440 self.request(
441 reqwest::Method::POST,
442 &format!("/orchestrations/{id}/terminate"),
443 Some(&body),
444 )
445 .await
446 }
447
448 pub async fn list_orchestration_definitions(&self) -> Result<Vec<OrchestrationDefinition>> {
450 self.request(
451 reqwest::Method::GET,
452 "/orchestrations/definitions",
453 None::<&()>,
454 )
455 .await
456 }
457
458 pub async fn upsert_orchestration_definition(
460 &self,
461 payload: OrchestrationDefinition,
462 ) -> Result<OrchestrationDefinition> {
463 self.request(
464 reqwest::Method::POST,
465 "/orchestrations/definitions",
466 Some(&payload),
467 )
468 .await
469 }
470
471 pub async fn get_orchestration_definition(
473 &self,
474 name: &str,
475 ) -> Result<OrchestrationDefinition> {
476 self.request(
477 reqwest::Method::GET,
478 &format!("/orchestrations/definitions/{name}"),
479 None::<&()>,
480 )
481 .await
482 }
483
484 pub async fn delete_orchestration_definition(&self, name: &str) -> Result<String> {
486 self.request(
487 reqwest::Method::DELETE,
488 &format!("/orchestrations/definitions/{name}"),
489 None::<&()>,
490 )
491 .await
492 }
493
494 pub async fn list_objects(&self) -> Result<Vec<DurableObject>> {
496 self.request(reqwest::Method::GET, "/objects", None::<&()>)
497 .await
498 }
499
500 pub async fn create_object(
502 &self,
503 payload: DurableObjectCreateRequest,
504 ) -> Result<DurableObject> {
505 self.request(reqwest::Method::POST, "/objects", Some(&payload))
506 .await
507 }
508
509 pub async fn get_object(&self, id: &str) -> Result<DurableObject> {
511 self.request(reqwest::Method::GET, &format!("/objects/{id}"), None::<&()>)
512 .await
513 }
514
515 pub async fn call_object(
517 &self,
518 class: &str,
519 object_id: &str,
520 method: &str,
521 args: serde_json::Value,
522 ) -> Result<serde_json::Value> {
523 let url = format!(
524 "{}/objects/{}/{}/call/{}",
525 self.base_url, class, object_id, method
526 );
527 let resp = self.http.post(&url).json(&args).send().await?;
528 let result = resp.json().await?;
529 Ok(result)
530 }
531
532 pub async fn delete_object(&self, id: &str) -> Result<String> {
534 self.request(
535 reqwest::Method::DELETE,
536 &format!("/objects/{id}"),
537 None::<&()>,
538 )
539 .await
540 }
541
542 pub async fn patch_object(
544 &self,
545 id: &str,
546 payload: serde_json::Value,
547 ) -> Result<DurableObject> {
548 self.request(
549 reqwest::Method::PATCH,
550 &format!("/objects/{id}"),
551 Some(&payload),
552 )
553 .await
554 }
555
556 pub async fn list_schedules(&self) -> Result<Vec<Schedule>> {
558 self.request(reqwest::Method::GET, "/schedules", None::<&()>)
559 .await
560 }
561
562 pub async fn create_schedule(&self, payload: ScheduleCreateRequest) -> Result<Schedule> {
564 self.request(reqwest::Method::POST, "/schedules", Some(&payload))
565 .await
566 }
567
568 pub async fn get_schedule(&self, id: &str) -> Result<Schedule> {
570 self.request(
571 reqwest::Method::GET,
572 &format!("/schedules/{id}"),
573 None::<&()>,
574 )
575 .await
576 }
577
578 pub async fn delete_schedule(&self, id: &str) -> Result<String> {
580 self.request(
581 reqwest::Method::DELETE,
582 &format!("/schedules/{id}"),
583 None::<&()>,
584 )
585 .await
586 }
587
588 pub async fn list_stores(&self) -> Result<Vec<DurableStore>> {
590 self.request(reqwest::Method::GET, "/stores", None::<&()>)
591 .await
592 }
593
594 pub async fn create_store(&self, payload: DurableStoreCreateRequest) -> Result<DurableStore> {
596 self.request(reqwest::Method::POST, "/stores", Some(&payload))
597 .await
598 }
599
600 pub async fn get_store(&self, id: &str) -> Result<DurableStore> {
602 self.request(reqwest::Method::GET, &format!("/stores/{id}"), None::<&()>)
603 .await
604 }
605
606 pub async fn delete_store(&self, id: &str) -> Result<String> {
608 self.request(
609 reqwest::Method::DELETE,
610 &format!("/stores/{id}"),
611 None::<&()>,
612 )
613 .await
614 }
615
616 pub async fn query_store(
618 &self,
619 id: &str,
620 payload: serde_json::Value,
621 ) -> Result<DurableStoreQueryResult> {
622 self.request(
623 reqwest::Method::POST,
624 &format!("/stores/{id}/query"),
625 Some(&payload),
626 )
627 .await
628 }
629
630 pub async fn execute_store(
632 &self,
633 id: &str,
634 payload: serde_json::Value,
635 ) -> Result<DurableStoreExecuteResult> {
636 self.request(
637 reqwest::Method::POST,
638 &format!("/stores/{id}/execute"),
639 Some(&payload),
640 )
641 .await
642 }
643
644 pub async fn command_store(
646 &self,
647 id: &str,
648 payload: serde_json::Value,
649 ) -> Result<DurableStoreCommandResult> {
650 self.request(
651 reqwest::Method::POST,
652 &format!("/stores/{id}/command"),
653 Some(&payload),
654 )
655 .await
656 }
657
658 pub async fn extend_ttl(&self, name: &str, by: &str) -> Result<ExtendTtlResponse> {
660 let body = ExtendTtlRequest { by: by.to_string() };
661 self.request(
662 reqwest::Method::POST,
663 &format!("/sandboxes/{name}/extend"),
664 Some(&body),
665 )
666 .await
667 }
668
669 pub async fn list_snapshots(&self) -> Result<Vec<SnapshotMeta>> {
671 self.request(reqwest::Method::GET, "/snapshots", None::<&()>)
672 .await
673 }
674
675 pub async fn take_snapshot(&self, opts: TakeSnapshotOptions) -> Result<SnapshotMeta> {
677 self.request(reqwest::Method::POST, "/snapshots", Some(&opts))
678 .await
679 }
680
681 pub async fn get_snapshot(&self, name: &str) -> Result<SnapshotMeta> {
683 self.request(
684 reqwest::Method::GET,
685 &format!("/snapshots/{name}"),
686 None::<&()>,
687 )
688 .await
689 }
690
691 pub async fn delete_snapshot(&self, name: &str) -> Result<()> {
693 let _: String = self
694 .request(
695 reqwest::Method::DELETE,
696 &format!("/snapshots/{name}"),
697 None::<&()>,
698 )
699 .await?;
700 Ok(())
701 }
702
703 pub async fn restore_snapshot(&self, name: &str) -> Result<SandboxInfo> {
705 self.request(
706 reqwest::Method::POST,
707 &format!("/snapshots/{name}/restore"),
708 None::<&()>,
709 )
710 .await
711 }
712
713 async fn request<T: serde::de::DeserializeOwned>(
716 &self,
717 method: reqwest::Method,
718 path: &str,
719 body: Option<&(impl serde::Serialize + ?Sized)>,
720 ) -> Result<T> {
721 let url = format!("{}{path}", self.base_url);
722 let mut req = self.http.request(method, &url);
723 if let Some(b) = body {
724 req = req.header(CONTENT_TYPE, "application/json").json(b);
725 }
726
727 let response = req.send().await?;
728 let status = response.status().as_u16();
729 let text = response.text().await?;
730
731 if status >= 400 {
732 return Err(error_from_status(status, &text));
733 }
734
735 let parsed: ApiResponse<T> = serde_json::from_str(&text)?;
736 if !parsed.success {
737 return Err(Error::Server(
738 parsed.error.unwrap_or_else(|| "Unknown error".to_string()),
739 ));
740 }
741 parsed
742 .data
743 .ok_or_else(|| Error::Server("Missing data field".to_string()))
744 }
745}
746
747pub struct SandboxHandle {
751 name: String,
752 client: AgentKernel,
753}
754
755impl SandboxHandle {
756 pub fn name(&self) -> &str {
758 &self.name
759 }
760
761 pub async fn run(&self, command: &[&str]) -> Result<RunOutput> {
763 self.client.exec_in_sandbox(&self.name, command, None).await
764 }
765
766 pub async fn run_with_options(&self, command: &[&str], opts: ExecOptions) -> Result<RunOutput> {
768 self.client
769 .exec_in_sandbox(&self.name, command, Some(opts))
770 .await
771 }
772
773 pub async fn info(&self) -> Result<SandboxInfo> {
775 self.client.get_sandbox(&self.name).await
776 }
777
778 pub async fn read_file(&self, path: &str) -> Result<FileReadResponse> {
780 self.client.read_file(&self.name, path).await
781 }
782
783 pub async fn write_file(
785 &self,
786 path: &str,
787 content: &str,
788 encoding: Option<&str>,
789 ) -> Result<String> {
790 self.client
791 .write_file(&self.name, path, content, encoding)
792 .await
793 }
794
795 pub async fn write_files(
797 &self,
798 files: std::collections::HashMap<String, String>,
799 ) -> Result<BatchFileWriteResponse> {
800 self.client.write_files(&self.name, files).await
801 }
802
803 pub async fn delete_file(&self, path: &str) -> Result<String> {
805 self.client.delete_file(&self.name, path).await
806 }
807}