agentkernel_sdk/
client.rs1use 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(&self, name: &str, image: Option<&str>) -> Result<SandboxInfo> {
119 let body = CreateRequest {
120 name: name.to_string(),
121 image: image.map(String::from),
122 };
123 self.request(reqwest::Method::POST, "/sandboxes", Some(&body))
124 .await
125 }
126
127 pub async fn get_sandbox(&self, name: &str) -> Result<SandboxInfo> {
129 self.request(
130 reqwest::Method::GET,
131 &format!("/sandboxes/{name}"),
132 None::<&()>,
133 )
134 .await
135 }
136
137 pub async fn remove_sandbox(&self, name: &str) -> Result<()> {
139 let _: String = self
140 .request(
141 reqwest::Method::DELETE,
142 &format!("/sandboxes/{name}"),
143 None::<&()>,
144 )
145 .await?;
146 Ok(())
147 }
148
149 pub async fn exec_in_sandbox(&self, name: &str, command: &[&str]) -> Result<RunOutput> {
151 let body = ExecRequest {
152 command: command.iter().map(|s| s.to_string()).collect(),
153 };
154 self.request(
155 reqwest::Method::POST,
156 &format!("/sandboxes/{name}/exec"),
157 Some(&body),
158 )
159 .await
160 }
161
162 pub async fn with_sandbox<F, Fut, T>(&self, name: &str, image: Option<&str>, f: F) -> Result<T>
166 where
167 F: FnOnce(SandboxHandle) -> Fut,
168 Fut: std::future::Future<Output = Result<T>>,
169 {
170 self.create_sandbox(name, image).await?;
171 let handle = SandboxHandle {
172 name: name.to_string(),
173 client: self.clone(),
174 };
175 let result = f(handle).await;
176 let _ = self.remove_sandbox(name).await;
178 result
179 }
180
181 async fn request<T: serde::de::DeserializeOwned>(
184 &self,
185 method: reqwest::Method,
186 path: &str,
187 body: Option<&(impl serde::Serialize + ?Sized)>,
188 ) -> Result<T> {
189 let url = format!("{}{path}", self.base_url);
190 let mut req = self.http.request(method, &url);
191 if let Some(b) = body {
192 req = req.header(CONTENT_TYPE, "application/json").json(b);
193 }
194
195 let response = req.send().await?;
196 let status = response.status().as_u16();
197 let text = response.text().await?;
198
199 if status >= 400 {
200 return Err(error_from_status(status, &text));
201 }
202
203 let parsed: ApiResponse<T> = serde_json::from_str(&text)?;
204 if !parsed.success {
205 return Err(Error::Server(
206 parsed.error.unwrap_or_else(|| "Unknown error".to_string()),
207 ));
208 }
209 parsed
210 .data
211 .ok_or_else(|| Error::Server("Missing data field".to_string()))
212 }
213}
214
215pub struct SandboxHandle {
219 name: String,
220 client: AgentKernel,
221}
222
223impl SandboxHandle {
224 pub fn name(&self) -> &str {
226 &self.name
227 }
228
229 pub async fn run(&self, command: &[&str]) -> Result<RunOutput> {
231 self.client.exec_in_sandbox(&self.name, command).await
232 }
233
234 pub async fn info(&self) -> Result<SandboxInfo> {
236 self.client.get_sandbox(&self.name).await
237 }
238}