#![allow(unsafe_code)]
#![allow(missing_docs)]
#![deny(clippy::all)]
#![deny(unreachable_pub)]
#![deny(clippy::unwrap_used)]
#![cfg_attr(test, allow(clippy::unwrap_used))]
use astrid_sys::*;
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use thiserror::Error;
pub mod types {
use serde::{Deserialize, Serialize};
pub use astrid_types::ipc;
pub use astrid_types::kernel;
pub use astrid_types::llm;
pub use astrid_types::ipc::{
IpcMessage, IpcPayload, OnboardingField, OnboardingFieldType, SelectionOption,
};
pub use astrid_types::kernel::{
CapsuleMetadataEntry, CommandInfo, KernelRequest, KernelResponse, LlmProviderInfo,
SYSTEM_SESSION_UUID,
};
pub use astrid_types::llm::{
ContentPart, LlmResponse, LlmToolDefinition, Message, MessageContent, MessageRole,
StopReason, StreamEvent, ToolCall, ToolCallResult, Usage,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallerContext {
pub session_id: Option<String>,
pub user_id: Option<String>,
}
}
pub use borsh;
pub use serde;
pub use serde_json;
#[doc(hidden)]
pub use extism_pdk;
#[doc(hidden)]
pub use schemars;
#[derive(Error, Debug)]
pub enum SysError {
#[error("Host function call failed: {0}")]
HostError(#[from] extism_pdk::Error),
#[error("JSON serialization error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("MessagePack serialization error: {0}")]
MsgPackEncodeError(#[from] rmp_serde::encode::Error),
#[error("MessagePack deserialization error: {0}")]
MsgPackDecodeError(#[from] rmp_serde::decode::Error),
#[error("Borsh serialization error: {0}")]
BorshError(#[from] std::io::Error),
#[error("API logic error: {0}")]
ApiError(String),
}
pub mod fs;
pub mod ipc {
use super::*;
#[derive(Debug, Clone)]
pub struct SubscriptionHandle(pub(crate) Vec<u8>);
impl SubscriptionHandle {
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
impl AsRef<[u8]> for SubscriptionHandle {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
pub fn publish_bytes(topic: impl AsRef<[u8]>, payload: &[u8]) -> Result<(), SysError> {
unsafe { astrid_ipc_publish(topic.as_ref().to_vec(), payload.to_vec())? };
Ok(())
}
pub fn publish_json<T: Serialize>(
topic: impl AsRef<[u8]>,
payload: &T,
) -> Result<(), SysError> {
let bytes = serde_json::to_vec(payload)?;
publish_bytes(topic, &bytes)
}
pub fn publish_msgpack<T: Serialize>(
topic: impl AsRef<[u8]>,
payload: &T,
) -> Result<(), SysError> {
let bytes = rmp_serde::to_vec_named(payload)?;
publish_bytes(topic, &bytes)
}
pub fn subscribe(topic: impl AsRef<[u8]>) -> Result<SubscriptionHandle, SysError> {
let handle_bytes = unsafe { astrid_ipc_subscribe(topic.as_ref().to_vec())? };
Ok(SubscriptionHandle(handle_bytes))
}
pub fn unsubscribe(handle: &SubscriptionHandle) -> Result<(), SysError> {
unsafe { astrid_ipc_unsubscribe(handle.0.clone())? };
Ok(())
}
pub fn poll_bytes(handle: &SubscriptionHandle) -> Result<Vec<u8>, SysError> {
let message_bytes = unsafe { astrid_ipc_poll(handle.0.clone())? };
Ok(message_bytes)
}
pub fn recv_bytes(handle: &SubscriptionHandle, timeout_ms: u64) -> Result<Vec<u8>, SysError> {
let timeout_str = timeout_ms.to_string();
let message_bytes = unsafe { astrid_ipc_recv(handle.0.clone(), timeout_str.into_bytes())? };
Ok(message_bytes)
}
}
pub mod uplink {
use super::*;
#[derive(Debug, Clone)]
pub struct UplinkId(pub(crate) Vec<u8>);
impl UplinkId {
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
impl AsRef<[u8]> for UplinkId {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
pub fn register(
name: impl AsRef<[u8]>,
platform: impl AsRef<[u8]>,
profile: impl AsRef<[u8]>,
) -> Result<UplinkId, SysError> {
let id_bytes = unsafe {
astrid_uplink_register(
name.as_ref().to_vec(),
platform.as_ref().to_vec(),
profile.as_ref().to_vec(),
)?
};
Ok(UplinkId(id_bytes))
}
pub fn send_bytes(
uplink_id: &UplinkId,
platform_user_id: impl AsRef<[u8]>,
content: &[u8],
) -> Result<Vec<u8>, SysError> {
let result = unsafe {
astrid_uplink_send(
uplink_id.0.clone(),
platform_user_id.as_ref().to_vec(),
content.to_vec(),
)?
};
Ok(result)
}
}
pub mod kv {
use super::*;
pub fn get_bytes(key: impl AsRef<[u8]>) -> Result<Vec<u8>, SysError> {
let result = unsafe { astrid_kv_get(key.as_ref().to_vec())? };
Ok(result)
}
pub fn set_bytes(key: impl AsRef<[u8]>, value: &[u8]) -> Result<(), SysError> {
unsafe { astrid_kv_set(key.as_ref().to_vec(), value.to_vec())? };
Ok(())
}
pub fn get_json<T: DeserializeOwned>(key: impl AsRef<[u8]>) -> Result<T, SysError> {
let bytes = get_bytes(key)?;
let parsed = serde_json::from_slice(&bytes)?;
Ok(parsed)
}
pub fn set_json<T: Serialize>(key: impl AsRef<[u8]>, value: &T) -> Result<(), SysError> {
let bytes = serde_json::to_vec(value)?;
set_bytes(key, &bytes)
}
pub fn delete(key: impl AsRef<[u8]>) -> Result<(), SysError> {
unsafe { astrid_kv_delete(key.as_ref().to_vec())? };
Ok(())
}
pub fn list_keys(prefix: impl AsRef<[u8]>) -> Result<Vec<String>, SysError> {
let result = unsafe { astrid_kv_list_keys(prefix.as_ref().to_vec())? };
let keys: Vec<String> = serde_json::from_slice(&result)?;
Ok(keys)
}
pub fn clear_prefix(prefix: impl AsRef<[u8]>) -> Result<u64, SysError> {
let result = unsafe { astrid_kv_clear_prefix(prefix.as_ref().to_vec())? };
let count: u64 = serde_json::from_slice(&result)?;
Ok(count)
}
pub fn get_borsh<T: BorshDeserialize>(key: impl AsRef<[u8]>) -> Result<T, SysError> {
let bytes = get_bytes(key)?;
let parsed = borsh::from_slice(&bytes)?;
Ok(parsed)
}
pub fn set_borsh<T: BorshSerialize>(key: impl AsRef<[u8]>, value: &T) -> Result<(), SysError> {
let bytes = borsh::to_vec(value)?;
set_bytes(key, &bytes)
}
#[derive(Serialize, Deserialize)]
struct VersionedEnvelope<T> {
#[serde(rename = "__sv")]
schema_version: u32,
data: T,
}
#[derive(Debug)]
pub enum Versioned<T> {
Current(T),
NeedsMigration {
raw: serde_json::Value,
stored_version: u32,
},
Unversioned(serde_json::Value),
NotFound,
}
pub fn set_versioned<T: Serialize>(
key: impl AsRef<[u8]>,
value: &T,
version: u32,
) -> Result<(), SysError> {
let envelope = VersionedEnvelope {
schema_version: version,
data: value,
};
set_json(key, &envelope)
}
pub fn get_versioned<T: DeserializeOwned>(
key: impl AsRef<[u8]>,
current_version: u32,
) -> Result<Versioned<T>, SysError> {
let bytes = get_bytes(&key)?;
parse_versioned(&bytes, current_version)
}
fn parse_versioned<T: DeserializeOwned>(
bytes: &[u8],
current_version: u32,
) -> Result<Versioned<T>, SysError> {
if bytes.is_empty() {
return Ok(Versioned::NotFound);
}
let mut value: serde_json::Value = serde_json::from_slice(bytes)?;
let sv_field = value.get("__sv");
let has_sv = sv_field.is_some();
let envelope_version = sv_field.and_then(|v| v.as_u64());
let has_data = value.get("data").is_some();
match (has_sv, envelope_version, has_data) {
(_, Some(v), true) => {
let v = u32::try_from(v)
.map_err(|_| SysError::ApiError("schema version exceeds u32::MAX".into()))?;
let data = value
.as_object_mut()
.and_then(|m| m.remove("data"))
.expect("data field guaranteed by match condition");
if v == current_version {
let parsed: T = serde_json::from_value(data)?;
Ok(Versioned::Current(parsed))
} else if v < current_version {
Ok(Versioned::NeedsMigration {
raw: data,
stored_version: v,
})
} else {
Err(SysError::ApiError(format!(
"stored schema version {v} is newer than current \
version {current_version} - cannot safely read"
)))
}
}
(true, _, _) => Err(SysError::ApiError(
"malformed versioned envelope: __sv field present but \
data field missing or __sv is not a number"
.into(),
)),
(false, _, _) => Ok(Versioned::Unversioned(value)),
}
}
pub fn get_versioned_or_migrate<T: Serialize + DeserializeOwned>(
key: impl AsRef<[u8]>,
current_version: u32,
migrate_fn: impl FnOnce(serde_json::Value, u32) -> Result<T, SysError>,
) -> Result<Option<T>, SysError> {
let key = key.as_ref();
match get_versioned::<T>(key, current_version)? {
Versioned::Current(data) => Ok(Some(data)),
Versioned::NeedsMigration {
raw,
stored_version,
} => {
let migrated = migrate_fn(raw, stored_version)?;
set_versioned(key, &migrated, current_version)?;
Ok(Some(migrated))
}
Versioned::Unversioned(raw) => {
let migrated = migrate_fn(raw, 0)?;
set_versioned(key, &migrated, current_version)?;
Ok(Some(migrated))
}
Versioned::NotFound => Ok(None),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct TestData {
name: String,
count: u32,
}
#[test]
fn versioned_envelope_roundtrip() {
let envelope = VersionedEnvelope {
schema_version: 1,
data: TestData {
name: "hello".into(),
count: 42,
},
};
let json = serde_json::to_string(&envelope).unwrap();
assert!(json.contains("\"__sv\":1"));
assert!(json.contains("\"data\":{"));
let parsed: VersionedEnvelope<TestData> = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.schema_version, 1);
assert_eq!(
parsed.data,
TestData {
name: "hello".into(),
count: 42,
}
);
}
#[test]
fn versioned_envelope_wire_format() {
let envelope = VersionedEnvelope {
schema_version: 3,
data: serde_json::json!({"key": "value"}),
};
let json = serde_json::to_string(&envelope).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["__sv"], 3);
assert_eq!(parsed["data"]["key"], "value");
}
#[test]
fn parse_versioned_empty_bytes_returns_not_found() {
let result = parse_versioned::<TestData>(b"", 1).unwrap();
assert!(matches!(result, Versioned::NotFound));
}
#[test]
fn parse_versioned_current_version_returns_current() {
let bytes = br#"{"__sv":2,"data":{"name":"hello","count":42}}"#;
let result = parse_versioned::<TestData>(bytes, 2).unwrap();
match result {
Versioned::Current(data) => {
assert_eq!(data.name, "hello");
assert_eq!(data.count, 42);
}
other => panic!("expected Current, got {other:?}"),
}
}
#[test]
fn parse_versioned_older_version_returns_needs_migration() {
let bytes = br#"{"__sv":1,"data":{"name":"old","count":1}}"#;
let result = parse_versioned::<TestData>(bytes, 3).unwrap();
match result {
Versioned::NeedsMigration {
raw,
stored_version,
} => {
assert_eq!(stored_version, 1);
assert_eq!(raw["name"], "old");
assert_eq!(raw["count"], 1);
}
other => panic!("expected NeedsMigration, got {other:?}"),
}
}
#[test]
fn parse_versioned_newer_version_returns_error() {
let bytes = br#"{"__sv":5,"data":{"name":"future","count":0}}"#;
let result = parse_versioned::<TestData>(bytes, 2);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("newer than current"),
"error should mention newer version: {err}"
);
}
#[test]
fn parse_versioned_plain_json_returns_unversioned() {
let bytes = br#"{"name":"legacy","count":99}"#;
let result = parse_versioned::<TestData>(bytes, 1).unwrap();
match result {
Versioned::Unversioned(val) => {
assert_eq!(val["name"], "legacy");
assert_eq!(val["count"], 99);
}
other => panic!("expected Unversioned, got {other:?}"),
}
}
#[test]
fn parse_versioned_malformed_sv_without_data_returns_error() {
let bytes = br#"{"__sv":1,"payload":"something"}"#;
let result = parse_versioned::<TestData>(bytes, 1);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("malformed"),
"error should mention malformed envelope: {err}"
);
}
#[test]
fn parse_versioned_non_numeric_sv_returns_error() {
let bytes = br#"{"__sv":"one","data":{}}"#;
let result = parse_versioned::<TestData>(bytes, 1);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("malformed"),
"error should mention malformed envelope: {err}"
);
}
#[test]
fn parse_versioned_version_zero_is_valid() {
let bytes = br#"{"__sv":0,"data":{"name":"v0","count":0}}"#;
let result = parse_versioned::<TestData>(bytes, 0).unwrap();
assert!(matches!(result, Versioned::Current(_)));
}
#[test]
fn parse_versioned_invalid_json_returns_error() {
let result = parse_versioned::<TestData>(b"not json", 1);
assert!(result.is_err());
}
}
}
pub mod http {
use super::*;
use serde::Serialize;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize)]
pub struct Request {
url: String,
method: String,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
headers: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
body: Option<String>,
}
impl Request {
pub fn new(method: impl Into<String>, url: impl Into<String>) -> Self {
Self {
url: url.into(),
method: method.into(),
headers: HashMap::new(),
body: None,
}
}
pub fn get(url: impl Into<String>) -> Self {
Self::new("GET", url)
}
pub fn post(url: impl Into<String>) -> Self {
Self::new("POST", url)
}
pub fn put(url: impl Into<String>) -> Self {
Self::new("PUT", url)
}
pub fn delete(url: impl Into<String>) -> Self {
Self::new("DELETE", url)
}
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(key.into(), value.into());
self
}
pub fn body(mut self, body: impl Into<String>) -> Self {
self.body = Some(body.into());
self
}
pub fn json<T: Serialize>(self, value: &T) -> Result<Self, SysError> {
let json = serde_json::to_string(value)?;
Ok(self.header("Content-Type", "application/json").body(json))
}
fn to_bytes(&self) -> Result<Vec<u8>, SysError> {
serde_json::to_vec(self).map_err(SysError::from)
}
}
#[derive(Debug)]
pub struct Response {
bytes: Vec<u8>,
}
impl Response {
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
pub fn text(&self) -> Result<&str, SysError> {
core::str::from_utf8(&self.bytes).map_err(|e| SysError::ApiError(e.to_string()))
}
pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, SysError> {
serde_json::from_slice(&self.bytes).map_err(SysError::from)
}
}
pub fn send(request: &Request) -> Result<Response, SysError> {
let req_bytes = request.to_bytes()?;
let result = unsafe { astrid_http_request(req_bytes)? };
Ok(Response { bytes: result })
}
#[derive(Debug)]
pub struct HttpStreamHandle(String);
pub struct StreamStartResponse {
pub handle: HttpStreamHandle,
pub status: u16,
pub headers: HashMap<String, String>,
}
pub fn stream_start(request: &Request) -> Result<StreamStartResponse, SysError> {
let req_bytes = request.to_bytes()?;
let result = unsafe { astrid_http_stream_start(req_bytes)? };
#[derive(serde::Deserialize)]
struct Resp {
handle: String,
status: u16,
headers: HashMap<String, String>,
}
let resp: Resp = serde_json::from_slice(&result)?;
Ok(StreamStartResponse {
handle: HttpStreamHandle(resp.handle),
status: resp.status,
headers: resp.headers,
})
}
pub fn stream_read(stream: &HttpStreamHandle) -> Result<Option<Vec<u8>>, SysError> {
let result = unsafe { astrid_http_stream_read(stream.0.as_bytes().to_vec())? };
if result.is_empty() {
Ok(None)
} else {
Ok(Some(result))
}
}
pub fn stream_close(stream: &HttpStreamHandle) -> Result<(), SysError> {
unsafe { astrid_http_stream_close(stream.0.as_bytes().to_vec())? };
Ok(())
}
}
pub mod cron {
use super::*;
pub fn schedule(
name: impl AsRef<[u8]>,
schedule: impl AsRef<[u8]>,
payload: &[u8],
) -> Result<(), SysError> {
unsafe {
astrid_cron_schedule(
name.as_ref().to_vec(),
schedule.as_ref().to_vec(),
payload.to_vec(),
)?
};
Ok(())
}
pub fn cancel(name: impl AsRef<[u8]>) -> Result<(), SysError> {
unsafe { astrid_cron_cancel(name.as_ref().to_vec())? };
Ok(())
}
}
pub mod env {
use super::*;
pub const CONFIG_SOCKET_PATH: &str = "ASTRID_SOCKET_PATH";
pub fn var_bytes(key: impl AsRef<[u8]>) -> Result<Vec<u8>, SysError> {
let result = unsafe { astrid_get_config(key.as_ref().to_vec())? };
Ok(result)
}
pub fn var(key: impl AsRef<[u8]>) -> Result<String, SysError> {
let bytes = var_bytes(key)?;
String::from_utf8(bytes).map_err(|e| SysError::ApiError(e.to_string()))
}
}
pub mod time {
use super::*;
pub fn now() -> Result<std::time::SystemTime, SysError> {
let bytes = unsafe { astrid_clock_ms()? };
let s = String::from_utf8_lossy(&bytes);
let ms = s
.trim()
.parse::<u64>()
.map_err(|e| SysError::ApiError(format!("clock parse error: {e}")))?;
Ok(std::time::UNIX_EPOCH + std::time::Duration::from_millis(ms))
}
}
pub mod log {
use super::*;
use core::fmt::Display;
pub fn log(level: &str, message: impl Display) -> Result<(), SysError> {
let msg = format!("{message}");
unsafe { astrid_log(level.as_bytes().to_vec(), msg.into_bytes())? };
Ok(())
}
pub fn debug(message: impl Display) -> Result<(), SysError> {
log("debug", message)
}
pub fn info(message: impl Display) -> Result<(), SysError> {
log("info", message)
}
pub fn warn(message: impl Display) -> Result<(), SysError> {
log("warn", message)
}
pub fn error(message: impl Display) -> Result<(), SysError> {
log("error", message)
}
}
pub mod runtime {
use super::*;
pub fn signal_ready() -> Result<(), SysError> {
unsafe { astrid_signal_ready()? };
Ok(())
}
pub fn caller() -> Result<crate::types::CallerContext, SysError> {
let bytes = unsafe { astrid_get_caller()? };
serde_json::from_slice(&bytes)
.map_err(|e| SysError::ApiError(format!("failed to parse caller context: {e}")))
}
pub fn socket_path() -> Result<String, SysError> {
let raw = crate::env::var(crate::env::CONFIG_SOCKET_PATH)?;
let path = serde_json::from_str::<String>(raw.trim()).or_else(|_| {
if raw.is_empty() {
Err(SysError::ApiError(
"ASTRID_SOCKET_PATH config key is empty".to_string(),
))
} else {
Ok(raw)
}
})?;
if path.contains('\0') {
return Err(SysError::ApiError(
"ASTRID_SOCKET_PATH contains null byte".to_string(),
));
}
Ok(path)
}
}
pub mod hooks {
use super::*;
pub fn trigger(event_bytes: &[u8]) -> Result<Vec<u8>, SysError> {
unsafe { Ok(astrid_trigger_hook(event_bytes.to_vec())?) }
}
}
pub mod capabilities {
use super::*;
pub fn check(source_uuid: &str, capability: &str) -> Result<bool, SysError> {
let request = serde_json::json!({
"source_uuid": source_uuid,
"capability": capability,
});
let request_bytes = serde_json::to_vec(&request)?;
let response_bytes = unsafe { astrid_check_capsule_capability(request_bytes)? };
let response: serde_json::Value = serde_json::from_slice(&response_bytes)?;
Ok(response["allowed"].as_bool().unwrap_or(false))
}
}
pub mod net;
pub mod process {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize)]
pub struct ProcessRequest<'a> {
pub cmd: &'a str,
pub args: &'a [&'a str],
}
#[derive(Debug, Deserialize)]
pub struct ProcessResult {
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
}
pub fn spawn(cmd: &str, args: &[&str]) -> Result<ProcessResult, SysError> {
let req = ProcessRequest { cmd, args };
let req_bytes = serde_json::to_vec(&req)?;
let result_bytes = unsafe { astrid_spawn_host(req_bytes)? };
let result: ProcessResult = serde_json::from_slice(&result_bytes)?;
Ok(result)
}
#[derive(Debug, Deserialize)]
pub struct BackgroundProcessHandle {
id: u64,
}
impl BackgroundProcessHandle {
pub fn id(&self) -> u64 {
self.id
}
}
#[derive(Debug, Deserialize)]
pub struct ProcessLogs {
pub stdout: String,
pub stderr: String,
pub running: bool,
pub exit_code: Option<i32>,
}
#[derive(Debug, Deserialize)]
pub struct KillResult {
pub killed: bool,
pub exit_code: Option<i32>,
pub stdout: String,
pub stderr: String,
}
pub fn spawn_background(cmd: &str, args: &[&str]) -> Result<BackgroundProcessHandle, SysError> {
let req = ProcessRequest { cmd, args };
let req_bytes = serde_json::to_vec(&req)?;
let result_bytes = unsafe { astrid_spawn_background_host(req_bytes)? };
let result: BackgroundProcessHandle = serde_json::from_slice(&result_bytes)?;
Ok(result)
}
pub fn read_logs(id: u64) -> Result<ProcessLogs, SysError> {
#[derive(Serialize)]
struct Req {
id: u64,
}
let req_bytes = serde_json::to_vec(&Req { id })?;
let result_bytes = unsafe { astrid_read_process_logs_host(req_bytes)? };
let result: ProcessLogs = serde_json::from_slice(&result_bytes)?;
Ok(result)
}
pub fn kill(id: u64) -> Result<KillResult, SysError> {
#[derive(Serialize)]
struct Req {
id: u64,
}
let req_bytes = serde_json::to_vec(&Req { id })?;
let result_bytes = unsafe { astrid_kill_process_host(req_bytes)? };
let result: KillResult = serde_json::from_slice(&result_bytes)?;
Ok(result)
}
}
pub mod elicit {
use super::*;
#[derive(Serialize)]
struct ElicitRequest<'a> {
#[serde(rename = "type")]
kind: &'a str,
key: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
options: Option<&'a [&'a str]>,
#[serde(skip_serializing_if = "Option::is_none")]
default: Option<&'a str>,
}
fn validate_key(key: &str) -> Result<(), SysError> {
if key.trim().is_empty() {
return Err(SysError::ApiError("elicit key must not be empty".into()));
}
Ok(())
}
pub fn secret(key: &str, description: &str) -> Result<(), SysError> {
validate_key(key)?;
let req = ElicitRequest {
kind: "secret",
key,
description: Some(description),
options: None,
default: None,
};
let req_bytes = serde_json::to_vec(&req)?;
let resp_bytes = unsafe { astrid_elicit(req_bytes)? };
#[derive(serde::Deserialize)]
struct SecretResp {
ok: bool,
}
let resp: SecretResp = serde_json::from_slice(&resp_bytes)?;
if !resp.ok {
return Err(SysError::ApiError(
"kernel did not confirm secret storage".into(),
));
}
Ok(())
}
pub fn has_secret(key: &str) -> Result<bool, SysError> {
validate_key(key)?;
#[derive(Serialize)]
struct HasSecretRequest<'a> {
key: &'a str,
}
let req_bytes = serde_json::to_vec(&HasSecretRequest { key })?;
let resp_bytes = unsafe { astrid_has_secret(req_bytes)? };
#[derive(serde::Deserialize)]
struct ExistsResp {
exists: bool,
}
let resp: ExistsResp = serde_json::from_slice(&resp_bytes)?;
Ok(resp.exists)
}
fn elicit_text(
key: &str,
description: &str,
default: Option<&str>,
) -> Result<String, SysError> {
validate_key(key)?;
let req = ElicitRequest {
kind: "text",
key,
description: Some(description),
options: None,
default,
};
let req_bytes = serde_json::to_vec(&req)?;
let resp_bytes = unsafe { astrid_elicit(req_bytes)? };
#[derive(serde::Deserialize)]
struct TextResp {
value: String,
}
let resp: TextResp = serde_json::from_slice(&resp_bytes)?;
Ok(resp.value)
}
pub fn text(key: &str, description: &str) -> Result<String, SysError> {
elicit_text(key, description, None)
}
pub fn text_with_default(
key: &str,
description: &str,
default: &str,
) -> Result<String, SysError> {
elicit_text(key, description, Some(default))
}
pub fn select(key: &str, description: &str, options: &[&str]) -> Result<String, SysError> {
validate_key(key)?;
if options.is_empty() {
return Err(SysError::ApiError(
"select requires at least one option".into(),
));
}
let req = ElicitRequest {
kind: "select",
key,
description: Some(description),
options: Some(options),
default: None,
};
let req_bytes = serde_json::to_vec(&req)?;
let resp_bytes = unsafe { astrid_elicit(req_bytes)? };
#[derive(serde::Deserialize)]
struct SelectResp {
value: String,
}
let resp: SelectResp = serde_json::from_slice(&resp_bytes)?;
if !options.iter().any(|o| *o == resp.value) {
let truncated: String = resp.value.chars().take(64).collect();
return Err(SysError::ApiError(format!(
"host returned value '{truncated}' not in provided options",
)));
}
Ok(resp.value)
}
pub fn array(key: &str, description: &str) -> Result<Vec<String>, SysError> {
validate_key(key)?;
let req = ElicitRequest {
kind: "array",
key,
description: Some(description),
options: None,
default: None,
};
let req_bytes = serde_json::to_vec(&req)?;
let resp_bytes = unsafe { astrid_elicit(req_bytes)? };
#[derive(serde::Deserialize)]
struct ArrayResp {
values: Vec<String>,
}
let resp: ArrayResp = serde_json::from_slice(&resp_bytes)?;
Ok(resp.values)
}
}
pub mod interceptors {
use super::*;
#[derive(Debug, serde::Deserialize)]
pub struct InterceptorBinding {
pub handle_id: u64,
pub action: String,
pub topic: String,
}
impl InterceptorBinding {
#[must_use]
pub fn subscription_handle(&self) -> ipc::SubscriptionHandle {
ipc::SubscriptionHandle(self.handle_id.to_string().into_bytes())
}
#[must_use]
pub fn handle_bytes(&self) -> Vec<u8> {
self.handle_id.to_string().into_bytes()
}
}
pub fn bindings() -> Result<Vec<InterceptorBinding>, SysError> {
let bytes = unsafe { astrid_get_interceptor_handles()? };
let bindings: Vec<InterceptorBinding> = serde_json::from_slice(&bytes)?;
Ok(bindings)
}
pub fn poll(
bindings: &[InterceptorBinding],
mut handler: impl FnMut(&str, &[u8]),
) -> Result<(), SysError> {
#[derive(serde::Deserialize)]
struct PollEnvelope {
messages: Vec<serde_json::Value>,
}
for binding in bindings {
let handle = binding.subscription_handle();
let envelope = ipc::poll_bytes(&handle)?;
let parsed: PollEnvelope = serde_json::from_slice(&envelope)?;
if !parsed.messages.is_empty() {
handler(&binding.action, &envelope);
}
}
Ok(())
}
}
pub mod identity {
use super::*;
#[derive(Debug)]
pub struct ResolvedUser {
pub user_id: String,
pub display_name: Option<String>,
}
#[derive(Debug)]
pub struct Link {
pub platform: String,
pub platform_user_id: String,
pub astrid_user_id: String,
pub linked_at: String,
pub method: String,
}
pub fn resolve(
platform: &str,
platform_user_id: &str,
) -> Result<Option<ResolvedUser>, SysError> {
#[derive(Serialize)]
struct Req<'a> {
platform: &'a str,
platform_user_id: &'a str,
}
let req_bytes = serde_json::to_vec(&Req {
platform,
platform_user_id,
})?;
let resp_bytes = unsafe { astrid_identity_resolve(req_bytes)? };
#[derive(Deserialize)]
struct Resp {
found: bool,
user_id: Option<String>,
display_name: Option<String>,
error: Option<String>,
}
let resp: Resp = serde_json::from_slice(&resp_bytes)?;
if resp.found {
let user_id = resp.user_id.ok_or_else(|| {
SysError::ApiError("host returned found=true but user_id was missing".into())
})?;
Ok(Some(ResolvedUser {
user_id,
display_name: resp.display_name,
}))
} else if let Some(err) = resp.error {
Err(SysError::ApiError(err))
} else {
Ok(None)
}
}
pub fn link(
platform: &str,
platform_user_id: &str,
astrid_user_id: &str,
method: &str,
) -> Result<Link, SysError> {
#[derive(Serialize)]
struct Req<'a> {
platform: &'a str,
platform_user_id: &'a str,
astrid_user_id: &'a str,
method: &'a str,
}
let req_bytes = serde_json::to_vec(&Req {
platform,
platform_user_id,
astrid_user_id,
method,
})?;
let resp_bytes = unsafe { astrid_identity_link(req_bytes)? };
#[derive(Deserialize)]
struct LinkInfo {
platform: String,
platform_user_id: String,
astrid_user_id: String,
linked_at: String,
method: String,
}
#[derive(Deserialize)]
struct Resp {
ok: bool,
error: Option<String>,
link: Option<LinkInfo>,
}
let resp: Resp = serde_json::from_slice(&resp_bytes)?;
if !resp.ok {
return Err(SysError::ApiError(
resp.error.unwrap_or_else(|| "identity link failed".into()),
));
}
let l = resp
.link
.ok_or_else(|| SysError::ApiError("missing link in response".into()))?;
Ok(Link {
platform: l.platform,
platform_user_id: l.platform_user_id,
astrid_user_id: l.astrid_user_id,
linked_at: l.linked_at,
method: l.method,
})
}
pub fn unlink(platform: &str, platform_user_id: &str) -> Result<bool, SysError> {
#[derive(Serialize)]
struct Req<'a> {
platform: &'a str,
platform_user_id: &'a str,
}
let req_bytes = serde_json::to_vec(&Req {
platform,
platform_user_id,
})?;
let resp_bytes = unsafe { astrid_identity_unlink(req_bytes)? };
#[derive(Deserialize)]
struct Resp {
ok: bool,
error: Option<String>,
removed: Option<bool>,
}
let resp: Resp = serde_json::from_slice(&resp_bytes)?;
if !resp.ok {
return Err(SysError::ApiError(
resp.error
.unwrap_or_else(|| "identity unlink failed".into()),
));
}
Ok(resp.removed.unwrap_or(false))
}
pub fn create_user(display_name: Option<&str>) -> Result<String, SysError> {
#[derive(Serialize)]
struct Req<'a> {
display_name: Option<&'a str>,
}
let req_bytes = serde_json::to_vec(&Req { display_name })?;
let resp_bytes = unsafe { astrid_identity_create_user(req_bytes)? };
#[derive(Deserialize)]
struct Resp {
ok: bool,
error: Option<String>,
user_id: Option<String>,
}
let resp: Resp = serde_json::from_slice(&resp_bytes)?;
if !resp.ok {
return Err(SysError::ApiError(
resp.error
.unwrap_or_else(|| "identity create_user failed".into()),
));
}
resp.user_id
.ok_or_else(|| SysError::ApiError("missing user_id in response".into()))
}
pub fn list_links(astrid_user_id: &str) -> Result<Vec<Link>, SysError> {
#[derive(Serialize)]
struct Req<'a> {
astrid_user_id: &'a str,
}
let req_bytes = serde_json::to_vec(&Req { astrid_user_id })?;
let resp_bytes = unsafe { astrid_identity_list_links(req_bytes)? };
#[derive(Deserialize)]
struct LinkInfo {
platform: String,
platform_user_id: String,
astrid_user_id: String,
linked_at: String,
method: String,
}
#[derive(Deserialize)]
struct Resp {
ok: bool,
error: Option<String>,
links: Option<Vec<LinkInfo>>,
}
let resp: Resp = serde_json::from_slice(&resp_bytes)?;
if !resp.ok {
return Err(SysError::ApiError(
resp.error
.unwrap_or_else(|| "identity list_links failed".into()),
));
}
Ok(resp
.links
.unwrap_or_default()
.into_iter()
.map(|l| Link {
platform: l.platform,
platform_user_id: l.platform_user_id,
astrid_user_id: l.astrid_user_id,
linked_at: l.linked_at,
method: l.method,
})
.collect())
}
}
pub mod approval {
use super::*;
#[derive(Debug)]
pub struct ApprovalResult {
pub approved: bool,
pub decision: String,
}
pub fn request(
action: &str,
resource: &str,
risk_level: &str,
) -> Result<ApprovalResult, SysError> {
#[derive(Serialize)]
struct ApprovalRequest<'a> {
action: &'a str,
resource: &'a str,
risk_level: &'a str,
}
let req = ApprovalRequest {
action,
resource,
risk_level,
};
let req_bytes = serde_json::to_vec(&req)?;
let resp_bytes = unsafe { astrid_request_approval(req_bytes)? };
#[derive(Deserialize)]
struct ApprovalResp {
approved: bool,
decision: String,
}
let resp: ApprovalResp = serde_json::from_slice(&resp_bytes)?;
Ok(ApprovalResult {
approved: resp.approved,
decision: resp.decision,
})
}
}
pub mod prelude {
pub use crate::{
SysError,
approval,
capabilities,
cron,
elicit,
env,
fs,
hooks,
http,
identity,
interceptors,
ipc,
kv,
log,
net,
process,
runtime,
time,
uplink,
};
#[cfg(feature = "derive")]
pub use astrid_sdk_macros::capsule;
}