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