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    /// Discover server-supported and ready sandbox backends.
119    pub async fn get_backends(&self) -> Result<BackendDiscovery> {
120        self.request(reqwest::Method::GET, "/backends", None::<&()>)
121            .await
122    }
123
124    /// Create a new sandbox with optional configuration.
125    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    /// Get info about a sandbox.
149    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    /// Get info about a sandbox by UUID.
159    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    /// Remove a sandbox.
169    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    /// Run a command in an existing sandbox.
181    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    /// Create a sandbox and return a guard that removes it on drop.
203    ///
204    /// Use `with_sandbox` for guaranteed cleanup via a closure.
205    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        // Always clean up
222        let _ = self.remove_sandbox(name).await;
223        result
224    }
225
226    /// Create a browser sandbox with Playwright/Chromium pre-installed.
227    ///
228    /// Returns a [`BrowserSession`] you can use to navigate pages, take
229    /// screenshots, and evaluate JavaScript expressions.
230    ///
231    /// # Example
232    ///
233    /// ```no_run
234    /// # async fn example() -> agentkernel_sdk::Result<()> {
235    /// let client = agentkernel_sdk::AgentKernel::builder().build()?;
236    /// let mut browser = client.browser("my-browser", None).await?;
237    /// let page = browser.goto("https://example.com").await?;
238    /// println!("{}", page.title);
239    /// browser.remove().await?;
240    /// # Ok(())
241    /// # }
242    /// ```
243    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        // Install Playwright + Chromium inside the sandbox.
253        self.exec_in_sandbox(name, BROWSER_SETUP_CMD, None).await?;
254
255        Ok(BrowserSession::new(name.to_string(), self.clone()))
256    }
257
258    /// Write multiple files to a sandbox in one request.
259    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    /// Read a file from a sandbox.
274    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    /// Write a file to a sandbox.
284    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    /// Delete a file from a sandbox.
304    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    /// Get audit log entries for a sandbox.
314    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    /// Start a detached (background) command in a sandbox.
324    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    /// Get the status of a detached command.
346    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    /// Get logs from a detached command.
356    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    /// Kill a detached command.
375    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    /// List detached commands in a sandbox.
385    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    /// Run multiple commands in parallel.
395    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    /// List all orchestrations.
402    pub async fn list_orchestrations(&self) -> Result<Vec<Orchestration>> {
403        self.request(reqwest::Method::GET, "/orchestrations", None::<&()>)
404            .await
405    }
406
407    /// Create a new orchestration.
408    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    /// Get a single orchestration by id.
417    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    /// Raise an external event for an orchestration.
427    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    /// Terminate an orchestration.
441    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    /// List orchestration definitions.
456    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    /// Register or update an orchestration definition.
466    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    /// Get an orchestration definition by name.
479    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    /// Delete an orchestration definition by name.
492    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    /// List all objects.
502    pub async fn list_objects(&self) -> Result<Vec<DurableObject>> {
503        self.request(reqwest::Method::GET, "/objects", None::<&()>)
504            .await
505    }
506
507    /// Create a new object.
508    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    /// Get a single object by id.
517    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    /// Call a method on a durable object (auto-creates/wakes if needed).
523    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    /// Delete a durable object by id.
540    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    /// Partially update a durable object (storage and/or status).
550    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    /// List all schedules.
564    pub async fn list_schedules(&self) -> Result<Vec<Schedule>> {
565        self.request(reqwest::Method::GET, "/schedules", None::<&()>)
566            .await
567    }
568
569    /// Create a new schedule.
570    pub async fn create_schedule(&self, payload: ScheduleCreateRequest) -> Result<Schedule> {
571        self.request(reqwest::Method::POST, "/schedules", Some(&payload))
572            .await
573    }
574
575    /// Get a single schedule by id.
576    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    /// Delete a schedule by id.
586    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    /// List all durable stores.
596    pub async fn list_stores(&self) -> Result<Vec<DurableStore>> {
597        self.request(reqwest::Method::GET, "/stores", None::<&()>)
598            .await
599    }
600
601    /// Create a new durable store.
602    pub async fn create_store(&self, payload: DurableStoreCreateRequest) -> Result<DurableStore> {
603        self.request(reqwest::Method::POST, "/stores", Some(&payload))
604            .await
605    }
606
607    /// Get a durable store by id.
608    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    /// Delete a durable store by id.
614    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    /// Run a read query against a durable store.
624    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    /// Run a write statement against a durable store.
638    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    /// Run a command against a durable store (Redis-style engines).
652    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    /// Extend a sandbox's time-to-live.
666    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    /// List all snapshots.
677    pub async fn list_snapshots(&self) -> Result<Vec<SnapshotMeta>> {
678        self.request(reqwest::Method::GET, "/snapshots", None::<&()>)
679            .await
680    }
681
682    /// Take a snapshot of a sandbox.
683    pub async fn take_snapshot(&self, opts: TakeSnapshotOptions) -> Result<SnapshotMeta> {
684        self.request(reqwest::Method::POST, "/snapshots", Some(&opts))
685            .await
686    }
687
688    /// Get info about a snapshot.
689    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    /// Delete a snapshot.
699    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    /// Restore a sandbox from a snapshot.
711    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    // -- Internal --
721
722    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
754/// Handle to a sandbox within a `with_sandbox` closure.
755///
756/// Owns a clone of the client (cheap — `reqwest::Client` is `Arc`-backed).
757pub struct SandboxHandle {
758    name: String,
759    client: AgentKernel,
760}
761
762impl SandboxHandle {
763    /// The sandbox name.
764    pub fn name(&self) -> &str {
765        &self.name
766    }
767
768    /// Run a command in this sandbox.
769    pub async fn run(&self, command: &[&str]) -> Result<RunOutput> {
770        self.client.exec_in_sandbox(&self.name, command, None).await
771    }
772
773    /// Run a command with options (workdir, env, sudo).
774    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    /// Get sandbox info.
781    pub async fn info(&self) -> Result<SandboxInfo> {
782        self.client.get_sandbox(&self.name).await
783    }
784
785    /// Read a file from this sandbox.
786    pub async fn read_file(&self, path: &str) -> Result<FileReadResponse> {
787        self.client.read_file(&self.name, path).await
788    }
789
790    /// Write a file to this sandbox.
791    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    /// Write multiple files to this sandbox.
803    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    /// Delete a file from this sandbox.
811    pub async fn delete_file(&self, path: &str) -> Result<String> {
812        self.client.delete_file(&self.name, path).await
813    }
814}