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::<&()>).await
497 }
498
499 pub async fn create_object(
501 &self,
502 payload: DurableObjectCreateRequest,
503 ) -> Result<DurableObject> {
504 self.request(reqwest::Method::POST, "/objects", Some(&payload)).await
505 }
506
507 pub async fn get_object(&self, id: &str) -> Result<DurableObject> {
509 self.request(
510 reqwest::Method::GET,
511 &format!("/objects/{id}"),
512 None::<&()>,
513 )
514 .await
515 }
516
517 pub async fn call_object(
519 &self,
520 class: &str,
521 object_id: &str,
522 method: &str,
523 args: serde_json::Value,
524 ) -> Result<serde_json::Value> {
525 let url = format!(
526 "{}/objects/{}/{}/call/{}",
527 self.base_url, class, object_id, method
528 );
529 let resp = self.http.post(&url).json(&args).send().await?;
530 let result = resp.json().await?;
531 Ok(result)
532 }
533
534 pub async fn delete_object(&self, id: &str) -> Result<String> {
536 self.request(reqwest::Method::DELETE, &format!("/objects/{id}"), None::<&()>)
537 .await
538 }
539
540 pub async fn patch_object(
542 &self,
543 id: &str,
544 payload: serde_json::Value,
545 ) -> Result<DurableObject> {
546 self.request(
547 reqwest::Method::PATCH,
548 &format!("/objects/{id}"),
549 Some(&payload),
550 )
551 .await
552 }
553
554 pub async fn list_schedules(&self) -> Result<Vec<Schedule>> {
556 self.request(reqwest::Method::GET, "/schedules", None::<&()>).await
557 }
558
559 pub async fn create_schedule(
561 &self,
562 payload: ScheduleCreateRequest,
563 ) -> 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::<&()>).await
591 }
592
593 pub async fn create_store(&self, payload: DurableStoreCreateRequest) -> Result<DurableStore> {
595 self.request(reqwest::Method::POST, "/stores", Some(&payload))
596 .await
597 }
598
599 pub async fn get_store(&self, id: &str) -> Result<DurableStore> {
601 self.request(reqwest::Method::GET, &format!("/stores/{id}"), None::<&()>)
602 .await
603 }
604
605 pub async fn delete_store(&self, id: &str) -> Result<String> {
607 self.request(
608 reqwest::Method::DELETE,
609 &format!("/stores/{id}"),
610 None::<&()>,
611 )
612 .await
613 }
614
615 pub async fn query_store(
617 &self,
618 id: &str,
619 payload: serde_json::Value,
620 ) -> Result<DurableStoreQueryResult> {
621 self.request(
622 reqwest::Method::POST,
623 &format!("/stores/{id}/query"),
624 Some(&payload),
625 )
626 .await
627 }
628
629 pub async fn execute_store(
631 &self,
632 id: &str,
633 payload: serde_json::Value,
634 ) -> Result<DurableStoreExecuteResult> {
635 self.request(
636 reqwest::Method::POST,
637 &format!("/stores/{id}/execute"),
638 Some(&payload),
639 )
640 .await
641 }
642
643 pub async fn command_store(
645 &self,
646 id: &str,
647 payload: serde_json::Value,
648 ) -> Result<DurableStoreCommandResult> {
649 self.request(
650 reqwest::Method::POST,
651 &format!("/stores/{id}/command"),
652 Some(&payload),
653 )
654 .await
655 }
656
657 pub async fn extend_ttl(&self, name: &str, by: &str) -> Result<ExtendTtlResponse> {
659 let body = ExtendTtlRequest { by: by.to_string() };
660 self.request(
661 reqwest::Method::POST,
662 &format!("/sandboxes/{name}/extend"),
663 Some(&body),
664 )
665 .await
666 }
667
668 pub async fn list_snapshots(&self) -> Result<Vec<SnapshotMeta>> {
670 self.request(reqwest::Method::GET, "/snapshots", None::<&()>)
671 .await
672 }
673
674 pub async fn take_snapshot(&self, opts: TakeSnapshotOptions) -> Result<SnapshotMeta> {
676 self.request(reqwest::Method::POST, "/snapshots", Some(&opts))
677 .await
678 }
679
680 pub async fn get_snapshot(&self, name: &str) -> Result<SnapshotMeta> {
682 self.request(
683 reqwest::Method::GET,
684 &format!("/snapshots/{name}"),
685 None::<&()>,
686 )
687 .await
688 }
689
690 pub async fn delete_snapshot(&self, name: &str) -> Result<()> {
692 let _: String = self
693 .request(
694 reqwest::Method::DELETE,
695 &format!("/snapshots/{name}"),
696 None::<&()>,
697 )
698 .await?;
699 Ok(())
700 }
701
702 pub async fn restore_snapshot(&self, name: &str) -> Result<SandboxInfo> {
704 self.request(
705 reqwest::Method::POST,
706 &format!("/snapshots/{name}/restore"),
707 None::<&()>,
708 )
709 .await
710 }
711
712 async fn request<T: serde::de::DeserializeOwned>(
715 &self,
716 method: reqwest::Method,
717 path: &str,
718 body: Option<&(impl serde::Serialize + ?Sized)>,
719 ) -> Result<T> {
720 let url = format!("{}{path}", self.base_url);
721 let mut req = self.http.request(method, &url);
722 if let Some(b) = body {
723 req = req.header(CONTENT_TYPE, "application/json").json(b);
724 }
725
726 let response = req.send().await?;
727 let status = response.status().as_u16();
728 let text = response.text().await?;
729
730 if status >= 400 {
731 return Err(error_from_status(status, &text));
732 }
733
734 let parsed: ApiResponse<T> = serde_json::from_str(&text)?;
735 if !parsed.success {
736 return Err(Error::Server(
737 parsed.error.unwrap_or_else(|| "Unknown error".to_string()),
738 ));
739 }
740 parsed
741 .data
742 .ok_or_else(|| Error::Server("Missing data field".to_string()))
743 }
744}
745
746pub struct SandboxHandle {
750 name: String,
751 client: AgentKernel,
752}
753
754impl SandboxHandle {
755 pub fn name(&self) -> &str {
757 &self.name
758 }
759
760 pub async fn run(&self, command: &[&str]) -> Result<RunOutput> {
762 self.client.exec_in_sandbox(&self.name, command, None).await
763 }
764
765 pub async fn run_with_options(&self, command: &[&str], opts: ExecOptions) -> Result<RunOutput> {
767 self.client
768 .exec_in_sandbox(&self.name, command, Some(opts))
769 .await
770 }
771
772 pub async fn info(&self) -> Result<SandboxInfo> {
774 self.client.get_sandbox(&self.name).await
775 }
776
777 pub async fn read_file(&self, path: &str) -> Result<FileReadResponse> {
779 self.client.read_file(&self.name, path).await
780 }
781
782 pub async fn write_file(
784 &self,
785 path: &str,
786 content: &str,
787 encoding: Option<&str>,
788 ) -> Result<String> {
789 self.client
790 .write_file(&self.name, path, content, encoding)
791 .await
792 }
793
794 pub async fn write_files(
796 &self,
797 files: std::collections::HashMap<String, String>,
798 ) -> Result<BatchFileWriteResponse> {
799 self.client.write_files(&self.name, files).await
800 }
801
802 pub async fn delete_file(&self, path: &str) -> Result<String> {
804 self.client.delete_file(&self.name, path).await
805 }
806}