Skip to main content

agentkernel_sdk/
client.rs

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
12/// Builder for constructing an [`AgentKernel`] client.
13pub struct AgentKernelBuilder {
14    base_url: String,
15    api_key: Option<String>,
16    timeout: Duration,
17}
18
19impl AgentKernelBuilder {
20    /// Set the base URL.
21    pub fn base_url(mut self, url: impl Into<String>) -> Self {
22        self.base_url = url.into();
23        self
24    }
25
26    /// Set the API key for Bearer authentication.
27    pub fn api_key(mut self, key: impl Into<String>) -> Self {
28        self.api_key = Some(key.into());
29        self
30    }
31
32    /// Set the request timeout.
33    pub fn timeout(mut self, timeout: Duration) -> Self {
34        self.timeout = timeout;
35        self
36    }
37
38    /// Build the client.
39    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/// Client for the agentkernel HTTP API.
66///
67/// # Example
68/// ```no_run
69/// # async fn example() -> agentkernel_sdk::Result<()> {
70/// let client = agentkernel_sdk::AgentKernel::builder().build()?;
71/// let output = client.run(&["echo", "hello"], None).await?;
72/// println!("{}", output.output);
73/// # Ok(())
74/// # }
75/// ```
76#[derive(Clone)]
77pub struct AgentKernel {
78    base_url: String,
79    http: reqwest::Client,
80}
81
82impl AgentKernel {
83    /// Create a new builder with defaults resolved from env vars.
84    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    /// Health check. Returns `"ok"`.
94    pub async fn health(&self) -> Result<String> {
95        self.request::<String>(reqwest::Method::GET, "/health", None::<&()>)
96            .await
97    }
98
99    /// Run a command in a temporary sandbox.
100    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    /// List all sandboxes.
113    pub async fn list_sandboxes(&self) -> Result<Vec<SandboxInfo>> {
114        self.request(reqwest::Method::GET, "/sandboxes", None::<&()>)
115            .await
116    }
117
118    /// Create a new sandbox with optional configuration.
119    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    /// Get info about a sandbox.
142    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    /// Get info about a sandbox by UUID.
152    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    /// Remove a sandbox.
162    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    /// Run a command in an existing sandbox.
174    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    /// Create a sandbox and return a guard that removes it on drop.
196    ///
197    /// Use `with_sandbox` for guaranteed cleanup via a closure.
198    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        // Always clean up
215        let _ = self.remove_sandbox(name).await;
216        result
217    }
218
219    /// Create a browser sandbox with Playwright/Chromium pre-installed.
220    ///
221    /// Returns a [`BrowserSession`] you can use to navigate pages, take
222    /// screenshots, and evaluate JavaScript expressions.
223    ///
224    /// # Example
225    ///
226    /// ```no_run
227    /// # async fn example() -> agentkernel_sdk::Result<()> {
228    /// let client = agentkernel_sdk::AgentKernel::builder().build()?;
229    /// let mut browser = client.browser("my-browser", None).await?;
230    /// let page = browser.goto("https://example.com").await?;
231    /// println!("{}", page.title);
232    /// browser.remove().await?;
233    /// # Ok(())
234    /// # }
235    /// ```
236    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        // Install Playwright + Chromium inside the sandbox.
246        self.exec_in_sandbox(name, BROWSER_SETUP_CMD, None).await?;
247
248        Ok(BrowserSession::new(name.to_string(), self.clone()))
249    }
250
251    /// Write multiple files to a sandbox in one request.
252    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    /// Read a file from a sandbox.
267    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    /// Write a file to a sandbox.
277    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    /// Delete a file from a sandbox.
297    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    /// Get audit log entries for a sandbox.
307    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    /// Start a detached (background) command in a sandbox.
317    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    /// Get the status of a detached command.
339    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    /// Get logs from a detached command.
349    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    /// Kill a detached command.
368    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    /// List detached commands in a sandbox.
378    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    /// Run multiple commands in parallel.
388    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    /// List all orchestrations.
395    pub async fn list_orchestrations(&self) -> Result<Vec<Orchestration>> {
396        self.request(reqwest::Method::GET, "/orchestrations", None::<&()>)
397            .await
398    }
399
400    /// Create a new orchestration.
401    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    /// Get a single orchestration by id.
410    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    /// Raise an external event for an orchestration.
420    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    /// Terminate an orchestration.
434    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    /// List orchestration definitions.
449    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    /// Register or update an orchestration definition.
459    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    /// Get an orchestration definition by name.
472    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    /// Delete an orchestration definition by name.
485    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    /// List all objects.
495    pub async fn list_objects(&self) -> Result<Vec<DurableObject>> {
496        self.request(reqwest::Method::GET, "/objects", None::<&()>).await
497    }
498
499    /// Create a new object.
500    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    /// Get a single object by id.
508    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    /// Call a method on a durable object (auto-creates/wakes if needed).
518    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    /// Delete a durable object by id.
535    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    /// Partially update a durable object (storage and/or status).
541    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    /// List all schedules.
555    pub async fn list_schedules(&self) -> Result<Vec<Schedule>> {
556        self.request(reqwest::Method::GET, "/schedules", None::<&()>).await
557    }
558
559    /// Create a new schedule.
560    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    /// Get a single schedule by id.
569    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    /// Delete a schedule by id.
579    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    /// List all durable stores.
589    pub async fn list_stores(&self) -> Result<Vec<DurableStore>> {
590        self.request(reqwest::Method::GET, "/stores", None::<&()>).await
591    }
592
593    /// Create a new durable store.
594    pub async fn create_store(&self, payload: DurableStoreCreateRequest) -> Result<DurableStore> {
595        self.request(reqwest::Method::POST, "/stores", Some(&payload))
596            .await
597    }
598
599    /// Get a durable store by id.
600    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    /// Delete a durable store by id.
606    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    /// Run a read query against a durable store.
616    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    /// Run a write statement against a durable store.
630    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    /// Run a command against a durable store (Redis-style engines).
644    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    /// Extend a sandbox's time-to-live.
658    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    /// List all snapshots.
669    pub async fn list_snapshots(&self) -> Result<Vec<SnapshotMeta>> {
670        self.request(reqwest::Method::GET, "/snapshots", None::<&()>)
671            .await
672    }
673
674    /// Take a snapshot of a sandbox.
675    pub async fn take_snapshot(&self, opts: TakeSnapshotOptions) -> Result<SnapshotMeta> {
676        self.request(reqwest::Method::POST, "/snapshots", Some(&opts))
677            .await
678    }
679
680    /// Get info about a snapshot.
681    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    /// Delete a snapshot.
691    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    /// Restore a sandbox from a snapshot.
703    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    // -- Internal --
713
714    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
746/// Handle to a sandbox within a `with_sandbox` closure.
747///
748/// Owns a clone of the client (cheap — `reqwest::Client` is `Arc`-backed).
749pub struct SandboxHandle {
750    name: String,
751    client: AgentKernel,
752}
753
754impl SandboxHandle {
755    /// The sandbox name.
756    pub fn name(&self) -> &str {
757        &self.name
758    }
759
760    /// Run a command in this sandbox.
761    pub async fn run(&self, command: &[&str]) -> Result<RunOutput> {
762        self.client.exec_in_sandbox(&self.name, command, None).await
763    }
764
765    /// Run a command with options (workdir, env, sudo).
766    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    /// Get sandbox info.
773    pub async fn info(&self) -> Result<SandboxInfo> {
774        self.client.get_sandbox(&self.name).await
775    }
776
777    /// Read a file from this sandbox.
778    pub async fn read_file(&self, path: &str) -> Result<FileReadResponse> {
779        self.client.read_file(&self.name, path).await
780    }
781
782    /// Write a file to this sandbox.
783    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    /// Write multiple files to this sandbox.
795    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    /// Delete a file from this sandbox.
803    pub async fn delete_file(&self, path: &str) -> Result<String> {
804        self.client.delete_file(&self.name, path).await
805    }
806}