1#![allow(unsafe_code)]
37#![allow(missing_docs)]
38#![deny(clippy::all)]
39#![deny(unreachable_pub)]
40#![deny(clippy::unwrap_used)]
41#![cfg_attr(test, allow(clippy::unwrap_used))]
42
43use astrid_sys::*;
44use borsh::{BorshDeserialize, BorshSerialize};
45use serde::{Deserialize, Serialize, de::DeserializeOwned};
46use thiserror::Error;
47
48pub use borsh;
49pub use serde;
50pub use serde_json;
51
52#[doc(hidden)]
55pub use extism_pdk;
56#[doc(hidden)]
57pub use schemars;
58
59#[derive(Error, Debug)]
61pub enum SysError {
62 #[error("Host function call failed: {0}")]
63 HostError(#[from] extism_pdk::Error),
64 #[error("JSON serialization error: {0}")]
65 JsonError(#[from] serde_json::Error),
66 #[error("MessagePack serialization error: {0}")]
67 MsgPackEncodeError(#[from] rmp_serde::encode::Error),
68 #[error("MessagePack deserialization error: {0}")]
69 MsgPackDecodeError(#[from] rmp_serde::decode::Error),
70 #[error("Borsh serialization error: {0}")]
71 BorshError(#[from] std::io::Error),
72 #[error("API logic error: {0}")]
73 ApiError(String),
74}
75
76pub mod fs {
78 use super::*;
79
80 pub fn exists(path: impl AsRef<[u8]>) -> Result<bool, SysError> {
82 let result = unsafe { astrid_fs_exists(path.as_ref().to_vec())? };
83 Ok(!result.is_empty() && result[0] != 0)
84 }
85
86 pub fn read(path: impl AsRef<[u8]>) -> Result<Vec<u8>, SysError> {
88 let result = unsafe { astrid_read_file(path.as_ref().to_vec())? };
89 Ok(result)
90 }
91
92 pub fn read_to_string(path: impl AsRef<[u8]>) -> Result<String, SysError> {
94 let bytes = read(path)?;
95 String::from_utf8(bytes).map_err(|e| SysError::ApiError(e.to_string()))
96 }
97
98 pub fn write(path: impl AsRef<[u8]>, contents: impl AsRef<[u8]>) -> Result<(), SysError> {
100 unsafe { astrid_write_file(path.as_ref().to_vec(), contents.as_ref().to_vec())? };
101 Ok(())
102 }
103
104 pub fn create_dir(path: impl AsRef<[u8]>) -> Result<(), SysError> {
106 unsafe { astrid_fs_mkdir(path.as_ref().to_vec())? };
107 Ok(())
108 }
109
110 pub fn read_dir(path: impl AsRef<[u8]>) -> Result<Vec<u8>, SysError> {
112 let result = unsafe { astrid_fs_readdir(path.as_ref().to_vec())? };
113 Ok(result)
114 }
115
116 pub fn metadata(path: impl AsRef<[u8]>) -> Result<Vec<u8>, SysError> {
118 let result = unsafe { astrid_fs_stat(path.as_ref().to_vec())? };
119 Ok(result)
120 }
121
122 pub fn remove_file(path: impl AsRef<[u8]>) -> Result<(), SysError> {
124 unsafe { astrid_fs_unlink(path.as_ref().to_vec())? };
125 Ok(())
126 }
127}
128
129pub mod ipc {
131 use super::*;
132
133 #[derive(Debug, Clone)]
137 pub struct SubscriptionHandle(pub(crate) Vec<u8>);
138
139 impl SubscriptionHandle {
140 #[must_use]
142 pub fn as_bytes(&self) -> &[u8] {
143 &self.0
144 }
145 }
146
147 impl AsRef<[u8]> for SubscriptionHandle {
149 fn as_ref(&self) -> &[u8] {
150 &self.0
151 }
152 }
153
154 pub fn publish_bytes(topic: impl AsRef<[u8]>, payload: &[u8]) -> Result<(), SysError> {
155 unsafe { astrid_ipc_publish(topic.as_ref().to_vec(), payload.to_vec())? };
156 Ok(())
157 }
158
159 pub fn publish_json<T: Serialize>(
160 topic: impl AsRef<[u8]>,
161 payload: &T,
162 ) -> Result<(), SysError> {
163 let bytes = serde_json::to_vec(payload)?;
164 publish_bytes(topic, &bytes)
165 }
166
167 pub fn publish_msgpack<T: Serialize>(
168 topic: impl AsRef<[u8]>,
169 payload: &T,
170 ) -> Result<(), SysError> {
171 let bytes = rmp_serde::to_vec_named(payload)?;
172 publish_bytes(topic, &bytes)
173 }
174
175 pub fn subscribe(topic: impl AsRef<[u8]>) -> Result<SubscriptionHandle, SysError> {
177 let handle_bytes = unsafe { astrid_ipc_subscribe(topic.as_ref().to_vec())? };
178 Ok(SubscriptionHandle(handle_bytes))
179 }
180
181 pub fn unsubscribe(handle: &SubscriptionHandle) -> Result<(), SysError> {
182 unsafe { astrid_ipc_unsubscribe(handle.0.clone())? };
183 Ok(())
184 }
185
186 pub fn poll_bytes(handle: &SubscriptionHandle) -> Result<Vec<u8>, SysError> {
187 let message_bytes = unsafe { astrid_ipc_poll(handle.0.clone())? };
188 Ok(message_bytes)
189 }
190
191 pub fn recv_bytes(handle: &SubscriptionHandle, timeout_ms: u64) -> Result<Vec<u8>, SysError> {
197 let timeout_str = timeout_ms.to_string();
198 let message_bytes = unsafe { astrid_ipc_recv(handle.0.clone(), timeout_str.into_bytes())? };
199 Ok(message_bytes)
200 }
201}
202
203pub mod uplink {
205 use super::*;
206
207 #[derive(Debug, Clone)]
209 pub struct UplinkId(pub(crate) Vec<u8>);
210
211 impl UplinkId {
212 #[must_use]
214 pub fn as_bytes(&self) -> &[u8] {
215 &self.0
216 }
217 }
218
219 impl AsRef<[u8]> for UplinkId {
220 fn as_ref(&self) -> &[u8] {
221 &self.0
222 }
223 }
224
225 pub fn register(
227 name: impl AsRef<[u8]>,
228 platform: impl AsRef<[u8]>,
229 profile: impl AsRef<[u8]>,
230 ) -> Result<UplinkId, SysError> {
231 let id_bytes = unsafe {
232 astrid_uplink_register(
233 name.as_ref().to_vec(),
234 platform.as_ref().to_vec(),
235 profile.as_ref().to_vec(),
236 )?
237 };
238 Ok(UplinkId(id_bytes))
239 }
240
241 pub fn send_bytes(
243 uplink_id: &UplinkId,
244 platform_user_id: impl AsRef<[u8]>,
245 content: &[u8],
246 ) -> Result<Vec<u8>, SysError> {
247 let result = unsafe {
248 astrid_uplink_send(
249 uplink_id.0.clone(),
250 platform_user_id.as_ref().to_vec(),
251 content.to_vec(),
252 )?
253 };
254 Ok(result)
255 }
256}
257
258pub mod kv {
260 use super::*;
261
262 pub fn get_bytes(key: impl AsRef<[u8]>) -> Result<Vec<u8>, SysError> {
263 let result = unsafe { astrid_kv_get(key.as_ref().to_vec())? };
264 Ok(result)
265 }
266
267 pub fn set_bytes(key: impl AsRef<[u8]>, value: &[u8]) -> Result<(), SysError> {
268 unsafe { astrid_kv_set(key.as_ref().to_vec(), value.to_vec())? };
269 Ok(())
270 }
271
272 pub fn get_json<T: DeserializeOwned>(key: impl AsRef<[u8]>) -> Result<T, SysError> {
273 let bytes = get_bytes(key)?;
274 let parsed = serde_json::from_slice(&bytes)?;
275 Ok(parsed)
276 }
277
278 pub fn set_json<T: Serialize>(key: impl AsRef<[u8]>, value: &T) -> Result<(), SysError> {
279 let bytes = serde_json::to_vec(value)?;
280 set_bytes(key, &bytes)
281 }
282
283 pub fn delete(key: impl AsRef<[u8]>) -> Result<(), SysError> {
289 unsafe { astrid_kv_delete(key.as_ref().to_vec())? };
290 Ok(())
291 }
292
293 pub fn list_keys(prefix: impl AsRef<[u8]>) -> Result<Vec<String>, SysError> {
298 let result = unsafe { astrid_kv_list_keys(prefix.as_ref().to_vec())? };
299 let keys: Vec<String> = serde_json::from_slice(&result)?;
300 Ok(keys)
301 }
302
303 pub fn clear_prefix(prefix: impl AsRef<[u8]>) -> Result<u64, SysError> {
308 let result = unsafe { astrid_kv_clear_prefix(prefix.as_ref().to_vec())? };
309 let count: u64 = serde_json::from_slice(&result)?;
310 Ok(count)
311 }
312
313 pub fn get_borsh<T: BorshDeserialize>(key: impl AsRef<[u8]>) -> Result<T, SysError> {
314 let bytes = get_bytes(key)?;
315 let parsed = borsh::from_slice(&bytes)?;
316 Ok(parsed)
317 }
318
319 pub fn set_borsh<T: BorshSerialize>(key: impl AsRef<[u8]>, value: &T) -> Result<(), SysError> {
320 let bytes = borsh::to_vec(value)?;
321 set_bytes(key, &bytes)
322 }
323
324 #[derive(Serialize, Deserialize)]
332 struct VersionedEnvelope<T> {
333 #[serde(rename = "__sv")]
334 schema_version: u32,
335 data: T,
336 }
337
338 #[derive(Debug)]
340 pub enum Versioned<T> {
341 Current(T),
343 NeedsMigration {
345 raw: serde_json::Value,
347 stored_version: u32,
349 },
350 Unversioned(serde_json::Value),
352 NotFound,
354 }
355
356 pub fn set_versioned<T: Serialize>(
361 key: impl AsRef<[u8]>,
362 value: &T,
363 version: u32,
364 ) -> Result<(), SysError> {
365 let envelope = VersionedEnvelope {
366 schema_version: version,
367 data: value,
368 };
369 set_json(key, &envelope)
370 }
371
372 pub fn get_versioned<T: DeserializeOwned>(
383 key: impl AsRef<[u8]>,
384 current_version: u32,
385 ) -> Result<Versioned<T>, SysError> {
386 let bytes = get_bytes(&key)?;
387 parse_versioned(&bytes, current_version)
388 }
389
390 fn parse_versioned<T: DeserializeOwned>(
393 bytes: &[u8],
394 current_version: u32,
395 ) -> Result<Versioned<T>, SysError> {
396 if bytes.is_empty() {
400 return Ok(Versioned::NotFound);
401 }
402
403 let mut value: serde_json::Value = serde_json::from_slice(bytes)?;
404
405 let sv_field = value.get("__sv");
409 let has_sv = sv_field.is_some();
410 let envelope_version = sv_field.and_then(|v| v.as_u64());
411 let has_data = value.get("data").is_some();
412
413 match (has_sv, envelope_version, has_data) {
414 (_, Some(v), true) => {
417 let v = u32::try_from(v)
418 .map_err(|_| SysError::ApiError("schema version exceeds u32::MAX".into()))?;
419 let data = value
422 .as_object_mut()
423 .and_then(|m| m.remove("data"))
424 .expect("data field guaranteed by match condition");
425 if v == current_version {
426 let parsed: T = serde_json::from_value(data)?;
427 Ok(Versioned::Current(parsed))
428 } else if v < current_version {
429 Ok(Versioned::NeedsMigration {
430 raw: data,
431 stored_version: v,
432 })
433 } else {
434 Err(SysError::ApiError(format!(
435 "stored schema version {v} is newer than current \
436 version {current_version} - cannot safely read"
437 )))
438 }
439 },
440 (true, _, _) => Err(SysError::ApiError(
442 "malformed versioned envelope: __sv field present but \
443 data field missing or __sv is not a number"
444 .into(),
445 )),
446 (false, _, _) => Ok(Versioned::Unversioned(value)),
448 }
449 }
450
451 pub fn get_versioned_or_migrate<T: Serialize + DeserializeOwned>(
466 key: impl AsRef<[u8]>,
467 current_version: u32,
468 migrate_fn: impl FnOnce(serde_json::Value, u32) -> Result<T, SysError>,
469 ) -> Result<Option<T>, SysError> {
470 let key = key.as_ref();
471
472 match get_versioned::<T>(key, current_version)? {
473 Versioned::Current(data) => Ok(Some(data)),
474 Versioned::NeedsMigration {
475 raw,
476 stored_version,
477 } => {
478 let migrated = migrate_fn(raw, stored_version)?;
479 set_versioned(key, &migrated, current_version)?;
480 Ok(Some(migrated))
481 },
482 Versioned::Unversioned(raw) => {
483 let migrated = migrate_fn(raw, 0)?;
484 set_versioned(key, &migrated, current_version)?;
485 Ok(Some(migrated))
486 },
487 Versioned::NotFound => Ok(None),
488 }
489 }
490
491 #[cfg(test)]
492 mod tests {
493 use super::*;
494
495 #[derive(Debug, Serialize, Deserialize, PartialEq)]
496 struct TestData {
497 name: String,
498 count: u32,
499 }
500
501 #[test]
504 fn versioned_envelope_roundtrip() {
505 let envelope = VersionedEnvelope {
506 schema_version: 1,
507 data: TestData {
508 name: "hello".into(),
509 count: 42,
510 },
511 };
512 let json = serde_json::to_string(&envelope).unwrap();
513 assert!(json.contains("\"__sv\":1"));
514 assert!(json.contains("\"data\":{"));
515
516 let parsed: VersionedEnvelope<TestData> = serde_json::from_str(&json).unwrap();
517 assert_eq!(parsed.schema_version, 1);
518 assert_eq!(
519 parsed.data,
520 TestData {
521 name: "hello".into(),
522 count: 42,
523 }
524 );
525 }
526
527 #[test]
528 fn versioned_envelope_wire_format() {
529 let envelope = VersionedEnvelope {
530 schema_version: 3,
531 data: serde_json::json!({"key": "value"}),
532 };
533 let json = serde_json::to_string(&envelope).unwrap();
534 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
535
536 assert_eq!(parsed["__sv"], 3);
537 assert_eq!(parsed["data"]["key"], "value");
538 }
539
540 #[test]
543 fn parse_versioned_empty_bytes_returns_not_found() {
544 let result = parse_versioned::<TestData>(b"", 1).unwrap();
545 assert!(matches!(result, Versioned::NotFound));
546 }
547
548 #[test]
549 fn parse_versioned_current_version_returns_current() {
550 let bytes = br#"{"__sv":2,"data":{"name":"hello","count":42}}"#;
551 let result = parse_versioned::<TestData>(bytes, 2).unwrap();
552 match result {
553 Versioned::Current(data) => {
554 assert_eq!(data.name, "hello");
555 assert_eq!(data.count, 42);
556 },
557 other => panic!("expected Current, got {other:?}"),
558 }
559 }
560
561 #[test]
562 fn parse_versioned_older_version_returns_needs_migration() {
563 let bytes = br#"{"__sv":1,"data":{"name":"old","count":1}}"#;
564 let result = parse_versioned::<TestData>(bytes, 3).unwrap();
565 match result {
566 Versioned::NeedsMigration {
567 raw,
568 stored_version,
569 } => {
570 assert_eq!(stored_version, 1);
571 assert_eq!(raw["name"], "old");
572 assert_eq!(raw["count"], 1);
573 },
574 other => panic!("expected NeedsMigration, got {other:?}"),
575 }
576 }
577
578 #[test]
579 fn parse_versioned_newer_version_returns_error() {
580 let bytes = br#"{"__sv":5,"data":{"name":"future","count":0}}"#;
581 let result = parse_versioned::<TestData>(bytes, 2);
582 assert!(result.is_err());
583 let err = result.unwrap_err().to_string();
584 assert!(
585 err.contains("newer than current"),
586 "error should mention newer version: {err}"
587 );
588 }
589
590 #[test]
591 fn parse_versioned_plain_json_returns_unversioned() {
592 let bytes = br#"{"name":"legacy","count":99}"#;
593 let result = parse_versioned::<TestData>(bytes, 1).unwrap();
594 match result {
595 Versioned::Unversioned(val) => {
596 assert_eq!(val["name"], "legacy");
597 assert_eq!(val["count"], 99);
598 },
599 other => panic!("expected Unversioned, got {other:?}"),
600 }
601 }
602
603 #[test]
604 fn parse_versioned_malformed_sv_without_data_returns_error() {
605 let bytes = br#"{"__sv":1,"payload":"something"}"#;
606 let result = parse_versioned::<TestData>(bytes, 1);
607 assert!(result.is_err());
608 let err = result.unwrap_err().to_string();
609 assert!(
610 err.contains("malformed"),
611 "error should mention malformed envelope: {err}"
612 );
613 }
614
615 #[test]
616 fn parse_versioned_non_numeric_sv_returns_error() {
617 let bytes = br#"{"__sv":"one","data":{}}"#;
618 let result = parse_versioned::<TestData>(bytes, 1);
619 assert!(result.is_err());
620 let err = result.unwrap_err().to_string();
621 assert!(
622 err.contains("malformed"),
623 "error should mention malformed envelope: {err}"
624 );
625 }
626
627 #[test]
628 fn parse_versioned_version_zero_is_valid() {
629 let bytes = br#"{"__sv":0,"data":{"name":"v0","count":0}}"#;
631 let result = parse_versioned::<TestData>(bytes, 0).unwrap();
632 assert!(matches!(result, Versioned::Current(_)));
633 }
634
635 #[test]
636 fn parse_versioned_invalid_json_returns_error() {
637 let result = parse_versioned::<TestData>(b"not json", 1);
638 assert!(result.is_err());
639 }
640 }
641}
642
643pub mod http {
645 use super::*;
646
647 pub fn request_bytes(request_bytes: &[u8]) -> Result<Vec<u8>, SysError> {
650 let result = unsafe { astrid_http_request(request_bytes.to_vec())? };
651 Ok(result)
652 }
653}
654
655pub mod cron {
657 use super::*;
658
659 pub fn schedule(
661 name: impl AsRef<[u8]>,
662 schedule: impl AsRef<[u8]>,
663 payload: &[u8],
664 ) -> Result<(), SysError> {
665 unsafe {
666 astrid_cron_schedule(
667 name.as_ref().to_vec(),
668 schedule.as_ref().to_vec(),
669 payload.to_vec(),
670 )?
671 };
672 Ok(())
673 }
674
675 pub fn cancel(name: impl AsRef<[u8]>) -> Result<(), SysError> {
677 unsafe { astrid_cron_cancel(name.as_ref().to_vec())? };
678 Ok(())
679 }
680}
681
682pub mod types;
683
684pub mod env {
689 use super::*;
690
691 pub const CONFIG_SOCKET_PATH: &str = "ASTRID_SOCKET_PATH";
693
694 pub fn var_bytes(key: impl AsRef<[u8]>) -> Result<Vec<u8>, SysError> {
696 let result = unsafe { astrid_get_config(key.as_ref().to_vec())? };
697 Ok(result)
698 }
699
700 pub fn var(key: impl AsRef<[u8]>) -> Result<String, SysError> {
702 let bytes = var_bytes(key)?;
703 String::from_utf8(bytes).map_err(|e| SysError::ApiError(e.to_string()))
704 }
705}
706
707pub mod time {
709 use super::*;
710
711 pub fn now_ms() -> Result<u64, SysError> {
716 let bytes = unsafe { astrid_clock_ms()? };
717 let s = String::from_utf8_lossy(&bytes);
718 s.trim()
719 .parse::<u64>()
720 .map_err(|e| SysError::ApiError(format!("clock_ms parse error: {e}")))
721 }
722}
723
724pub mod log {
726 use super::*;
727
728 pub fn log(level: impl AsRef<[u8]>, message: impl AsRef<[u8]>) -> Result<(), SysError> {
730 unsafe { astrid_log(level.as_ref().to_vec(), message.as_ref().to_vec())? };
731 Ok(())
732 }
733
734 pub fn debug(message: impl AsRef<[u8]>) -> Result<(), SysError> {
736 log("debug", message)
737 }
738
739 pub fn info(message: impl AsRef<[u8]>) -> Result<(), SysError> {
741 log("info", message)
742 }
743
744 pub fn warn(message: impl AsRef<[u8]>) -> Result<(), SysError> {
746 log("warn", message)
747 }
748
749 pub fn error(message: impl AsRef<[u8]>) -> Result<(), SysError> {
751 log("error", message)
752 }
753}
754
755pub mod runtime {
757 use super::*;
758
759 pub fn signal_ready() -> Result<(), SysError> {
765 unsafe { astrid_signal_ready()? };
766 Ok(())
767 }
768
769 pub fn caller() -> Result<crate::types::CallerContext, SysError> {
771 let bytes = unsafe { astrid_get_caller()? };
772 serde_json::from_slice(&bytes)
773 .map_err(|e| SysError::ApiError(format!("failed to parse caller context: {e}")))
774 }
775
776 pub fn socket_path() -> Result<String, SysError> {
781 let raw = crate::env::var(crate::env::CONFIG_SOCKET_PATH)?;
782 let path = serde_json::from_str::<String>(raw.trim()).or_else(|_| {
785 if raw.is_empty() {
787 Err(SysError::ApiError(
788 "ASTRID_SOCKET_PATH config key is empty".to_string(),
789 ))
790 } else {
791 Ok(raw)
792 }
793 })?;
794 if path.contains('\0') {
796 return Err(SysError::ApiError(
797 "ASTRID_SOCKET_PATH contains null byte".to_string(),
798 ));
799 }
800 Ok(path)
801 }
802}
803
804pub mod hooks {
806 use super::*;
807
808 pub fn trigger(event_bytes: &[u8]) -> Result<Vec<u8>, SysError> {
809 unsafe { Ok(astrid_trigger_hook(event_bytes.to_vec())?) }
810 }
811}
812
813pub mod capabilities {
819 use super::*;
820
821 pub fn check(source_uuid: &str, capability: &str) -> Result<bool, SysError> {
827 let request = serde_json::json!({
828 "source_uuid": source_uuid,
829 "capability": capability,
830 });
831 let request_bytes = serde_json::to_vec(&request)?;
832 let response_bytes = unsafe { astrid_check_capsule_capability(request_bytes)? };
833 let response: serde_json::Value = serde_json::from_slice(&response_bytes)?;
834 Ok(response["allowed"].as_bool().unwrap_or(false))
835 }
836}
837
838pub mod net;
839pub mod process {
840 use super::*;
841 use serde::{Deserialize, Serialize};
842
843 #[derive(Debug, Serialize)]
845 pub struct ProcessRequest<'a> {
846 pub cmd: &'a str,
847 pub args: &'a [&'a str],
848 }
849
850 #[derive(Debug, Deserialize)]
852 pub struct ProcessResult {
853 pub stdout: String,
854 pub stderr: String,
855 pub exit_code: i32,
856 }
857
858 pub fn spawn(cmd: &str, args: &[&str]) -> Result<ProcessResult, SysError> {
861 let req = ProcessRequest { cmd, args };
862 let req_bytes = serde_json::to_vec(&req)?;
863 let result_bytes = unsafe { astrid_spawn_host(req_bytes)? };
864 let result: ProcessResult = serde_json::from_slice(&result_bytes)?;
865 Ok(result)
866 }
867
868 #[derive(Debug, Deserialize)]
874 pub struct BackgroundProcessHandle {
875 pub id: u64,
877 }
878
879 #[derive(Debug, Deserialize)]
881 pub struct ProcessLogs {
882 pub stdout: String,
884 pub stderr: String,
886 pub running: bool,
888 pub exit_code: Option<i32>,
890 }
891
892 #[derive(Debug, Deserialize)]
894 pub struct KillResult {
895 pub killed: bool,
897 pub exit_code: Option<i32>,
899 pub stdout: String,
901 pub stderr: String,
903 }
904
905 pub fn spawn_background(cmd: &str, args: &[&str]) -> Result<BackgroundProcessHandle, SysError> {
910 let req = ProcessRequest { cmd, args };
911 let req_bytes = serde_json::to_vec(&req)?;
912 let result_bytes = unsafe { astrid_spawn_background_host(req_bytes)? };
913 let result: BackgroundProcessHandle = serde_json::from_slice(&result_bytes)?;
914 Ok(result)
915 }
916
917 pub fn read_logs(id: u64) -> Result<ProcessLogs, SysError> {
922 #[derive(Serialize)]
923 struct Req {
924 id: u64,
925 }
926 let req_bytes = serde_json::to_vec(&Req { id })?;
927 let result_bytes = unsafe { astrid_read_process_logs_host(req_bytes)? };
928 let result: ProcessLogs = serde_json::from_slice(&result_bytes)?;
929 Ok(result)
930 }
931
932 pub fn kill(id: u64) -> Result<KillResult, SysError> {
936 #[derive(Serialize)]
937 struct Req {
938 id: u64,
939 }
940 let req_bytes = serde_json::to_vec(&Req { id })?;
941 let result_bytes = unsafe { astrid_kill_process_host(req_bytes)? };
942 let result: KillResult = serde_json::from_slice(&result_bytes)?;
943 Ok(result)
944 }
945}
946
947pub mod elicit {
953 use super::*;
954
955 #[derive(Serialize)]
957 struct ElicitRequest<'a> {
958 #[serde(rename = "type")]
959 kind: &'a str,
960 key: &'a str,
961 #[serde(skip_serializing_if = "Option::is_none")]
962 description: Option<&'a str>,
963 #[serde(skip_serializing_if = "Option::is_none")]
964 options: Option<&'a [&'a str]>,
965 #[serde(skip_serializing_if = "Option::is_none")]
966 default: Option<&'a str>,
967 }
968
969 fn validate_key(key: &str) -> Result<(), SysError> {
971 if key.trim().is_empty() {
972 return Err(SysError::ApiError("elicit key must not be empty".into()));
973 }
974 Ok(())
975 }
976
977 pub fn secret(key: &str, description: &str) -> Result<(), SysError> {
980 validate_key(key)?;
981 let req = ElicitRequest {
982 kind: "secret",
983 key,
984 description: Some(description),
985 options: None,
986 default: None,
987 };
988 let req_bytes = serde_json::to_vec(&req)?;
989 let resp_bytes = unsafe { astrid_elicit(req_bytes)? };
992
993 #[derive(serde::Deserialize)]
994 struct SecretResp {
995 ok: bool,
996 }
997 let resp: SecretResp = serde_json::from_slice(&resp_bytes)?;
998 if !resp.ok {
999 return Err(SysError::ApiError(
1000 "kernel did not confirm secret storage".into(),
1001 ));
1002 }
1003 Ok(())
1004 }
1005
1006 pub fn has_secret(key: &str) -> Result<bool, SysError> {
1008 validate_key(key)?;
1009 #[derive(Serialize)]
1010 struct HasSecretRequest<'a> {
1011 key: &'a str,
1012 }
1013 let req_bytes = serde_json::to_vec(&HasSecretRequest { key })?;
1014 let resp_bytes = unsafe { astrid_has_secret(req_bytes)? };
1017
1018 #[derive(serde::Deserialize)]
1019 struct ExistsResp {
1020 exists: bool,
1021 }
1022 let resp: ExistsResp = serde_json::from_slice(&resp_bytes)?;
1023 Ok(resp.exists)
1024 }
1025
1026 fn elicit_text(
1028 key: &str,
1029 description: &str,
1030 default: Option<&str>,
1031 ) -> Result<String, SysError> {
1032 validate_key(key)?;
1033 let req = ElicitRequest {
1034 kind: "text",
1035 key,
1036 description: Some(description),
1037 options: None,
1038 default,
1039 };
1040 let req_bytes = serde_json::to_vec(&req)?;
1041 let resp_bytes = unsafe { astrid_elicit(req_bytes)? };
1044
1045 #[derive(serde::Deserialize)]
1046 struct TextResp {
1047 value: String,
1048 }
1049 let resp: TextResp = serde_json::from_slice(&resp_bytes)?;
1050 Ok(resp.value)
1051 }
1052
1053 pub fn text(key: &str, description: &str) -> Result<String, SysError> {
1056 elicit_text(key, description, None)
1057 }
1058
1059 pub fn text_with_default(
1061 key: &str,
1062 description: &str,
1063 default: &str,
1064 ) -> Result<String, SysError> {
1065 elicit_text(key, description, Some(default))
1066 }
1067
1068 pub fn select(key: &str, description: &str, options: &[&str]) -> Result<String, SysError> {
1070 validate_key(key)?;
1071 if options.is_empty() {
1072 return Err(SysError::ApiError(
1073 "select requires at least one option".into(),
1074 ));
1075 }
1076 let req = ElicitRequest {
1077 kind: "select",
1078 key,
1079 description: Some(description),
1080 options: Some(options),
1081 default: None,
1082 };
1083 let req_bytes = serde_json::to_vec(&req)?;
1084 let resp_bytes = unsafe { astrid_elicit(req_bytes)? };
1087
1088 #[derive(serde::Deserialize)]
1089 struct SelectResp {
1090 value: String,
1091 }
1092 let resp: SelectResp = serde_json::from_slice(&resp_bytes)?;
1093 if !options.iter().any(|o| *o == resp.value) {
1094 let truncated: String = resp.value.chars().take(64).collect();
1095 return Err(SysError::ApiError(format!(
1096 "host returned value '{truncated}' not in provided options",
1097 )));
1098 }
1099 Ok(resp.value)
1100 }
1101
1102 pub fn array(key: &str, description: &str) -> Result<Vec<String>, SysError> {
1104 validate_key(key)?;
1105 let req = ElicitRequest {
1106 kind: "array",
1107 key,
1108 description: Some(description),
1109 options: None,
1110 default: None,
1111 };
1112 let req_bytes = serde_json::to_vec(&req)?;
1113 let resp_bytes = unsafe { astrid_elicit(req_bytes)? };
1116
1117 #[derive(serde::Deserialize)]
1118 struct ArrayResp {
1119 values: Vec<String>,
1120 }
1121 let resp: ArrayResp = serde_json::from_slice(&resp_bytes)?;
1122 Ok(resp.values)
1123 }
1124}
1125
1126pub mod interceptors {
1133 use super::*;
1134
1135 #[derive(Debug, serde::Deserialize)]
1137 pub struct InterceptorBinding {
1138 pub handle_id: u64,
1140 pub action: String,
1142 pub topic: String,
1144 }
1145
1146 impl InterceptorBinding {
1147 #[must_use]
1149 pub fn subscription_handle(&self) -> ipc::SubscriptionHandle {
1150 ipc::SubscriptionHandle(self.handle_id.to_string().into_bytes())
1151 }
1152
1153 #[must_use]
1155 pub fn handle_bytes(&self) -> Vec<u8> {
1156 self.handle_id.to_string().into_bytes()
1157 }
1158 }
1159
1160 pub fn bindings() -> Result<Vec<InterceptorBinding>, SysError> {
1165 let bytes = unsafe { astrid_get_interceptor_handles()? };
1169 let bindings: Vec<InterceptorBinding> = serde_json::from_slice(&bytes)?;
1170 Ok(bindings)
1171 }
1172
1173 pub fn poll(
1180 bindings: &[InterceptorBinding],
1181 mut handler: impl FnMut(&str, &[u8]),
1182 ) -> Result<(), SysError> {
1183 #[derive(serde::Deserialize)]
1184 struct PollEnvelope {
1185 messages: Vec<serde_json::Value>,
1186 }
1187
1188 for binding in bindings {
1189 let handle = binding.subscription_handle();
1190 let envelope = ipc::poll_bytes(&handle)?;
1191
1192 let parsed: PollEnvelope = serde_json::from_slice(&envelope)?;
1196 if !parsed.messages.is_empty() {
1197 handler(&binding.action, &envelope);
1198 }
1199 }
1200 Ok(())
1201 }
1202}
1203
1204pub mod identity {
1232 use super::*;
1233
1234 #[derive(Debug)]
1236 pub struct ResolvedUser {
1237 pub user_id: String,
1239 pub display_name: Option<String>,
1241 }
1242
1243 #[derive(Debug)]
1245 pub struct Link {
1246 pub platform: String,
1248 pub platform_user_id: String,
1250 pub astrid_user_id: String,
1252 pub linked_at: String,
1254 pub method: String,
1256 }
1257
1258 pub fn resolve(
1263 platform: &str,
1264 platform_user_id: &str,
1265 ) -> Result<Option<ResolvedUser>, SysError> {
1266 #[derive(Serialize)]
1267 struct Req<'a> {
1268 platform: &'a str,
1269 platform_user_id: &'a str,
1270 }
1271
1272 let req_bytes = serde_json::to_vec(&Req {
1273 platform,
1274 platform_user_id,
1275 })?;
1276
1277 let resp_bytes = unsafe { astrid_identity_resolve(req_bytes)? };
1279
1280 #[derive(Deserialize)]
1281 struct Resp {
1282 found: bool,
1283 user_id: Option<String>,
1284 display_name: Option<String>,
1285 error: Option<String>,
1286 }
1287 let resp: Resp = serde_json::from_slice(&resp_bytes)?;
1288 if resp.found {
1289 let user_id = resp.user_id.ok_or_else(|| {
1290 SysError::ApiError("host returned found=true but user_id was missing".into())
1291 })?;
1292 Ok(Some(ResolvedUser {
1293 user_id,
1294 display_name: resp.display_name,
1295 }))
1296 } else if let Some(err) = resp.error {
1297 Err(SysError::ApiError(err))
1298 } else {
1299 Ok(None)
1300 }
1301 }
1302
1303 pub fn link(
1309 platform: &str,
1310 platform_user_id: &str,
1311 astrid_user_id: &str,
1312 method: &str,
1313 ) -> Result<Link, SysError> {
1314 #[derive(Serialize)]
1315 struct Req<'a> {
1316 platform: &'a str,
1317 platform_user_id: &'a str,
1318 astrid_user_id: &'a str,
1319 method: &'a str,
1320 }
1321
1322 let req_bytes = serde_json::to_vec(&Req {
1323 platform,
1324 platform_user_id,
1325 astrid_user_id,
1326 method,
1327 })?;
1328
1329 let resp_bytes = unsafe { astrid_identity_link(req_bytes)? };
1331
1332 #[derive(Deserialize)]
1333 struct LinkInfo {
1334 platform: String,
1335 platform_user_id: String,
1336 astrid_user_id: String,
1337 linked_at: String,
1338 method: String,
1339 }
1340 #[derive(Deserialize)]
1341 struct Resp {
1342 ok: bool,
1343 error: Option<String>,
1344 link: Option<LinkInfo>,
1345 }
1346 let resp: Resp = serde_json::from_slice(&resp_bytes)?;
1347 if !resp.ok {
1348 return Err(SysError::ApiError(
1349 resp.error.unwrap_or_else(|| "identity link failed".into()),
1350 ));
1351 }
1352 let l = resp
1353 .link
1354 .ok_or_else(|| SysError::ApiError("missing link in response".into()))?;
1355 Ok(Link {
1356 platform: l.platform,
1357 platform_user_id: l.platform_user_id,
1358 astrid_user_id: l.astrid_user_id,
1359 linked_at: l.linked_at,
1360 method: l.method,
1361 })
1362 }
1363
1364 pub fn unlink(platform: &str, platform_user_id: &str) -> Result<bool, SysError> {
1369 #[derive(Serialize)]
1370 struct Req<'a> {
1371 platform: &'a str,
1372 platform_user_id: &'a str,
1373 }
1374
1375 let req_bytes = serde_json::to_vec(&Req {
1376 platform,
1377 platform_user_id,
1378 })?;
1379
1380 let resp_bytes = unsafe { astrid_identity_unlink(req_bytes)? };
1382
1383 #[derive(Deserialize)]
1384 struct Resp {
1385 ok: bool,
1386 error: Option<String>,
1387 removed: Option<bool>,
1388 }
1389 let resp: Resp = serde_json::from_slice(&resp_bytes)?;
1390 if !resp.ok {
1391 return Err(SysError::ApiError(
1392 resp.error
1393 .unwrap_or_else(|| "identity unlink failed".into()),
1394 ));
1395 }
1396 Ok(resp.removed.unwrap_or(false))
1397 }
1398
1399 pub fn create_user(display_name: Option<&str>) -> Result<String, SysError> {
1404 #[derive(Serialize)]
1405 struct Req<'a> {
1406 display_name: Option<&'a str>,
1407 }
1408
1409 let req_bytes = serde_json::to_vec(&Req { display_name })?;
1410
1411 let resp_bytes = unsafe { astrid_identity_create_user(req_bytes)? };
1413
1414 #[derive(Deserialize)]
1415 struct Resp {
1416 ok: bool,
1417 error: Option<String>,
1418 user_id: Option<String>,
1419 }
1420 let resp: Resp = serde_json::from_slice(&resp_bytes)?;
1421 if !resp.ok {
1422 return Err(SysError::ApiError(
1423 resp.error
1424 .unwrap_or_else(|| "identity create_user failed".into()),
1425 ));
1426 }
1427 resp.user_id
1428 .ok_or_else(|| SysError::ApiError("missing user_id in response".into()))
1429 }
1430
1431 pub fn list_links(astrid_user_id: &str) -> Result<Vec<Link>, SysError> {
1436 #[derive(Serialize)]
1437 struct Req<'a> {
1438 astrid_user_id: &'a str,
1439 }
1440
1441 let req_bytes = serde_json::to_vec(&Req { astrid_user_id })?;
1442
1443 let resp_bytes = unsafe { astrid_identity_list_links(req_bytes)? };
1445
1446 #[derive(Deserialize)]
1447 struct LinkInfo {
1448 platform: String,
1449 platform_user_id: String,
1450 astrid_user_id: String,
1451 linked_at: String,
1452 method: String,
1453 }
1454 #[derive(Deserialize)]
1455 struct Resp {
1456 ok: bool,
1457 error: Option<String>,
1458 links: Option<Vec<LinkInfo>>,
1459 }
1460 let resp: Resp = serde_json::from_slice(&resp_bytes)?;
1461 if !resp.ok {
1462 return Err(SysError::ApiError(
1463 resp.error
1464 .unwrap_or_else(|| "identity list_links failed".into()),
1465 ));
1466 }
1467 Ok(resp
1468 .links
1469 .unwrap_or_default()
1470 .into_iter()
1471 .map(|l| Link {
1472 platform: l.platform,
1473 platform_user_id: l.platform_user_id,
1474 astrid_user_id: l.astrid_user_id,
1475 linked_at: l.linked_at,
1476 method: l.method,
1477 })
1478 .collect())
1479 }
1480}
1481
1482pub mod approval {
1483 use super::*;
1484
1485 #[derive(Debug)]
1487 pub struct ApprovalResult {
1488 pub approved: bool,
1490 pub decision: String,
1493 }
1494
1495 pub fn request(
1505 action: &str,
1506 resource: &str,
1507 risk_level: &str,
1508 ) -> Result<ApprovalResult, SysError> {
1509 #[derive(Serialize)]
1510 struct ApprovalRequest<'a> {
1511 action: &'a str,
1512 resource: &'a str,
1513 risk_level: &'a str,
1514 }
1515
1516 let req = ApprovalRequest {
1517 action,
1518 resource,
1519 risk_level,
1520 };
1521 let req_bytes = serde_json::to_vec(&req)?;
1522
1523 let resp_bytes = unsafe { astrid_request_approval(req_bytes)? };
1527
1528 #[derive(Deserialize)]
1529 struct ApprovalResp {
1530 approved: bool,
1531 decision: String,
1532 }
1533 let resp: ApprovalResp = serde_json::from_slice(&resp_bytes)?;
1534 Ok(ApprovalResult {
1535 approved: resp.approved,
1536 decision: resp.decision,
1537 })
1538 }
1539}
1540
1541pub mod prelude {
1542 pub use crate::{
1543 SysError,
1544 approval,
1546 capabilities,
1547 cron,
1548 elicit,
1549 env,
1551 fs,
1552 hooks,
1553 http,
1554 identity,
1555 interceptors,
1556 ipc,
1557 kv,
1558 log,
1559 net,
1560 process,
1561 runtime,
1562 time,
1563 uplink,
1564 };
1565
1566 #[cfg(feature = "derive")]
1567 pub use astrid_sdk_macros::capsule;
1568}