Skip to main content

agentkernel_sdk/
client.rs

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
11/// Builder for constructing an [`AgentKernel`] client.
12pub struct AgentKernelBuilder {
13    base_url: String,
14    api_key: Option<String>,
15    timeout: Duration,
16}
17
18impl AgentKernelBuilder {
19    /// Set the base URL.
20    pub fn base_url(mut self, url: impl Into<String>) -> Self {
21        self.base_url = url.into();
22        self
23    }
24
25    /// Set the API key for Bearer authentication.
26    pub fn api_key(mut self, key: impl Into<String>) -> Self {
27        self.api_key = Some(key.into());
28        self
29    }
30
31    /// Set the request timeout.
32    pub fn timeout(mut self, timeout: Duration) -> Self {
33        self.timeout = timeout;
34        self
35    }
36
37    /// Build the client.
38    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/// Client for the agentkernel HTTP API.
65///
66/// # Example
67/// ```no_run
68/// # async fn example() -> agentkernel_sdk::Result<()> {
69/// let client = agentkernel_sdk::AgentKernel::builder().build()?;
70/// let output = client.run(&["echo", "hello"], None).await?;
71/// println!("{}", output.output);
72/// # Ok(())
73/// # }
74/// ```
75#[derive(Clone)]
76pub struct AgentKernel {
77    base_url: String,
78    http: reqwest::Client,
79}
80
81impl AgentKernel {
82    /// Create a new builder with defaults resolved from env vars.
83    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    /// Health check. Returns `"ok"`.
93    pub async fn health(&self) -> Result<String> {
94        self.request::<String>(reqwest::Method::GET, "/health", None::<&()>)
95            .await
96    }
97
98    /// Run a command in a temporary sandbox.
99    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    /// List all sandboxes.
112    pub async fn list_sandboxes(&self) -> Result<Vec<SandboxInfo>> {
113        self.request(reqwest::Method::GET, "/sandboxes", None::<&()>)
114            .await
115    }
116
117    /// Create a new sandbox.
118    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    /// Get info about a sandbox.
128    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    /// Remove a sandbox.
138    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    /// Run a command in an existing sandbox.
150    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    /// Create a sandbox and return a guard that removes it on drop.
163    ///
164    /// Use `with_sandbox` for guaranteed cleanup via a closure.
165    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        // Always clean up
177        let _ = self.remove_sandbox(name).await;
178        result
179    }
180
181    // -- Internal --
182
183    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
215/// Handle to a sandbox within a `with_sandbox` closure.
216///
217/// Owns a clone of the client (cheap — `reqwest::Client` is `Arc`-backed).
218pub struct SandboxHandle {
219    name: String,
220    client: AgentKernel,
221}
222
223impl SandboxHandle {
224    /// The sandbox name.
225    pub fn name(&self) -> &str {
226        &self.name
227    }
228
229    /// Run a command in this sandbox.
230    pub async fn run(&self, command: &[&str]) -> Result<RunOutput> {
231        self.client.exec_in_sandbox(&self.name, command).await
232    }
233
234    /// Get sandbox info.
235    pub async fn info(&self) -> Result<SandboxInfo> {
236        self.client.get_sandbox(&self.name).await
237    }
238}