use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue, USER_AGENT};
use std::time::Duration;
use crate::browser::{BROWSER_SETUP_CMD, BrowserSession};
use crate::error::{Error, Result, error_from_status};
use crate::types::*;
const SDK_VERSION: &str = env!("CARGO_PKG_VERSION");
const DEFAULT_BASE_URL: &str = "http://localhost:18888";
const DEFAULT_TIMEOUT_SECS: u64 = 30;
pub struct AgentKernelBuilder {
base_url: String,
api_key: Option<String>,
timeout: Duration,
}
impl AgentKernelBuilder {
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
pub fn api_key(mut self, key: impl Into<String>) -> Self {
self.api_key = Some(key.into());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn build(self) -> Result<AgentKernel> {
let mut headers = HeaderMap::new();
headers.insert(
USER_AGENT,
HeaderValue::from_str(&format!("agentkernel-rust-sdk/{SDK_VERSION}")).unwrap(),
);
if let Some(ref key) = self.api_key {
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {key}"))
.map_err(|e| Error::Auth(e.to_string()))?,
);
}
let http = reqwest::Client::builder()
.default_headers(headers)
.timeout(self.timeout)
.build()?;
Ok(AgentKernel {
base_url: self.base_url.trim_end_matches('/').to_string(),
http,
})
}
}
#[derive(Clone)]
pub struct AgentKernel {
base_url: String,
http: reqwest::Client,
}
impl AgentKernel {
pub fn builder() -> AgentKernelBuilder {
AgentKernelBuilder {
base_url: std::env::var("AGENTKERNEL_BASE_URL")
.unwrap_or_else(|_| DEFAULT_BASE_URL.to_string()),
api_key: std::env::var("AGENTKERNEL_API_KEY").ok(),
timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS),
}
}
pub async fn health(&self) -> Result<String> {
self.request::<String>(reqwest::Method::GET, "/health", None::<&()>)
.await
}
pub async fn run(&self, command: &[&str], opts: Option<RunOptions>) -> Result<RunOutput> {
let opts = opts.unwrap_or_default();
let body = RunRequest {
command: command.iter().map(|s| s.to_string()).collect(),
image: opts.image,
profile: opts.profile,
fast: opts.fast.unwrap_or(true),
};
self.request(reqwest::Method::POST, "/run", Some(&body))
.await
}
pub async fn list_sandboxes(&self) -> Result<Vec<SandboxInfo>> {
self.request(reqwest::Method::GET, "/sandboxes", None::<&()>)
.await
}
pub async fn create_sandbox(
&self,
name: &str,
opts: Option<CreateSandboxOptions>,
) -> Result<SandboxInfo> {
let opts = opts.unwrap_or_default();
let body = CreateRequest {
name: name.to_string(),
image: opts.image,
vcpus: opts.vcpus,
memory_mb: opts.memory_mb,
profile: opts.profile,
source_url: opts.source_url,
source_ref: opts.source_ref,
volumes: opts.volumes,
secrets: opts.secrets,
secret_files: opts.secret_files,
};
self.request(reqwest::Method::POST, "/sandboxes", Some(&body))
.await
}
pub async fn get_sandbox(&self, name: &str) -> Result<SandboxInfo> {
self.request(
reqwest::Method::GET,
&format!("/sandboxes/{name}"),
None::<&()>,
)
.await
}
pub async fn get_sandbox_by_uuid(&self, uuid: &str) -> Result<SandboxInfo> {
self.request(
reqwest::Method::GET,
&format!("/sandboxes/by-uuid/{uuid}"),
None::<&()>,
)
.await
}
pub async fn remove_sandbox(&self, name: &str) -> Result<()> {
let _: String = self
.request(
reqwest::Method::DELETE,
&format!("/sandboxes/{name}"),
None::<&()>,
)
.await?;
Ok(())
}
pub async fn exec_in_sandbox(
&self,
name: &str,
command: &[&str],
opts: Option<ExecOptions>,
) -> Result<RunOutput> {
let opts = opts.unwrap_or_default();
let body = ExecRequest {
command: command.iter().map(|s| s.to_string()).collect(),
env: opts.env,
workdir: opts.workdir,
sudo: opts.sudo,
};
self.request(
reqwest::Method::POST,
&format!("/sandboxes/{name}/exec"),
Some(&body),
)
.await
}
pub async fn with_sandbox<F, Fut, T>(
&self,
name: &str,
opts: Option<CreateSandboxOptions>,
f: F,
) -> Result<T>
where
F: FnOnce(SandboxHandle) -> Fut,
Fut: std::future::Future<Output = Result<T>>,
{
self.create_sandbox(name, opts).await?;
let handle = SandboxHandle {
name: name.to_string(),
client: self.clone(),
};
let result = f(handle).await;
let _ = self.remove_sandbox(name).await;
result
}
pub async fn browser(&self, name: &str, memory_mb: Option<u64>) -> Result<BrowserSession> {
let opts = CreateSandboxOptions {
image: Some("python:3.12-slim".to_string()),
memory_mb: Some(memory_mb.unwrap_or(2048)),
profile: Some(SecurityProfile::Moderate),
..Default::default()
};
self.create_sandbox(name, Some(opts)).await?;
self.exec_in_sandbox(name, BROWSER_SETUP_CMD, None).await?;
Ok(BrowserSession::new(name.to_string(), self.clone()))
}
pub async fn write_files(
&self,
name: &str,
files: std::collections::HashMap<String, String>,
) -> Result<BatchFileWriteResponse> {
let body = BatchFileWriteRequest { files };
self.request(
reqwest::Method::POST,
&format!("/sandboxes/{name}/files"),
Some(&body),
)
.await
}
pub async fn read_file(&self, name: &str, path: &str) -> Result<FileReadResponse> {
self.request(
reqwest::Method::GET,
&format!("/sandboxes/{name}/files/{path}"),
None::<&()>,
)
.await
}
pub async fn write_file(
&self,
name: &str,
path: &str,
content: &str,
encoding: Option<&str>,
) -> Result<String> {
let body = FileWriteRequest {
content: content.to_string(),
encoding: encoding.map(String::from),
};
self.request(
reqwest::Method::PUT,
&format!("/sandboxes/{name}/files/{path}"),
Some(&body),
)
.await
}
pub async fn delete_file(&self, name: &str, path: &str) -> Result<String> {
self.request(
reqwest::Method::DELETE,
&format!("/sandboxes/{name}/files/{path}"),
None::<&()>,
)
.await
}
pub async fn get_sandbox_logs(&self, name: &str) -> Result<Vec<serde_json::Value>> {
self.request(
reqwest::Method::GET,
&format!("/sandboxes/{name}/logs"),
None::<&()>,
)
.await
}
pub async fn exec_detached(
&self,
name: &str,
command: &[&str],
opts: Option<ExecOptions>,
) -> Result<DetachedCommand> {
let opts = opts.unwrap_or_default();
let body = ExecRequest {
command: command.iter().map(|s| s.to_string()).collect(),
env: opts.env,
workdir: opts.workdir,
sudo: opts.sudo,
};
self.request(
reqwest::Method::POST,
&format!("/sandboxes/{name}/exec/detach"),
Some(&body),
)
.await
}
pub async fn detached_status(&self, name: &str, cmd_id: &str) -> Result<DetachedCommand> {
self.request(
reqwest::Method::GET,
&format!("/sandboxes/{name}/exec/detached/{cmd_id}"),
None::<&()>,
)
.await
}
pub async fn detached_logs(
&self,
name: &str,
cmd_id: &str,
stream: Option<&str>,
) -> Result<DetachedLogsResponse> {
let query = match stream {
Some(s) => format!("?stream={s}"),
None => String::new(),
};
self.request(
reqwest::Method::GET,
&format!("/sandboxes/{name}/exec/detached/{cmd_id}/logs{query}"),
None::<&()>,
)
.await
}
pub async fn detached_kill(&self, name: &str, cmd_id: &str) -> Result<String> {
self.request(
reqwest::Method::DELETE,
&format!("/sandboxes/{name}/exec/detached/{cmd_id}"),
None::<&()>,
)
.await
}
pub async fn detached_list(&self, name: &str) -> Result<Vec<DetachedCommand>> {
self.request(
reqwest::Method::GET,
&format!("/sandboxes/{name}/exec/detached"),
None::<&()>,
)
.await
}
pub async fn batch_run(&self, commands: Vec<BatchCommand>) -> Result<BatchRunResponse> {
let body = BatchRunRequest { commands };
self.request(reqwest::Method::POST, "/batch/run", Some(&body))
.await
}
pub async fn list_orchestrations(&self) -> Result<Vec<Orchestration>> {
self.request(reqwest::Method::GET, "/orchestrations", None::<&()>)
.await
}
pub async fn create_orchestration(
&self,
payload: OrchestrationCreateRequest,
) -> Result<Orchestration> {
self.request(reqwest::Method::POST, "/orchestrations", Some(&payload))
.await
}
pub async fn get_orchestration(&self, id: &str) -> Result<Orchestration> {
self.request(
reqwest::Method::GET,
&format!("/orchestrations/{id}"),
None::<&()>,
)
.await
}
pub async fn signal_orchestration(
&self,
id: &str,
payload: serde_json::Value,
) -> Result<Orchestration> {
self.request(
reqwest::Method::POST,
&format!("/orchestrations/{id}/events"),
Some(&payload),
)
.await
}
pub async fn terminate_orchestration(
&self,
id: &str,
payload: Option<serde_json::Value>,
) -> Result<Orchestration> {
let body = payload.unwrap_or_else(|| serde_json::json!({}));
self.request(
reqwest::Method::POST,
&format!("/orchestrations/{id}/terminate"),
Some(&body),
)
.await
}
pub async fn list_orchestration_definitions(&self) -> Result<Vec<OrchestrationDefinition>> {
self.request(
reqwest::Method::GET,
"/orchestrations/definitions",
None::<&()>,
)
.await
}
pub async fn upsert_orchestration_definition(
&self,
payload: OrchestrationDefinition,
) -> Result<OrchestrationDefinition> {
self.request(
reqwest::Method::POST,
"/orchestrations/definitions",
Some(&payload),
)
.await
}
pub async fn get_orchestration_definition(
&self,
name: &str,
) -> Result<OrchestrationDefinition> {
self.request(
reqwest::Method::GET,
&format!("/orchestrations/definitions/{name}"),
None::<&()>,
)
.await
}
pub async fn delete_orchestration_definition(&self, name: &str) -> Result<String> {
self.request(
reqwest::Method::DELETE,
&format!("/orchestrations/definitions/{name}"),
None::<&()>,
)
.await
}
pub async fn list_objects(&self) -> Result<Vec<DurableObject>> {
self.request(reqwest::Method::GET, "/objects", None::<&()>).await
}
pub async fn create_object(
&self,
payload: DurableObjectCreateRequest,
) -> Result<DurableObject> {
self.request(reqwest::Method::POST, "/objects", Some(&payload)).await
}
pub async fn get_object(&self, id: &str) -> Result<DurableObject> {
self.request(
reqwest::Method::GET,
&format!("/objects/{id}"),
None::<&()>,
)
.await
}
pub async fn call_object(
&self,
class: &str,
object_id: &str,
method: &str,
args: serde_json::Value,
) -> Result<serde_json::Value> {
let url = format!(
"{}/objects/{}/{}/call/{}",
self.base_url, class, object_id, method
);
let resp = self.http.post(&url).json(&args).send().await?;
let result = resp.json().await?;
Ok(result)
}
pub async fn delete_object(&self, id: &str) -> Result<String> {
self.request(reqwest::Method::DELETE, &format!("/objects/{id}"), None::<&()>)
.await
}
pub async fn patch_object(
&self,
id: &str,
payload: serde_json::Value,
) -> Result<DurableObject> {
self.request(
reqwest::Method::PATCH,
&format!("/objects/{id}"),
Some(&payload),
)
.await
}
pub async fn list_schedules(&self) -> Result<Vec<Schedule>> {
self.request(reqwest::Method::GET, "/schedules", None::<&()>).await
}
pub async fn create_schedule(
&self,
payload: ScheduleCreateRequest,
) -> Result<Schedule> {
self.request(reqwest::Method::POST, "/schedules", Some(&payload))
.await
}
pub async fn get_schedule(&self, id: &str) -> Result<Schedule> {
self.request(
reqwest::Method::GET,
&format!("/schedules/{id}"),
None::<&()>,
)
.await
}
pub async fn delete_schedule(&self, id: &str) -> Result<String> {
self.request(
reqwest::Method::DELETE,
&format!("/schedules/{id}"),
None::<&()>,
)
.await
}
pub async fn list_stores(&self) -> Result<Vec<DurableStore>> {
self.request(reqwest::Method::GET, "/stores", None::<&()>).await
}
pub async fn create_store(&self, payload: DurableStoreCreateRequest) -> Result<DurableStore> {
self.request(reqwest::Method::POST, "/stores", Some(&payload))
.await
}
pub async fn get_store(&self, id: &str) -> Result<DurableStore> {
self.request(reqwest::Method::GET, &format!("/stores/{id}"), None::<&()>)
.await
}
pub async fn delete_store(&self, id: &str) -> Result<String> {
self.request(
reqwest::Method::DELETE,
&format!("/stores/{id}"),
None::<&()>,
)
.await
}
pub async fn query_store(
&self,
id: &str,
payload: serde_json::Value,
) -> Result<DurableStoreQueryResult> {
self.request(
reqwest::Method::POST,
&format!("/stores/{id}/query"),
Some(&payload),
)
.await
}
pub async fn execute_store(
&self,
id: &str,
payload: serde_json::Value,
) -> Result<DurableStoreExecuteResult> {
self.request(
reqwest::Method::POST,
&format!("/stores/{id}/execute"),
Some(&payload),
)
.await
}
pub async fn command_store(
&self,
id: &str,
payload: serde_json::Value,
) -> Result<DurableStoreCommandResult> {
self.request(
reqwest::Method::POST,
&format!("/stores/{id}/command"),
Some(&payload),
)
.await
}
pub async fn extend_ttl(&self, name: &str, by: &str) -> Result<ExtendTtlResponse> {
let body = ExtendTtlRequest { by: by.to_string() };
self.request(
reqwest::Method::POST,
&format!("/sandboxes/{name}/extend"),
Some(&body),
)
.await
}
pub async fn list_snapshots(&self) -> Result<Vec<SnapshotMeta>> {
self.request(reqwest::Method::GET, "/snapshots", None::<&()>)
.await
}
pub async fn take_snapshot(&self, opts: TakeSnapshotOptions) -> Result<SnapshotMeta> {
self.request(reqwest::Method::POST, "/snapshots", Some(&opts))
.await
}
pub async fn get_snapshot(&self, name: &str) -> Result<SnapshotMeta> {
self.request(
reqwest::Method::GET,
&format!("/snapshots/{name}"),
None::<&()>,
)
.await
}
pub async fn delete_snapshot(&self, name: &str) -> Result<()> {
let _: String = self
.request(
reqwest::Method::DELETE,
&format!("/snapshots/{name}"),
None::<&()>,
)
.await?;
Ok(())
}
pub async fn restore_snapshot(&self, name: &str) -> Result<SandboxInfo> {
self.request(
reqwest::Method::POST,
&format!("/snapshots/{name}/restore"),
None::<&()>,
)
.await
}
async fn request<T: serde::de::DeserializeOwned>(
&self,
method: reqwest::Method,
path: &str,
body: Option<&(impl serde::Serialize + ?Sized)>,
) -> Result<T> {
let url = format!("{}{path}", self.base_url);
let mut req = self.http.request(method, &url);
if let Some(b) = body {
req = req.header(CONTENT_TYPE, "application/json").json(b);
}
let response = req.send().await?;
let status = response.status().as_u16();
let text = response.text().await?;
if status >= 400 {
return Err(error_from_status(status, &text));
}
let parsed: ApiResponse<T> = serde_json::from_str(&text)?;
if !parsed.success {
return Err(Error::Server(
parsed.error.unwrap_or_else(|| "Unknown error".to_string()),
));
}
parsed
.data
.ok_or_else(|| Error::Server("Missing data field".to_string()))
}
}
pub struct SandboxHandle {
name: String,
client: AgentKernel,
}
impl SandboxHandle {
pub fn name(&self) -> &str {
&self.name
}
pub async fn run(&self, command: &[&str]) -> Result<RunOutput> {
self.client.exec_in_sandbox(&self.name, command, None).await
}
pub async fn run_with_options(&self, command: &[&str], opts: ExecOptions) -> Result<RunOutput> {
self.client
.exec_in_sandbox(&self.name, command, Some(opts))
.await
}
pub async fn info(&self) -> Result<SandboxInfo> {
self.client.get_sandbox(&self.name).await
}
pub async fn read_file(&self, path: &str) -> Result<FileReadResponse> {
self.client.read_file(&self.name, path).await
}
pub async fn write_file(
&self,
path: &str,
content: &str,
encoding: Option<&str>,
) -> Result<String> {
self.client
.write_file(&self.name, path, content, encoding)
.await
}
pub async fn write_files(
&self,
files: std::collections::HashMap<String, String>,
) -> Result<BatchFileWriteResponse> {
self.client.write_files(&self.name, files).await
}
pub async fn delete_file(&self, path: &str) -> Result<String> {
self.client.delete_file(&self.name, path).await
}
}