1use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE, USER_AGENT};
2use std::time::Duration;
3
4use crate::error::{error_from_status, Error, Result};
5use crate::types::*;
6
7const SDK_VERSION: &str = env!("CARGO_PKG_VERSION");
8const DEFAULT_BASE_URL: &str = "http://localhost:18888";
9const DEFAULT_TIMEOUT_SECS: u64 = 30;
10
11pub struct AgentKernelBuilder {
13 base_url: String,
14 api_key: Option<String>,
15 timeout: Duration,
16}
17
18impl AgentKernelBuilder {
19 pub fn base_url(mut self, url: impl Into<String>) -> Self {
21 self.base_url = url.into();
22 self
23 }
24
25 pub fn api_key(mut self, key: impl Into<String>) -> Self {
27 self.api_key = Some(key.into());
28 self
29 }
30
31 pub fn timeout(mut self, timeout: Duration) -> Self {
33 self.timeout = timeout;
34 self
35 }
36
37 pub fn build(self) -> Result<AgentKernel> {
39 let mut headers = HeaderMap::new();
40 headers.insert(
41 USER_AGENT,
42 HeaderValue::from_str(&format!("agentkernel-rust-sdk/{SDK_VERSION}")).unwrap(),
43 );
44 if let Some(ref key) = self.api_key {
45 headers.insert(
46 AUTHORIZATION,
47 HeaderValue::from_str(&format!("Bearer {key}"))
48 .map_err(|e| Error::Auth(e.to_string()))?,
49 );
50 }
51
52 let http = reqwest::Client::builder()
53 .default_headers(headers)
54 .timeout(self.timeout)
55 .build()?;
56
57 Ok(AgentKernel {
58 base_url: self.base_url.trim_end_matches('/').to_string(),
59 http,
60 })
61 }
62}
63
64#[derive(Clone)]
76pub struct AgentKernel {
77 base_url: String,
78 http: reqwest::Client,
79}
80
81impl AgentKernel {
82 pub fn builder() -> AgentKernelBuilder {
84 AgentKernelBuilder {
85 base_url: std::env::var("AGENTKERNEL_BASE_URL")
86 .unwrap_or_else(|_| DEFAULT_BASE_URL.to_string()),
87 api_key: std::env::var("AGENTKERNEL_API_KEY").ok(),
88 timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS),
89 }
90 }
91
92 pub async fn health(&self) -> Result<String> {
94 self.request::<String>(reqwest::Method::GET, "/health", None::<&()>)
95 .await
96 }
97
98 pub async fn run(&self, command: &[&str], opts: Option<RunOptions>) -> Result<RunOutput> {
100 let opts = opts.unwrap_or_default();
101 let body = RunRequest {
102 command: command.iter().map(|s| s.to_string()).collect(),
103 image: opts.image,
104 profile: opts.profile,
105 fast: opts.fast.unwrap_or(true),
106 };
107 self.request(reqwest::Method::POST, "/run", Some(&body))
108 .await
109 }
110
111 pub async fn list_sandboxes(&self) -> Result<Vec<SandboxInfo>> {
113 self.request(reqwest::Method::GET, "/sandboxes", None::<&()>)
114 .await
115 }
116
117 pub async fn create_sandbox(
119 &self,
120 name: &str,
121 image: Option<&str>,
122 vcpus: Option<u32>,
123 memory_mb: Option<u64>,
124 profile: Option<SecurityProfile>,
125 ) -> Result<SandboxInfo> {
126 let body = CreateRequest {
127 name: name.to_string(),
128 image: image.map(String::from),
129 vcpus,
130 memory_mb,
131 profile,
132 };
133 self.request(reqwest::Method::POST, "/sandboxes", Some(&body))
134 .await
135 }
136
137 pub async fn get_sandbox(&self, name: &str) -> Result<SandboxInfo> {
139 self.request(
140 reqwest::Method::GET,
141 &format!("/sandboxes/{name}"),
142 None::<&()>,
143 )
144 .await
145 }
146
147 pub async fn remove_sandbox(&self, name: &str) -> Result<()> {
149 let _: String = self
150 .request(
151 reqwest::Method::DELETE,
152 &format!("/sandboxes/{name}"),
153 None::<&()>,
154 )
155 .await?;
156 Ok(())
157 }
158
159 pub async fn exec_in_sandbox(&self, name: &str, command: &[&str]) -> Result<RunOutput> {
161 let body = ExecRequest {
162 command: command.iter().map(|s| s.to_string()).collect(),
163 };
164 self.request(
165 reqwest::Method::POST,
166 &format!("/sandboxes/{name}/exec"),
167 Some(&body),
168 )
169 .await
170 }
171
172 pub async fn with_sandbox<F, Fut, T>(&self, name: &str, image: Option<&str>, f: F) -> Result<T>
176 where
177 F: FnOnce(SandboxHandle) -> Fut,
178 Fut: std::future::Future<Output = Result<T>>,
179 {
180 self.create_sandbox(name, image, None, None, None).await?;
181 let handle = SandboxHandle {
182 name: name.to_string(),
183 client: self.clone(),
184 };
185 let result = f(handle).await;
186 let _ = self.remove_sandbox(name).await;
188 result
189 }
190
191 pub async fn read_file(&self, name: &str, path: &str) -> Result<FileReadResponse> {
193 self.request(
194 reqwest::Method::GET,
195 &format!("/sandboxes/{name}/files/{path}"),
196 None::<&()>,
197 )
198 .await
199 }
200
201 pub async fn write_file(
203 &self,
204 name: &str,
205 path: &str,
206 content: &str,
207 encoding: Option<&str>,
208 ) -> Result<String> {
209 let body = FileWriteRequest {
210 content: content.to_string(),
211 encoding: encoding.map(String::from),
212 };
213 self.request(
214 reqwest::Method::PUT,
215 &format!("/sandboxes/{name}/files/{path}"),
216 Some(&body),
217 )
218 .await
219 }
220
221 pub async fn delete_file(&self, name: &str, path: &str) -> Result<String> {
223 self.request(
224 reqwest::Method::DELETE,
225 &format!("/sandboxes/{name}/files/{path}"),
226 None::<&()>,
227 )
228 .await
229 }
230
231 pub async fn get_sandbox_logs(&self, name: &str) -> Result<Vec<serde_json::Value>> {
233 self.request(
234 reqwest::Method::GET,
235 &format!("/sandboxes/{name}/logs"),
236 None::<&()>,
237 )
238 .await
239 }
240
241 pub async fn batch_run(&self, commands: Vec<BatchCommand>) -> Result<BatchRunResponse> {
243 let body = BatchRunRequest { commands };
244 self.request(reqwest::Method::POST, "/batch/run", Some(&body))
245 .await
246 }
247
248 async fn request<T: serde::de::DeserializeOwned>(
251 &self,
252 method: reqwest::Method,
253 path: &str,
254 body: Option<&(impl serde::Serialize + ?Sized)>,
255 ) -> Result<T> {
256 let url = format!("{}{path}", self.base_url);
257 let mut req = self.http.request(method, &url);
258 if let Some(b) = body {
259 req = req.header(CONTENT_TYPE, "application/json").json(b);
260 }
261
262 let response = req.send().await?;
263 let status = response.status().as_u16();
264 let text = response.text().await?;
265
266 if status >= 400 {
267 return Err(error_from_status(status, &text));
268 }
269
270 let parsed: ApiResponse<T> = serde_json::from_str(&text)?;
271 if !parsed.success {
272 return Err(Error::Server(
273 parsed.error.unwrap_or_else(|| "Unknown error".to_string()),
274 ));
275 }
276 parsed
277 .data
278 .ok_or_else(|| Error::Server("Missing data field".to_string()))
279 }
280}
281
282pub struct SandboxHandle {
286 name: String,
287 client: AgentKernel,
288}
289
290impl SandboxHandle {
291 pub fn name(&self) -> &str {
293 &self.name
294 }
295
296 pub async fn run(&self, command: &[&str]) -> Result<RunOutput> {
298 self.client.exec_in_sandbox(&self.name, command).await
299 }
300
301 pub async fn info(&self) -> Result<SandboxInfo> {
303 self.client.get_sandbox(&self.name).await
304 }
305
306 pub async fn read_file(&self, path: &str) -> Result<FileReadResponse> {
308 self.client.read_file(&self.name, path).await
309 }
310
311 pub async fn write_file(&self, path: &str, content: &str, encoding: Option<&str>) -> Result<String> {
313 self.client.write_file(&self.name, path, content, encoding).await
314 }
315
316 pub async fn delete_file(&self, path: &str) -> Result<String> {
318 self.client.delete_file(&self.name, path).await
319 }
320}