1#![allow(unsafe_code)]
38#![allow(missing_docs)]
39#![deny(clippy::all)]
40#![deny(unreachable_pub)]
41#![deny(clippy::unwrap_used)]
42#![cfg_attr(test, allow(clippy::unwrap_used))]
43
44use astrid_sys::*;
45use borsh::{BorshDeserialize, BorshSerialize};
46use serde::{Deserialize, Serialize, de::DeserializeOwned};
47use thiserror::Error;
48
49pub mod types {
54 use serde::{Deserialize, Serialize};
55
56 pub use astrid_types::ipc;
58 pub use astrid_types::kernel;
59 pub use astrid_types::llm;
60
61 pub use astrid_types::ipc::{
63 IpcMessage, IpcPayload, OnboardingField, OnboardingFieldType, SelectionOption,
64 };
65
66 pub use astrid_types::kernel::{
68 CapsuleMetadataEntry, CommandInfo, KernelRequest, KernelResponse, LlmProviderInfo,
69 SYSTEM_SESSION_UUID,
70 };
71
72 pub use astrid_types::llm::{
74 ContentPart, LlmResponse, LlmToolDefinition, Message, MessageContent, MessageRole,
75 StopReason, StreamEvent, ToolCall, ToolCallResult, Usage,
76 };
77
78 #[derive(Debug, Clone, Serialize, Deserialize)]
80 pub struct CallerContext {
81 pub session_id: Option<String>,
83 pub user_id: Option<String>,
85 }
86}
87pub use borsh;
88pub use serde;
89pub use serde_json;
90
91#[doc(hidden)]
94pub use extism_pdk;
95#[doc(hidden)]
96pub use schemars;
97
98#[derive(Error, Debug)]
100pub enum SysError {
101 #[error("Host function call failed: {0}")]
102 HostError(#[from] extism_pdk::Error),
103 #[error("JSON serialization error: {0}")]
104 JsonError(#[from] serde_json::Error),
105 #[error("MessagePack serialization error: {0}")]
106 MsgPackEncodeError(#[from] rmp_serde::encode::Error),
107 #[error("MessagePack deserialization error: {0}")]
108 MsgPackDecodeError(#[from] rmp_serde::decode::Error),
109 #[error("Borsh serialization error: {0}")]
110 BorshError(#[from] std::io::Error),
111 #[error("API logic error: {0}")]
112 ApiError(String),
113}
114
115pub mod fs {
117 use super::*;
118
119 pub fn exists(path: impl AsRef<[u8]>) -> Result<bool, SysError> {
121 let result = unsafe { astrid_fs_exists(path.as_ref().to_vec())? };
122 Ok(!result.is_empty() && result[0] != 0)
123 }
124
125 pub fn read(path: impl AsRef<[u8]>) -> Result<Vec<u8>, SysError> {
127 let result = unsafe { astrid_read_file(path.as_ref().to_vec())? };
128 Ok(result)
129 }
130
131 pub fn read_to_string(path: impl AsRef<[u8]>) -> Result<String, SysError> {
133 let bytes = read(path)?;
134 String::from_utf8(bytes).map_err(|e| SysError::ApiError(e.to_string()))
135 }
136
137 pub fn write(path: impl AsRef<[u8]>, contents: impl AsRef<[u8]>) -> Result<(), SysError> {
139 unsafe { astrid_write_file(path.as_ref().to_vec(), contents.as_ref().to_vec())? };
140 Ok(())
141 }
142
143 pub fn create_dir(path: impl AsRef<[u8]>) -> Result<(), SysError> {
145 unsafe { astrid_fs_mkdir(path.as_ref().to_vec())? };
146 Ok(())
147 }
148
149 pub fn read_dir(path: impl AsRef<[u8]>) -> Result<Vec<u8>, SysError> {
151 let result = unsafe { astrid_fs_readdir(path.as_ref().to_vec())? };
152 Ok(result)
153 }
154
155 pub fn metadata(path: impl AsRef<[u8]>) -> Result<Vec<u8>, SysError> {
157 let result = unsafe { astrid_fs_stat(path.as_ref().to_vec())? };
158 Ok(result)
159 }
160
161 pub fn remove_file(path: impl AsRef<[u8]>) -> Result<(), SysError> {
163 unsafe { astrid_fs_unlink(path.as_ref().to_vec())? };
164 Ok(())
165 }
166}
167
168pub mod ipc {
170 use super::*;
171
172 #[derive(Debug, Clone)]
176 pub struct SubscriptionHandle(pub(crate) Vec<u8>);
177
178 impl SubscriptionHandle {
179 #[must_use]
181 pub fn as_bytes(&self) -> &[u8] {
182 &self.0
183 }
184 }
185
186 impl AsRef<[u8]> for SubscriptionHandle {
188 fn as_ref(&self) -> &[u8] {
189 &self.0
190 }
191 }
192
193 pub fn publish_bytes(topic: impl AsRef<[u8]>, payload: &[u8]) -> Result<(), SysError> {
194 unsafe { astrid_ipc_publish(topic.as_ref().to_vec(), payload.to_vec())? };
195 Ok(())
196 }
197
198 pub fn publish_json<T: Serialize>(
199 topic: impl AsRef<[u8]>,
200 payload: &T,
201 ) -> Result<(), SysError> {
202 let bytes = serde_json::to_vec(payload)?;
203 publish_bytes(topic, &bytes)
204 }
205
206 pub fn publish_msgpack<T: Serialize>(
207 topic: impl AsRef<[u8]>,
208 payload: &T,
209 ) -> Result<(), SysError> {
210 let bytes = rmp_serde::to_vec_named(payload)?;
211 publish_bytes(topic, &bytes)
212 }
213
214 pub fn subscribe(topic: impl AsRef<[u8]>) -> Result<SubscriptionHandle, SysError> {
216 let handle_bytes = unsafe { astrid_ipc_subscribe(topic.as_ref().to_vec())? };
217 Ok(SubscriptionHandle(handle_bytes))
218 }
219
220 pub fn unsubscribe(handle: &SubscriptionHandle) -> Result<(), SysError> {
221 unsafe { astrid_ipc_unsubscribe(handle.0.clone())? };
222 Ok(())
223 }
224
225 pub fn poll_bytes(handle: &SubscriptionHandle) -> Result<Vec<u8>, SysError> {
226 let message_bytes = unsafe { astrid_ipc_poll(handle.0.clone())? };
227 Ok(message_bytes)
228 }
229
230 pub fn recv_bytes(handle: &SubscriptionHandle, timeout_ms: u64) -> Result<Vec<u8>, SysError> {
236 let timeout_str = timeout_ms.to_string();
237 let message_bytes = unsafe { astrid_ipc_recv(handle.0.clone(), timeout_str.into_bytes())? };
238 Ok(message_bytes)
239 }
240}
241
242pub mod uplink {
244 use super::*;
245
246 #[derive(Debug, Clone)]
248 pub struct UplinkId(pub(crate) Vec<u8>);
249
250 impl UplinkId {
251 #[must_use]
253 pub fn as_bytes(&self) -> &[u8] {
254 &self.0
255 }
256 }
257
258 impl AsRef<[u8]> for UplinkId {
259 fn as_ref(&self) -> &[u8] {
260 &self.0
261 }
262 }
263
264 pub fn register(
266 name: impl AsRef<[u8]>,
267 platform: impl AsRef<[u8]>,
268 profile: impl AsRef<[u8]>,
269 ) -> Result<UplinkId, SysError> {
270 let id_bytes = unsafe {
271 astrid_uplink_register(
272 name.as_ref().to_vec(),
273 platform.as_ref().to_vec(),
274 profile.as_ref().to_vec(),
275 )?
276 };
277 Ok(UplinkId(id_bytes))
278 }
279
280 pub fn send_bytes(
282 uplink_id: &UplinkId,
283 platform_user_id: impl AsRef<[u8]>,
284 content: &[u8],
285 ) -> Result<Vec<u8>, SysError> {
286 let result = unsafe {
287 astrid_uplink_send(
288 uplink_id.0.clone(),
289 platform_user_id.as_ref().to_vec(),
290 content.to_vec(),
291 )?
292 };
293 Ok(result)
294 }
295}
296
297pub mod kv {
299 use super::*;
300
301 pub fn get_bytes(key: impl AsRef<[u8]>) -> Result<Vec<u8>, SysError> {
302 let result = unsafe { astrid_kv_get(key.as_ref().to_vec())? };
303 Ok(result)
304 }
305
306 pub fn set_bytes(key: impl AsRef<[u8]>, value: &[u8]) -> Result<(), SysError> {
307 unsafe { astrid_kv_set(key.as_ref().to_vec(), value.to_vec())? };
308 Ok(())
309 }
310
311 pub fn get_json<T: DeserializeOwned>(key: impl AsRef<[u8]>) -> Result<T, SysError> {
312 let bytes = get_bytes(key)?;
313 let parsed = serde_json::from_slice(&bytes)?;
314 Ok(parsed)
315 }
316
317 pub fn set_json<T: Serialize>(key: impl AsRef<[u8]>, value: &T) -> Result<(), SysError> {
318 let bytes = serde_json::to_vec(value)?;
319 set_bytes(key, &bytes)
320 }
321
322 pub fn delete(key: impl AsRef<[u8]>) -> Result<(), SysError> {
328 unsafe { astrid_kv_delete(key.as_ref().to_vec())? };
329 Ok(())
330 }
331
332 pub fn list_keys(prefix: impl AsRef<[u8]>) -> Result<Vec<String>, SysError> {
337 let result = unsafe { astrid_kv_list_keys(prefix.as_ref().to_vec())? };
338 let keys: Vec<String> = serde_json::from_slice(&result)?;
339 Ok(keys)
340 }
341
342 pub fn clear_prefix(prefix: impl AsRef<[u8]>) -> Result<u64, SysError> {
347 let result = unsafe { astrid_kv_clear_prefix(prefix.as_ref().to_vec())? };
348 let count: u64 = serde_json::from_slice(&result)?;
349 Ok(count)
350 }
351
352 pub fn get_borsh<T: BorshDeserialize>(key: impl AsRef<[u8]>) -> Result<T, SysError> {
353 let bytes = get_bytes(key)?;
354 let parsed = borsh::from_slice(&bytes)?;
355 Ok(parsed)
356 }
357
358 pub fn set_borsh<T: BorshSerialize>(key: impl AsRef<[u8]>, value: &T) -> Result<(), SysError> {
359 let bytes = borsh::to_vec(value)?;
360 set_bytes(key, &bytes)
361 }
362
363 #[derive(Serialize, Deserialize)]
371 struct VersionedEnvelope<T> {
372 #[serde(rename = "__sv")]
373 schema_version: u32,
374 data: T,
375 }
376
377 #[derive(Debug)]
379 pub enum Versioned<T> {
380 Current(T),
382 NeedsMigration {
384 raw: serde_json::Value,
386 stored_version: u32,
388 },
389 Unversioned(serde_json::Value),
391 NotFound,
393 }
394
395 pub fn set_versioned<T: Serialize>(
400 key: impl AsRef<[u8]>,
401 value: &T,
402 version: u32,
403 ) -> Result<(), SysError> {
404 let envelope = VersionedEnvelope {
405 schema_version: version,
406 data: value,
407 };
408 set_json(key, &envelope)
409 }
410
411 pub fn get_versioned<T: DeserializeOwned>(
422 key: impl AsRef<[u8]>,
423 current_version: u32,
424 ) -> Result<Versioned<T>, SysError> {
425 let bytes = get_bytes(&key)?;
426 parse_versioned(&bytes, current_version)
427 }
428
429 fn parse_versioned<T: DeserializeOwned>(
432 bytes: &[u8],
433 current_version: u32,
434 ) -> Result<Versioned<T>, SysError> {
435 if bytes.is_empty() {
439 return Ok(Versioned::NotFound);
440 }
441
442 let mut value: serde_json::Value = serde_json::from_slice(bytes)?;
443
444 let sv_field = value.get("__sv");
448 let has_sv = sv_field.is_some();
449 let envelope_version = sv_field.and_then(|v| v.as_u64());
450 let has_data = value.get("data").is_some();
451
452 match (has_sv, envelope_version, has_data) {
453 (_, Some(v), true) => {
456 let v = u32::try_from(v)
457 .map_err(|_| SysError::ApiError("schema version exceeds u32::MAX".into()))?;
458 let data = value
461 .as_object_mut()
462 .and_then(|m| m.remove("data"))
463 .expect("data field guaranteed by match condition");
464 if v == current_version {
465 let parsed: T = serde_json::from_value(data)?;
466 Ok(Versioned::Current(parsed))
467 } else if v < current_version {
468 Ok(Versioned::NeedsMigration {
469 raw: data,
470 stored_version: v,
471 })
472 } else {
473 Err(SysError::ApiError(format!(
474 "stored schema version {v} is newer than current \
475 version {current_version} - cannot safely read"
476 )))
477 }
478 }
479 (true, _, _) => Err(SysError::ApiError(
481 "malformed versioned envelope: __sv field present but \
482 data field missing or __sv is not a number"
483 .into(),
484 )),
485 (false, _, _) => Ok(Versioned::Unversioned(value)),
487 }
488 }
489
490 pub fn get_versioned_or_migrate<T: Serialize + DeserializeOwned>(
505 key: impl AsRef<[u8]>,
506 current_version: u32,
507 migrate_fn: impl FnOnce(serde_json::Value, u32) -> Result<T, SysError>,
508 ) -> Result<Option<T>, SysError> {
509 let key = key.as_ref();
510
511 match get_versioned::<T>(key, current_version)? {
512 Versioned::Current(data) => Ok(Some(data)),
513 Versioned::NeedsMigration {
514 raw,
515 stored_version,
516 } => {
517 let migrated = migrate_fn(raw, stored_version)?;
518 set_versioned(key, &migrated, current_version)?;
519 Ok(Some(migrated))
520 }
521 Versioned::Unversioned(raw) => {
522 let migrated = migrate_fn(raw, 0)?;
523 set_versioned(key, &migrated, current_version)?;
524 Ok(Some(migrated))
525 }
526 Versioned::NotFound => Ok(None),
527 }
528 }
529
530 #[cfg(test)]
531 mod tests {
532 use super::*;
533
534 #[derive(Debug, Serialize, Deserialize, PartialEq)]
535 struct TestData {
536 name: String,
537 count: u32,
538 }
539
540 #[test]
543 fn versioned_envelope_roundtrip() {
544 let envelope = VersionedEnvelope {
545 schema_version: 1,
546 data: TestData {
547 name: "hello".into(),
548 count: 42,
549 },
550 };
551 let json = serde_json::to_string(&envelope).unwrap();
552 assert!(json.contains("\"__sv\":1"));
553 assert!(json.contains("\"data\":{"));
554
555 let parsed: VersionedEnvelope<TestData> = serde_json::from_str(&json).unwrap();
556 assert_eq!(parsed.schema_version, 1);
557 assert_eq!(
558 parsed.data,
559 TestData {
560 name: "hello".into(),
561 count: 42,
562 }
563 );
564 }
565
566 #[test]
567 fn versioned_envelope_wire_format() {
568 let envelope = VersionedEnvelope {
569 schema_version: 3,
570 data: serde_json::json!({"key": "value"}),
571 };
572 let json = serde_json::to_string(&envelope).unwrap();
573 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
574
575 assert_eq!(parsed["__sv"], 3);
576 assert_eq!(parsed["data"]["key"], "value");
577 }
578
579 #[test]
582 fn parse_versioned_empty_bytes_returns_not_found() {
583 let result = parse_versioned::<TestData>(b"", 1).unwrap();
584 assert!(matches!(result, Versioned::NotFound));
585 }
586
587 #[test]
588 fn parse_versioned_current_version_returns_current() {
589 let bytes = br#"{"__sv":2,"data":{"name":"hello","count":42}}"#;
590 let result = parse_versioned::<TestData>(bytes, 2).unwrap();
591 match result {
592 Versioned::Current(data) => {
593 assert_eq!(data.name, "hello");
594 assert_eq!(data.count, 42);
595 }
596 other => panic!("expected Current, got {other:?}"),
597 }
598 }
599
600 #[test]
601 fn parse_versioned_older_version_returns_needs_migration() {
602 let bytes = br#"{"__sv":1,"data":{"name":"old","count":1}}"#;
603 let result = parse_versioned::<TestData>(bytes, 3).unwrap();
604 match result {
605 Versioned::NeedsMigration {
606 raw,
607 stored_version,
608 } => {
609 assert_eq!(stored_version, 1);
610 assert_eq!(raw["name"], "old");
611 assert_eq!(raw["count"], 1);
612 }
613 other => panic!("expected NeedsMigration, got {other:?}"),
614 }
615 }
616
617 #[test]
618 fn parse_versioned_newer_version_returns_error() {
619 let bytes = br#"{"__sv":5,"data":{"name":"future","count":0}}"#;
620 let result = parse_versioned::<TestData>(bytes, 2);
621 assert!(result.is_err());
622 let err = result.unwrap_err().to_string();
623 assert!(
624 err.contains("newer than current"),
625 "error should mention newer version: {err}"
626 );
627 }
628
629 #[test]
630 fn parse_versioned_plain_json_returns_unversioned() {
631 let bytes = br#"{"name":"legacy","count":99}"#;
632 let result = parse_versioned::<TestData>(bytes, 1).unwrap();
633 match result {
634 Versioned::Unversioned(val) => {
635 assert_eq!(val["name"], "legacy");
636 assert_eq!(val["count"], 99);
637 }
638 other => panic!("expected Unversioned, got {other:?}"),
639 }
640 }
641
642 #[test]
643 fn parse_versioned_malformed_sv_without_data_returns_error() {
644 let bytes = br#"{"__sv":1,"payload":"something"}"#;
645 let result = parse_versioned::<TestData>(bytes, 1);
646 assert!(result.is_err());
647 let err = result.unwrap_err().to_string();
648 assert!(
649 err.contains("malformed"),
650 "error should mention malformed envelope: {err}"
651 );
652 }
653
654 #[test]
655 fn parse_versioned_non_numeric_sv_returns_error() {
656 let bytes = br#"{"__sv":"one","data":{}}"#;
657 let result = parse_versioned::<TestData>(bytes, 1);
658 assert!(result.is_err());
659 let err = result.unwrap_err().to_string();
660 assert!(
661 err.contains("malformed"),
662 "error should mention malformed envelope: {err}"
663 );
664 }
665
666 #[test]
667 fn parse_versioned_version_zero_is_valid() {
668 let bytes = br#"{"__sv":0,"data":{"name":"v0","count":0}}"#;
670 let result = parse_versioned::<TestData>(bytes, 0).unwrap();
671 assert!(matches!(result, Versioned::Current(_)));
672 }
673
674 #[test]
675 fn parse_versioned_invalid_json_returns_error() {
676 let result = parse_versioned::<TestData>(b"not json", 1);
677 assert!(result.is_err());
678 }
679 }
680}
681
682pub mod http {
684 use super::*;
685
686 pub fn request_bytes(request_bytes: &[u8]) -> Result<Vec<u8>, SysError> {
689 let result = unsafe { astrid_http_request(request_bytes.to_vec())? };
690 Ok(result)
691 }
692
693 #[derive(Debug)]
698 pub struct HttpStreamHandle(String);
699
700 pub struct StreamStartResponse {
702 pub handle: HttpStreamHandle,
704 pub status: u16,
706 pub headers: std::collections::HashMap<String, String>,
708 }
709
710 pub fn stream_start(request_bytes: &[u8]) -> Result<StreamStartResponse, SysError> {
716 let result = unsafe { astrid_http_stream_start(request_bytes.to_vec())? };
717
718 #[derive(serde::Deserialize)]
719 struct Resp {
720 handle: String,
721 status: u16,
722 headers: std::collections::HashMap<String, String>,
723 }
724 let resp: Resp = serde_json::from_slice(&result)?;
725 Ok(StreamStartResponse {
726 handle: HttpStreamHandle(resp.handle),
727 status: resp.status,
728 headers: resp.headers,
729 })
730 }
731
732 pub fn stream_read(stream: &HttpStreamHandle) -> Result<Option<Vec<u8>>, SysError> {
737 let result = unsafe { astrid_http_stream_read(stream.0.as_bytes().to_vec())? };
738 if result.is_empty() {
739 Ok(None)
740 } else {
741 Ok(Some(result))
742 }
743 }
744
745 pub fn stream_close(stream: &HttpStreamHandle) -> Result<(), SysError> {
749 unsafe { astrid_http_stream_close(stream.0.as_bytes().to_vec())? };
750 Ok(())
751 }
752}
753
754pub mod cron {
756 use super::*;
757
758 pub fn schedule(
760 name: impl AsRef<[u8]>,
761 schedule: impl AsRef<[u8]>,
762 payload: &[u8],
763 ) -> Result<(), SysError> {
764 unsafe {
765 astrid_cron_schedule(
766 name.as_ref().to_vec(),
767 schedule.as_ref().to_vec(),
768 payload.to_vec(),
769 )?
770 };
771 Ok(())
772 }
773
774 pub fn cancel(name: impl AsRef<[u8]>) -> Result<(), SysError> {
776 unsafe { astrid_cron_cancel(name.as_ref().to_vec())? };
777 Ok(())
778 }
779}
780
781pub mod env {
786 use super::*;
787
788 pub const CONFIG_SOCKET_PATH: &str = "ASTRID_SOCKET_PATH";
790
791 pub fn var_bytes(key: impl AsRef<[u8]>) -> Result<Vec<u8>, SysError> {
793 let result = unsafe { astrid_get_config(key.as_ref().to_vec())? };
794 Ok(result)
795 }
796
797 pub fn var(key: impl AsRef<[u8]>) -> Result<String, SysError> {
799 let bytes = var_bytes(key)?;
800 String::from_utf8(bytes).map_err(|e| SysError::ApiError(e.to_string()))
801 }
802}
803
804pub mod time {
806 use super::*;
807
808 pub fn now_ms() -> Result<u64, SysError> {
813 let bytes = unsafe { astrid_clock_ms()? };
814 let s = String::from_utf8_lossy(&bytes);
815 s.trim()
816 .parse::<u64>()
817 .map_err(|e| SysError::ApiError(format!("clock_ms parse error: {e}")))
818 }
819}
820
821pub mod log {
823 use super::*;
824
825 pub fn log(level: impl AsRef<[u8]>, message: impl AsRef<[u8]>) -> Result<(), SysError> {
827 unsafe { astrid_log(level.as_ref().to_vec(), message.as_ref().to_vec())? };
828 Ok(())
829 }
830
831 pub fn debug(message: impl AsRef<[u8]>) -> Result<(), SysError> {
833 log("debug", message)
834 }
835
836 pub fn info(message: impl AsRef<[u8]>) -> Result<(), SysError> {
838 log("info", message)
839 }
840
841 pub fn warn(message: impl AsRef<[u8]>) -> Result<(), SysError> {
843 log("warn", message)
844 }
845
846 pub fn error(message: impl AsRef<[u8]>) -> Result<(), SysError> {
848 log("error", message)
849 }
850}
851
852pub mod runtime {
854 use super::*;
855
856 pub fn signal_ready() -> Result<(), SysError> {
862 unsafe { astrid_signal_ready()? };
863 Ok(())
864 }
865
866 pub fn caller() -> Result<crate::types::CallerContext, SysError> {
868 let bytes = unsafe { astrid_get_caller()? };
869 serde_json::from_slice(&bytes)
870 .map_err(|e| SysError::ApiError(format!("failed to parse caller context: {e}")))
871 }
872
873 pub fn socket_path() -> Result<String, SysError> {
878 let raw = crate::env::var(crate::env::CONFIG_SOCKET_PATH)?;
879 let path = serde_json::from_str::<String>(raw.trim()).or_else(|_| {
882 if raw.is_empty() {
884 Err(SysError::ApiError(
885 "ASTRID_SOCKET_PATH config key is empty".to_string(),
886 ))
887 } else {
888 Ok(raw)
889 }
890 })?;
891 if path.contains('\0') {
893 return Err(SysError::ApiError(
894 "ASTRID_SOCKET_PATH contains null byte".to_string(),
895 ));
896 }
897 Ok(path)
898 }
899}
900
901pub mod hooks {
903 use super::*;
904
905 pub fn trigger(event_bytes: &[u8]) -> Result<Vec<u8>, SysError> {
906 unsafe { Ok(astrid_trigger_hook(event_bytes.to_vec())?) }
907 }
908}
909
910pub mod capabilities {
916 use super::*;
917
918 pub fn check(source_uuid: &str, capability: &str) -> Result<bool, SysError> {
924 let request = serde_json::json!({
925 "source_uuid": source_uuid,
926 "capability": capability,
927 });
928 let request_bytes = serde_json::to_vec(&request)?;
929 let response_bytes = unsafe { astrid_check_capsule_capability(request_bytes)? };
930 let response: serde_json::Value = serde_json::from_slice(&response_bytes)?;
931 Ok(response["allowed"].as_bool().unwrap_or(false))
932 }
933}
934
935pub mod net;
936pub mod process {
937 use super::*;
938 use serde::{Deserialize, Serialize};
939
940 #[derive(Debug, Serialize)]
942 pub struct ProcessRequest<'a> {
943 pub cmd: &'a str,
944 pub args: &'a [&'a str],
945 }
946
947 #[derive(Debug, Deserialize)]
949 pub struct ProcessResult {
950 pub stdout: String,
951 pub stderr: String,
952 pub exit_code: i32,
953 }
954
955 pub fn spawn(cmd: &str, args: &[&str]) -> Result<ProcessResult, SysError> {
958 let req = ProcessRequest { cmd, args };
959 let req_bytes = serde_json::to_vec(&req)?;
960 let result_bytes = unsafe { astrid_spawn_host(req_bytes)? };
961 let result: ProcessResult = serde_json::from_slice(&result_bytes)?;
962 Ok(result)
963 }
964
965 #[derive(Debug, Deserialize)]
971 pub struct BackgroundProcessHandle {
972 pub id: u64,
974 }
975
976 #[derive(Debug, Deserialize)]
978 pub struct ProcessLogs {
979 pub stdout: String,
981 pub stderr: String,
983 pub running: bool,
985 pub exit_code: Option<i32>,
987 }
988
989 #[derive(Debug, Deserialize)]
991 pub struct KillResult {
992 pub killed: bool,
994 pub exit_code: Option<i32>,
996 pub stdout: String,
998 pub stderr: String,
1000 }
1001
1002 pub fn spawn_background(cmd: &str, args: &[&str]) -> Result<BackgroundProcessHandle, SysError> {
1007 let req = ProcessRequest { cmd, args };
1008 let req_bytes = serde_json::to_vec(&req)?;
1009 let result_bytes = unsafe { astrid_spawn_background_host(req_bytes)? };
1010 let result: BackgroundProcessHandle = serde_json::from_slice(&result_bytes)?;
1011 Ok(result)
1012 }
1013
1014 pub fn read_logs(id: u64) -> Result<ProcessLogs, SysError> {
1019 #[derive(Serialize)]
1020 struct Req {
1021 id: u64,
1022 }
1023 let req_bytes = serde_json::to_vec(&Req { id })?;
1024 let result_bytes = unsafe { astrid_read_process_logs_host(req_bytes)? };
1025 let result: ProcessLogs = serde_json::from_slice(&result_bytes)?;
1026 Ok(result)
1027 }
1028
1029 pub fn kill(id: u64) -> Result<KillResult, SysError> {
1033 #[derive(Serialize)]
1034 struct Req {
1035 id: u64,
1036 }
1037 let req_bytes = serde_json::to_vec(&Req { id })?;
1038 let result_bytes = unsafe { astrid_kill_process_host(req_bytes)? };
1039 let result: KillResult = serde_json::from_slice(&result_bytes)?;
1040 Ok(result)
1041 }
1042}
1043
1044pub mod elicit {
1050 use super::*;
1051
1052 #[derive(Serialize)]
1054 struct ElicitRequest<'a> {
1055 #[serde(rename = "type")]
1056 kind: &'a str,
1057 key: &'a str,
1058 #[serde(skip_serializing_if = "Option::is_none")]
1059 description: Option<&'a str>,
1060 #[serde(skip_serializing_if = "Option::is_none")]
1061 options: Option<&'a [&'a str]>,
1062 #[serde(skip_serializing_if = "Option::is_none")]
1063 default: Option<&'a str>,
1064 }
1065
1066 fn validate_key(key: &str) -> Result<(), SysError> {
1068 if key.trim().is_empty() {
1069 return Err(SysError::ApiError("elicit key must not be empty".into()));
1070 }
1071 Ok(())
1072 }
1073
1074 pub fn secret(key: &str, description: &str) -> Result<(), SysError> {
1077 validate_key(key)?;
1078 let req = ElicitRequest {
1079 kind: "secret",
1080 key,
1081 description: Some(description),
1082 options: None,
1083 default: None,
1084 };
1085 let req_bytes = serde_json::to_vec(&req)?;
1086 let resp_bytes = unsafe { astrid_elicit(req_bytes)? };
1089
1090 #[derive(serde::Deserialize)]
1091 struct SecretResp {
1092 ok: bool,
1093 }
1094 let resp: SecretResp = serde_json::from_slice(&resp_bytes)?;
1095 if !resp.ok {
1096 return Err(SysError::ApiError(
1097 "kernel did not confirm secret storage".into(),
1098 ));
1099 }
1100 Ok(())
1101 }
1102
1103 pub fn has_secret(key: &str) -> Result<bool, SysError> {
1105 validate_key(key)?;
1106 #[derive(Serialize)]
1107 struct HasSecretRequest<'a> {
1108 key: &'a str,
1109 }
1110 let req_bytes = serde_json::to_vec(&HasSecretRequest { key })?;
1111 let resp_bytes = unsafe { astrid_has_secret(req_bytes)? };
1114
1115 #[derive(serde::Deserialize)]
1116 struct ExistsResp {
1117 exists: bool,
1118 }
1119 let resp: ExistsResp = serde_json::from_slice(&resp_bytes)?;
1120 Ok(resp.exists)
1121 }
1122
1123 fn elicit_text(
1125 key: &str,
1126 description: &str,
1127 default: Option<&str>,
1128 ) -> Result<String, SysError> {
1129 validate_key(key)?;
1130 let req = ElicitRequest {
1131 kind: "text",
1132 key,
1133 description: Some(description),
1134 options: None,
1135 default,
1136 };
1137 let req_bytes = serde_json::to_vec(&req)?;
1138 let resp_bytes = unsafe { astrid_elicit(req_bytes)? };
1141
1142 #[derive(serde::Deserialize)]
1143 struct TextResp {
1144 value: String,
1145 }
1146 let resp: TextResp = serde_json::from_slice(&resp_bytes)?;
1147 Ok(resp.value)
1148 }
1149
1150 pub fn text(key: &str, description: &str) -> Result<String, SysError> {
1153 elicit_text(key, description, None)
1154 }
1155
1156 pub fn text_with_default(
1158 key: &str,
1159 description: &str,
1160 default: &str,
1161 ) -> Result<String, SysError> {
1162 elicit_text(key, description, Some(default))
1163 }
1164
1165 pub fn select(key: &str, description: &str, options: &[&str]) -> Result<String, SysError> {
1167 validate_key(key)?;
1168 if options.is_empty() {
1169 return Err(SysError::ApiError(
1170 "select requires at least one option".into(),
1171 ));
1172 }
1173 let req = ElicitRequest {
1174 kind: "select",
1175 key,
1176 description: Some(description),
1177 options: Some(options),
1178 default: None,
1179 };
1180 let req_bytes = serde_json::to_vec(&req)?;
1181 let resp_bytes = unsafe { astrid_elicit(req_bytes)? };
1184
1185 #[derive(serde::Deserialize)]
1186 struct SelectResp {
1187 value: String,
1188 }
1189 let resp: SelectResp = serde_json::from_slice(&resp_bytes)?;
1190 if !options.iter().any(|o| *o == resp.value) {
1191 let truncated: String = resp.value.chars().take(64).collect();
1192 return Err(SysError::ApiError(format!(
1193 "host returned value '{truncated}' not in provided options",
1194 )));
1195 }
1196 Ok(resp.value)
1197 }
1198
1199 pub fn array(key: &str, description: &str) -> Result<Vec<String>, SysError> {
1201 validate_key(key)?;
1202 let req = ElicitRequest {
1203 kind: "array",
1204 key,
1205 description: Some(description),
1206 options: None,
1207 default: None,
1208 };
1209 let req_bytes = serde_json::to_vec(&req)?;
1210 let resp_bytes = unsafe { astrid_elicit(req_bytes)? };
1213
1214 #[derive(serde::Deserialize)]
1215 struct ArrayResp {
1216 values: Vec<String>,
1217 }
1218 let resp: ArrayResp = serde_json::from_slice(&resp_bytes)?;
1219 Ok(resp.values)
1220 }
1221}
1222
1223pub mod interceptors {
1230 use super::*;
1231
1232 #[derive(Debug, serde::Deserialize)]
1234 pub struct InterceptorBinding {
1235 pub handle_id: u64,
1237 pub action: String,
1239 pub topic: String,
1241 }
1242
1243 impl InterceptorBinding {
1244 #[must_use]
1246 pub fn subscription_handle(&self) -> ipc::SubscriptionHandle {
1247 ipc::SubscriptionHandle(self.handle_id.to_string().into_bytes())
1248 }
1249
1250 #[must_use]
1252 pub fn handle_bytes(&self) -> Vec<u8> {
1253 self.handle_id.to_string().into_bytes()
1254 }
1255 }
1256
1257 pub fn bindings() -> Result<Vec<InterceptorBinding>, SysError> {
1262 let bytes = unsafe { astrid_get_interceptor_handles()? };
1266 let bindings: Vec<InterceptorBinding> = serde_json::from_slice(&bytes)?;
1267 Ok(bindings)
1268 }
1269
1270 pub fn poll(
1277 bindings: &[InterceptorBinding],
1278 mut handler: impl FnMut(&str, &[u8]),
1279 ) -> Result<(), SysError> {
1280 #[derive(serde::Deserialize)]
1281 struct PollEnvelope {
1282 messages: Vec<serde_json::Value>,
1283 }
1284
1285 for binding in bindings {
1286 let handle = binding.subscription_handle();
1287 let envelope = ipc::poll_bytes(&handle)?;
1288
1289 let parsed: PollEnvelope = serde_json::from_slice(&envelope)?;
1293 if !parsed.messages.is_empty() {
1294 handler(&binding.action, &envelope);
1295 }
1296 }
1297 Ok(())
1298 }
1299}
1300
1301pub mod identity {
1329 use super::*;
1330
1331 #[derive(Debug)]
1333 pub struct ResolvedUser {
1334 pub user_id: String,
1336 pub display_name: Option<String>,
1338 }
1339
1340 #[derive(Debug)]
1342 pub struct Link {
1343 pub platform: String,
1345 pub platform_user_id: String,
1347 pub astrid_user_id: String,
1349 pub linked_at: String,
1351 pub method: String,
1353 }
1354
1355 pub fn resolve(
1360 platform: &str,
1361 platform_user_id: &str,
1362 ) -> Result<Option<ResolvedUser>, SysError> {
1363 #[derive(Serialize)]
1364 struct Req<'a> {
1365 platform: &'a str,
1366 platform_user_id: &'a str,
1367 }
1368
1369 let req_bytes = serde_json::to_vec(&Req {
1370 platform,
1371 platform_user_id,
1372 })?;
1373
1374 let resp_bytes = unsafe { astrid_identity_resolve(req_bytes)? };
1376
1377 #[derive(Deserialize)]
1378 struct Resp {
1379 found: bool,
1380 user_id: Option<String>,
1381 display_name: Option<String>,
1382 error: Option<String>,
1383 }
1384 let resp: Resp = serde_json::from_slice(&resp_bytes)?;
1385 if resp.found {
1386 let user_id = resp.user_id.ok_or_else(|| {
1387 SysError::ApiError("host returned found=true but user_id was missing".into())
1388 })?;
1389 Ok(Some(ResolvedUser {
1390 user_id,
1391 display_name: resp.display_name,
1392 }))
1393 } else if let Some(err) = resp.error {
1394 Err(SysError::ApiError(err))
1395 } else {
1396 Ok(None)
1397 }
1398 }
1399
1400 pub fn link(
1406 platform: &str,
1407 platform_user_id: &str,
1408 astrid_user_id: &str,
1409 method: &str,
1410 ) -> Result<Link, SysError> {
1411 #[derive(Serialize)]
1412 struct Req<'a> {
1413 platform: &'a str,
1414 platform_user_id: &'a str,
1415 astrid_user_id: &'a str,
1416 method: &'a str,
1417 }
1418
1419 let req_bytes = serde_json::to_vec(&Req {
1420 platform,
1421 platform_user_id,
1422 astrid_user_id,
1423 method,
1424 })?;
1425
1426 let resp_bytes = unsafe { astrid_identity_link(req_bytes)? };
1428
1429 #[derive(Deserialize)]
1430 struct LinkInfo {
1431 platform: String,
1432 platform_user_id: String,
1433 astrid_user_id: String,
1434 linked_at: String,
1435 method: String,
1436 }
1437 #[derive(Deserialize)]
1438 struct Resp {
1439 ok: bool,
1440 error: Option<String>,
1441 link: Option<LinkInfo>,
1442 }
1443 let resp: Resp = serde_json::from_slice(&resp_bytes)?;
1444 if !resp.ok {
1445 return Err(SysError::ApiError(
1446 resp.error.unwrap_or_else(|| "identity link failed".into()),
1447 ));
1448 }
1449 let l = resp
1450 .link
1451 .ok_or_else(|| SysError::ApiError("missing link in response".into()))?;
1452 Ok(Link {
1453 platform: l.platform,
1454 platform_user_id: l.platform_user_id,
1455 astrid_user_id: l.astrid_user_id,
1456 linked_at: l.linked_at,
1457 method: l.method,
1458 })
1459 }
1460
1461 pub fn unlink(platform: &str, platform_user_id: &str) -> Result<bool, SysError> {
1466 #[derive(Serialize)]
1467 struct Req<'a> {
1468 platform: &'a str,
1469 platform_user_id: &'a str,
1470 }
1471
1472 let req_bytes = serde_json::to_vec(&Req {
1473 platform,
1474 platform_user_id,
1475 })?;
1476
1477 let resp_bytes = unsafe { astrid_identity_unlink(req_bytes)? };
1479
1480 #[derive(Deserialize)]
1481 struct Resp {
1482 ok: bool,
1483 error: Option<String>,
1484 removed: Option<bool>,
1485 }
1486 let resp: Resp = serde_json::from_slice(&resp_bytes)?;
1487 if !resp.ok {
1488 return Err(SysError::ApiError(
1489 resp.error
1490 .unwrap_or_else(|| "identity unlink failed".into()),
1491 ));
1492 }
1493 Ok(resp.removed.unwrap_or(false))
1494 }
1495
1496 pub fn create_user(display_name: Option<&str>) -> Result<String, SysError> {
1501 #[derive(Serialize)]
1502 struct Req<'a> {
1503 display_name: Option<&'a str>,
1504 }
1505
1506 let req_bytes = serde_json::to_vec(&Req { display_name })?;
1507
1508 let resp_bytes = unsafe { astrid_identity_create_user(req_bytes)? };
1510
1511 #[derive(Deserialize)]
1512 struct Resp {
1513 ok: bool,
1514 error: Option<String>,
1515 user_id: Option<String>,
1516 }
1517 let resp: Resp = serde_json::from_slice(&resp_bytes)?;
1518 if !resp.ok {
1519 return Err(SysError::ApiError(
1520 resp.error
1521 .unwrap_or_else(|| "identity create_user failed".into()),
1522 ));
1523 }
1524 resp.user_id
1525 .ok_or_else(|| SysError::ApiError("missing user_id in response".into()))
1526 }
1527
1528 pub fn list_links(astrid_user_id: &str) -> Result<Vec<Link>, SysError> {
1533 #[derive(Serialize)]
1534 struct Req<'a> {
1535 astrid_user_id: &'a str,
1536 }
1537
1538 let req_bytes = serde_json::to_vec(&Req { astrid_user_id })?;
1539
1540 let resp_bytes = unsafe { astrid_identity_list_links(req_bytes)? };
1542
1543 #[derive(Deserialize)]
1544 struct LinkInfo {
1545 platform: String,
1546 platform_user_id: String,
1547 astrid_user_id: String,
1548 linked_at: String,
1549 method: String,
1550 }
1551 #[derive(Deserialize)]
1552 struct Resp {
1553 ok: bool,
1554 error: Option<String>,
1555 links: Option<Vec<LinkInfo>>,
1556 }
1557 let resp: Resp = serde_json::from_slice(&resp_bytes)?;
1558 if !resp.ok {
1559 return Err(SysError::ApiError(
1560 resp.error
1561 .unwrap_or_else(|| "identity list_links failed".into()),
1562 ));
1563 }
1564 Ok(resp
1565 .links
1566 .unwrap_or_default()
1567 .into_iter()
1568 .map(|l| Link {
1569 platform: l.platform,
1570 platform_user_id: l.platform_user_id,
1571 astrid_user_id: l.astrid_user_id,
1572 linked_at: l.linked_at,
1573 method: l.method,
1574 })
1575 .collect())
1576 }
1577}
1578
1579pub mod approval {
1580 use super::*;
1581
1582 #[derive(Debug)]
1584 pub struct ApprovalResult {
1585 pub approved: bool,
1587 pub decision: String,
1590 }
1591
1592 pub fn request(
1602 action: &str,
1603 resource: &str,
1604 risk_level: &str,
1605 ) -> Result<ApprovalResult, SysError> {
1606 #[derive(Serialize)]
1607 struct ApprovalRequest<'a> {
1608 action: &'a str,
1609 resource: &'a str,
1610 risk_level: &'a str,
1611 }
1612
1613 let req = ApprovalRequest {
1614 action,
1615 resource,
1616 risk_level,
1617 };
1618 let req_bytes = serde_json::to_vec(&req)?;
1619
1620 let resp_bytes = unsafe { astrid_request_approval(req_bytes)? };
1624
1625 #[derive(Deserialize)]
1626 struct ApprovalResp {
1627 approved: bool,
1628 decision: String,
1629 }
1630 let resp: ApprovalResp = serde_json::from_slice(&resp_bytes)?;
1631 Ok(ApprovalResult {
1632 approved: resp.approved,
1633 decision: resp.decision,
1634 })
1635 }
1636}
1637
1638pub mod prelude {
1639 pub use crate::{
1640 SysError,
1641 approval,
1643 capabilities,
1644 cron,
1645 elicit,
1646 env,
1648 fs,
1649 hooks,
1650 http,
1651 identity,
1652 interceptors,
1653 ipc,
1654 kv,
1655 log,
1656 net,
1657 process,
1658 runtime,
1659 time,
1660 uplink,
1661 };
1662
1663 #[cfg(feature = "derive")]
1664 pub use astrid_sdk_macros::capsule;
1665}