1pub mod config;
69pub mod error;
70
71pub mod runtime_error;
73
74pub mod verify;
78
79#[cfg(not(target_arch = "wasm32"))]
84pub mod actr_ref;
85#[cfg(not(target_arch = "wasm32"))]
86pub mod ais_client;
87#[cfg(not(target_arch = "wasm32"))]
88pub(crate) mod key_cache;
89#[cfg(not(target_arch = "wasm32"))]
90pub mod storage;
91
92#[cfg(all(not(target_arch = "wasm32"), feature = "test-utils"))]
94pub mod inbound;
95#[cfg(all(not(target_arch = "wasm32"), not(feature = "test-utils")))]
96pub(crate) mod inbound;
97#[cfg(not(target_arch = "wasm32"))]
98pub mod lifecycle;
99#[cfg(all(not(target_arch = "wasm32"), feature = "test-utils"))]
100pub mod outbound;
101#[cfg(all(not(target_arch = "wasm32"), not(feature = "test-utils")))]
102pub(crate) mod outbound;
103#[cfg(not(target_arch = "wasm32"))]
104pub mod transport;
105#[cfg(not(target_arch = "wasm32"))]
106pub mod wire;
107
108#[cfg(all(not(target_arch = "wasm32"), feature = "test-utils"))]
110pub mod test_support;
111
112#[cfg(not(target_arch = "wasm32"))]
114pub mod context;
115
116#[cfg(not(target_arch = "wasm32"))]
118pub mod workload;
119
120#[cfg(not(target_arch = "wasm32"))]
123mod service_spec;
124
125#[cfg(all(not(target_arch = "wasm32"), feature = "wasm-engine"))]
127pub mod wasm;
128
129#[cfg(all(not(target_arch = "wasm32"), feature = "dynclib-engine"))]
131pub mod dynclib;
132
133#[cfg(not(target_arch = "wasm32"))]
136pub(crate) mod monitoring;
137#[cfg(not(target_arch = "wasm32"))]
138pub mod observability;
139#[cfg(not(target_arch = "wasm32"))]
140pub(crate) mod resource;
141
142pub use actr_pack::{PackageManifest, VerifiedPackage};
147pub use config::HyperConfig;
148pub use error::HyperError;
149pub(crate) use error::HyperResult;
150
151pub use actr_protocol::{Acl, ActrId, ActrType, ServiceSpec};
153
154pub use actr_framework::{MediaSample, MediaType};
156
157pub use runtime_error::{ActorResult, ActrError, Classify, ErrorKind};
159
160pub use actr_platform_traits::{CryptoProvider, KvStore, PlatformError, PlatformProvider};
162
163#[cfg(not(target_arch = "wasm32"))]
168pub use ais_client::AisClient;
169#[cfg(not(target_arch = "wasm32"))]
170pub use storage::ActorStore;
171#[cfg(not(target_arch = "wasm32"))]
172pub use verify::{ChainTrust, MfrCertCache, RegistryTrust, StaticTrust, TrustProvider};
173
174#[cfg(not(target_arch = "wasm32"))]
176pub use observability::{ObservabilityGuard, init_observability};
177
178#[cfg(not(target_arch = "wasm32"))]
179pub use actr_ref::ActrRef;
180#[cfg(not(target_arch = "wasm32"))]
182pub use lifecycle::{CredentialState, NetworkEventHandle};
183
184#[cfg(all(not(target_arch = "wasm32"), feature = "test-utils"))]
186pub use transport::{
187 ConnType, DataLane, DefaultWireBuilder, DefaultWireBuilderConfig, HostTransport, PeerTransport,
188 WireBuilder, WireHandle,
189};
190#[cfg(not(target_arch = "wasm32"))]
191pub use transport::{Dest, ExponentialBackoff, NetworkError, NetworkResult};
192
193#[cfg(not(target_arch = "wasm32"))]
195pub use wire::{
196 AuthConfig, AuthType, DisconnectReason, ReconnectConfig, SignalingClient, SignalingConfig,
197 SignalingEvent, SignalingStats, WebRtcConfig,
198};
199#[cfg(all(not(target_arch = "wasm32"), feature = "test-utils"))]
200pub use wire::{WebRtcCoordinator, WebSocketSignalingClient};
201
202#[cfg(not(target_arch = "wasm32"))]
204pub use actr_runtime_mailbox::{
205 Mailbox, MailboxStats, MessagePriority, MessageRecord, MessageStatus,
206};
207
208#[cfg(not(target_arch = "wasm32"))]
213pub use workload::{HostAbiFn, HostOperation, HostOperationResult, InvocationContext};
214
215pub(crate) const INITIAL_CONNECTION_TIMEOUT: std::time::Duration =
220 std::time::Duration::from_secs(10);
221
222pub mod prelude {
227 pub use crate::verify::{ChainTrust, RegistryTrust, StaticTrust, TrustProvider};
237 #[cfg(not(target_arch = "wasm32"))]
238 pub use crate::{Attached, Hyper, Init, Node, Registered, storage::ActorStore};
239 pub use crate::{HyperConfig, HyperError};
240 pub use actr_pack::{PackageManifest, VerifiedPackage};
241
242 #[cfg(not(target_arch = "wasm32"))]
244 pub use crate::actr_ref::ActrRef;
245
246 pub use actr_framework::{MediaSample, MediaType};
248
249 #[cfg(not(target_arch = "wasm32"))]
251 pub use crate::wire::webrtc::{
252 AuthConfig, AuthType, DisconnectReason, ReconnectConfig, SignalingClient, SignalingConfig,
253 SignalingEvent, SignalingStats, WebRtcConfig,
254 };
255 #[cfg(feature = "test-utils")]
256 pub use crate::wire::webrtc::{WebRtcCoordinator, WebSocketSignalingClient};
257
258 #[cfg(not(target_arch = "wasm32"))]
260 pub use actr_runtime_mailbox::{
261 Mailbox, MailboxStats, MessagePriority, MessageRecord, MessageStatus,
262 };
263
264 #[cfg(feature = "test-utils")]
266 pub use crate::transport::{
267 ConnType, DataLane, DefaultWireBuilder, DefaultWireBuilderConfig, HostTransport,
268 PeerTransport, WireBuilder, WireHandle,
269 };
270 #[cfg(not(target_arch = "wasm32"))]
271 pub use crate::transport::{Dest, NetworkError, NetworkResult};
272
273 pub use crate::runtime_error::{ActorResult, ActrError};
275
276 pub use actr_protocol::ActrId;
278
279 pub use actr_framework::{Context, Workload};
281
282 pub use async_trait::async_trait;
284
285 pub use anyhow::{Context as AnyhowContext, Result as AnyhowResult};
287 pub use chrono::{DateTime, Utc};
288 pub use uuid::Uuid;
289
290 pub use tokio::sync::{Mutex, RwLock, broadcast, mpsc, oneshot};
292 #[cfg(not(target_arch = "wasm32"))]
293 pub use tokio::time::{Duration, Instant, sleep, timeout};
294
295 pub use tracing::{debug, error, info, trace, warn};
297}
298
299#[cfg(all(not(target_arch = "wasm32"), feature = "dynclib-engine"))]
304use std::io::Write;
305#[cfg(all(not(target_arch = "wasm32"), feature = "dynclib-engine"))]
306use std::path::Path;
307#[cfg(not(target_arch = "wasm32"))]
308use std::path::PathBuf;
309#[cfg(not(target_arch = "wasm32"))]
310use std::str::FromStr;
311#[cfg(not(target_arch = "wasm32"))]
312use std::sync::Arc;
313#[cfg(not(target_arch = "wasm32"))]
314use std::time::{SystemTime, UNIX_EPOCH};
315
316#[cfg(not(target_arch = "wasm32"))]
317use prost::Message;
318#[cfg(not(target_arch = "wasm32"))]
319use tracing::{debug, error, info, warn};
320#[cfg(not(target_arch = "wasm32"))]
321use uuid::Uuid;
322
323#[cfg(not(target_arch = "wasm32"))]
324use actr_platform_traits::KvOp;
325#[cfg(not(target_arch = "wasm32"))]
326use actr_protocol::{Realm, RegisterAuthMode, RegisterRequest, register_response};
327
328#[cfg(not(target_arch = "wasm32"))]
329pub struct Init;
334#[cfg(not(target_arch = "wasm32"))]
335pub struct Attached;
337#[cfg(not(target_arch = "wasm32"))]
338pub struct Registered;
340
341#[cfg(not(target_arch = "wasm32"))]
342mod node_state_sealed {
343 pub trait Sealed {}
344 impl Sealed for super::Init {}
345 impl Sealed for super::Attached {}
346 impl Sealed for super::Registered {}
347}
348
349#[cfg(not(target_arch = "wasm32"))]
350pub trait NodeState: node_state_sealed::Sealed {}
352#[cfg(not(target_arch = "wasm32"))]
353impl NodeState for Init {}
354#[cfg(not(target_arch = "wasm32"))]
355impl NodeState for Attached {}
356#[cfg(not(target_arch = "wasm32"))]
357impl NodeState for Registered {}
358
359#[cfg(not(target_arch = "wasm32"))]
360pub struct Hyper {
383 inner: Arc<HyperInner>,
384}
385
386#[cfg(not(target_arch = "wasm32"))]
387struct HyperInner {
388 config: HyperConfig,
389 instance_id: String,
391 platform: Option<Arc<dyn PlatformProvider>>,
393}
394
395#[cfg(not(target_arch = "wasm32"))]
396struct Attachment {
398 node: crate::lifecycle::node::Inner,
399 verified: Option<VerifiedPackage>,
407 package_bytes: bytes::Bytes,
408}
409
410#[cfg(not(target_arch = "wasm32"))]
411pub struct Node<S: NodeState = Attached> {
431 hyper: Arc<HyperInner>,
432 attachment: Option<Attachment>,
435 pending_runtime_config: Option<actr_config::RuntimeConfig>,
438 _state: std::marker::PhantomData<S>,
439}
440
441#[cfg(not(target_arch = "wasm32"))]
442#[derive(Debug, Clone, Copy, PartialEq, Eq)]
444pub enum BinaryKind {
445 Wasm,
447 DynClib,
449}
450
451#[cfg(not(target_arch = "wasm32"))]
452#[derive(Debug, Clone)]
454pub struct WorkloadPackage {
455 bytes: bytes::Bytes,
456}
457
458#[cfg(not(target_arch = "wasm32"))]
459impl WorkloadPackage {
460 pub fn new(bytes: impl Into<bytes::Bytes>) -> Self {
462 Self {
463 bytes: bytes.into(),
464 }
465 }
466
467 pub fn from_path(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
469 let bytes = std::fs::read(path)?;
470 Ok(Self {
471 bytes: bytes.into(),
472 })
473 }
474
475 pub fn bytes(&self) -> &[u8] {
477 &self.bytes
478 }
479
480 pub fn manifest(&self) -> HyperResult<actr_pack::PackageManifest> {
486 actr_pack::read_manifest(&self.bytes)
487 .map_err(|e| HyperError::InvalidManifest(e.to_string()))
488 }
489}
490
491#[cfg(not(target_arch = "wasm32"))]
492pub(crate) struct LoadedWorkload {
494 pub verified: VerifiedPackage,
498 pub binary_kind: BinaryKind,
500 pub workload: crate::workload::Workload,
502}
503
504#[cfg(not(target_arch = "wasm32"))]
505impl std::fmt::Debug for LoadedWorkload {
506 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
507 f.debug_struct("LoadedWorkload")
508 .field("manifest", &self.verified.manifest)
509 .field("backend", &self.binary_kind)
510 .finish_non_exhaustive()
511 }
512}
513
514#[cfg(not(target_arch = "wasm32"))]
515impl Hyper {
516 pub async fn new(config: HyperConfig) -> HyperResult<Self> {
522 Self::init_inner(config, None).await
523 }
524
525 pub async fn with_platform(
533 config: HyperConfig,
534 platform: Arc<dyn PlatformProvider>,
535 ) -> HyperResult<Self> {
536 Self::init_inner(config, Some(platform)).await
537 }
538
539 async fn init_inner(
540 config: HyperConfig,
541 platform: Option<Arc<dyn PlatformProvider>>,
542 ) -> HyperResult<Self> {
543 info!(
544 data_dir = %config.data_dir.display(),
545 "Hyper initializing"
546 );
547
548 let instance_id = if let Some(ref p) = platform {
553 p.instance_uid()
554 .await
555 .map_err(|e| HyperError::Storage(format!("failed to load instance_uid: {e}")))?
556 } else {
557 tokio::fs::create_dir_all(&config.data_dir)
558 .await
559 .map_err(|e| {
560 HyperError::Config(format!(
561 "failed to create data_dir `{}`: {e}",
562 config.data_dir.display()
563 ))
564 })?;
565 load_or_create_instance_uid_local(&config.data_dir).await?
566 };
567 debug!(instance_id, "Hyper instance_uid ready");
568
569 Ok(Self {
570 inner: Arc::new(HyperInner {
571 config,
572 instance_id,
573 platform,
574 }),
575 })
576 }
577
578 pub async fn verify_package(&self, package: &WorkloadPackage) -> HyperResult<VerifiedPackage> {
585 self.inner
586 .config
587 .trust_provider
588 .verify_package(package.bytes())
589 .await
590 }
591
592 #[cfg(feature = "test-utils")]
597 pub(crate) async fn load_workload_package(
598 &self,
599 package: &WorkloadPackage,
600 ) -> HyperResult<LoadedWorkload> {
601 load_workload_package_inner(&self.inner, package).await
602 }
603}
604
605#[cfg(not(target_arch = "wasm32"))]
608impl Node {
609 pub async fn from_config_file(path: impl AsRef<std::path::Path>) -> HyperResult<Node<Init>> {
625 config::node_from_config_file(path.as_ref()).await
626 }
627
628 pub async fn from_config_with_package(
637 path: impl AsRef<std::path::Path>,
638 package_info: actr_config::PackageInfo,
639 ) -> HyperResult<Node<Init>> {
640 config::node_from_config_file_with_package(path.as_ref(), Some(package_info)).await
641 }
642
643 pub fn from_hyper(hyper: Hyper, runtime_config: actr_config::RuntimeConfig) -> Node<Init> {
651 Node {
652 hyper: hyper.inner,
653 attachment: None,
654 pending_runtime_config: Some(runtime_config),
655 _state: std::marker::PhantomData,
656 }
657 }
658
659 pub async fn run_from_config(
668 path: impl AsRef<std::path::Path>,
669 package: &WorkloadPackage,
670 ) -> HyperResult<ActrRef> {
671 let init = Self::from_config_file(path).await?;
672 let ais_endpoint = init
673 .pending_runtime_config
674 .as_ref()
675 .map(|c| c.ais_endpoint.clone())
676 .expect("Node<Init> without pending runtime config");
677 let attached = init.attach(package).await?;
678 let registered = attached.register(&ais_endpoint).await?;
679 registered
680 .start()
681 .await
682 .map_err(|e| HyperError::Runtime(format!("failed to start node: {e}")))
683 }
684}
685
686#[cfg(not(target_arch = "wasm32"))]
689impl Node<Init> {
690 pub fn runtime_config(&self) -> &actr_config::RuntimeConfig {
694 self.pending_runtime_config
695 .as_ref()
696 .expect("Node<Init> without pending runtime config")
697 }
698
699 pub fn with_actor_type(mut self, actor_type: actr_protocol::ActrType) -> Self {
705 let runtime_config = self
706 .pending_runtime_config
707 .as_mut()
708 .expect("Node<Init> without pending runtime config");
709 runtime_config.package.name = actor_type.name.clone();
710 runtime_config.package.actr_type = actor_type;
711 self
712 }
713}
714
715#[cfg(not(target_arch = "wasm32"))]
716impl Node<Init> {
717 pub async fn attach(self, package: &WorkloadPackage) -> HyperResult<Node<Attached>> {
725 let runtime_config = self
726 .pending_runtime_config
727 .expect("Node<Init> without pending runtime config");
728 let hyper_inner = self.hyper;
729 let loaded = load_workload_package_inner(&hyper_inner, package).await?;
730 let packaged_lock = actr_pack::read_lock_file(package.bytes())
731 .map_err(|e| HyperError::Runtime(e.to_string()))?
732 .map(|bytes| {
733 let raw = std::str::from_utf8(&bytes).map_err(|e| {
734 HyperError::Runtime(format!("manifest.lock.toml is not valid UTF-8: {e}"))
735 })?;
736 actr_config::lock::LockFile::from_str(raw).map_err(|e| {
737 HyperError::Runtime(format!("failed to parse manifest.lock.toml: {e}"))
738 })
739 })
740 .transpose()?;
741 let mailbox_backpressure_threshold =
742 hyper_inner.config.resolved_mailbox_backpressure_threshold();
743 let credential_expiry_warning = hyper_inner.config.credential_expiry_warning;
744 let mut node_inner = crate::lifecycle::node::Inner::build(
745 runtime_config,
746 loaded.workload,
747 Some(loaded.verified.manifest.clone()),
748 packaged_lock,
749 mailbox_backpressure_threshold,
750 credential_expiry_warning,
751 )
752 .await
753 .map_err(|e| HyperError::Runtime(e.to_string()))?;
754 let observer: Arc<dyn crate::lifecycle::hooks::WorkloadHookObserver> =
755 Arc::new(crate::workload::PackageHookObserver {
756 workload_dispatch: node_inner.workload_dispatch.clone(),
757 });
758 node_inner.hook_observer = Some(observer);
759 Ok(Node {
760 hyper: hyper_inner,
761 attachment: Some(Attachment {
762 node: node_inner,
763 verified: Some(loaded.verified),
764 package_bytes: package.bytes.clone(),
765 }),
766 pending_runtime_config: None,
767 _state: std::marker::PhantomData,
768 })
769 }
770
771 pub(crate) async fn link_handle(
781 self,
782 handle: Arc<dyn workload::LinkedWorkloadHandle>,
783 ) -> HyperResult<Node<Attached>> {
784 let runtime_config = self
785 .pending_runtime_config
786 .expect("Node<Init> without pending runtime config");
787 let hyper_inner = self.hyper;
788 let mailbox_backpressure_threshold =
789 hyper_inner.config.resolved_mailbox_backpressure_threshold();
790 let credential_expiry_warning = hyper_inner.config.credential_expiry_warning;
791 let mut node_inner = crate::lifecycle::node::Inner::build(
792 runtime_config,
793 crate::workload::Workload::Linked(handle.clone()),
794 None,
795 None,
796 mailbox_backpressure_threshold,
797 credential_expiry_warning,
798 )
799 .await
800 .map_err(|e| HyperError::Runtime(e.to_string()))?;
801 let observer: Arc<dyn crate::lifecycle::hooks::WorkloadHookObserver> =
802 Arc::new(crate::workload::LinkedHandleObserver { handle });
803 node_inner.hook_observer = Some(observer);
804 Ok(Node {
805 hyper: hyper_inner,
806 attachment: Some(Attachment {
807 node: node_inner,
808 verified: None,
809 package_bytes: bytes::Bytes::new(),
810 }),
811 pending_runtime_config: None,
812 _state: std::marker::PhantomData,
813 })
814 }
815
816 pub async fn link<W: actr_framework::Workload>(
825 self,
826 workload: W,
827 ) -> HyperResult<Node<Attached>> {
828 let handle: Arc<dyn workload::LinkedWorkloadHandle> =
829 workload::WorkloadAdapter::new(workload);
830 self.link_handle(handle).await
831 }
832}
833
834#[cfg(not(target_arch = "wasm32"))]
837impl Node<Attached> {
838 pub fn with_hook_observer<W: actr_framework::Workload>(mut self, observer: W) -> Self {
845 let attachment = self
846 .attachment
847 .as_mut()
848 .expect("Node<Attached> without attachment");
849 let handle: Arc<dyn workload::LinkedWorkloadHandle> =
850 workload::WorkloadAdapter::new(observer);
851 let observer: Arc<dyn crate::lifecycle::hooks::WorkloadHookObserver> =
852 Arc::new(crate::workload::LinkedHandleObserver { handle });
853 attachment.node.hook_observer = crate::lifecycle::hooks::chain_observers(
854 attachment.node.hook_observer.take(),
855 Some(observer),
856 );
857 self
858 }
859
860 pub async fn register(self, ais_endpoint: &str) -> HyperResult<Node<Registered>> {
868 let attachment = self
869 .attachment
870 .as_ref()
871 .expect("Node<Attached> without attachment");
872 let service_spec = if let Some(verified) = attachment.verified.as_ref() {
873 crate::service_spec::calculate_service_spec_from_package(
874 &attachment.package_bytes,
875 &verified.manifest,
876 )?
877 } else {
878 None
879 };
880 self.register_with(ais_endpoint, service_spec).await
881 }
882
883 pub async fn register_with(
889 mut self,
890 ais_endpoint: &str,
891 service_spec: Option<ServiceSpec>,
892 ) -> HyperResult<Node<Registered>> {
893 let attachment = self
894 .attachment
895 .as_mut()
896 .expect("Node<Attached> without attachment");
897 let realm_id = attachment.node.config.realm.realm_id;
898 let acl = attachment.node.config.acl.clone();
899 let realm_secret = attachment.node.config.realm_secret.clone();
900
901 let register_ok = if let Some(verified) = attachment.verified.as_ref() {
902 let verified = verified.clone();
903 bootstrap_credential_inner(
904 &self.hyper,
905 &verified,
906 ais_endpoint,
907 realm_id,
908 service_spec,
909 acl,
910 realm_secret.as_deref(),
911 )
912 .await?
913 } else {
914 bootstrap_linked_credential_inner(&attachment.node.config, ais_endpoint, service_spec)
915 .await?
916 };
917
918 attachment.node.set_preregistered_credential(register_ok);
919
920 Ok(Node {
921 hyper: self.hyper,
922 attachment: self.attachment,
923 pending_runtime_config: None,
924 _state: std::marker::PhantomData,
925 })
926 }
927
928 pub fn create_network_event_handle(
931 &mut self,
932 debounce_ms: u64,
933 ) -> crate::lifecycle::NetworkEventHandle {
934 self.attachment
935 .as_mut()
936 .expect("Node<Attached> without attachment")
937 .node
938 .create_network_event_handle(debounce_ms)
939 }
940
941 pub fn ais_endpoint(&self) -> &str {
945 &self
946 .attachment
947 .as_ref()
948 .expect("Node<Attached> without attachment")
949 .node
950 .config
951 .ais_endpoint
952 }
953}
954
955#[cfg(not(target_arch = "wasm32"))]
958impl Node<Registered> {
959 pub async fn start(self) -> actr_protocol::ActorResult<crate::actr_ref::ActrRef> {
961 let Attachment { node, .. } = self
962 .attachment
963 .expect("Node<Registered> without attachment");
964 node.start().await
965 }
966
967 pub fn create_network_event_handle(
970 &mut self,
971 debounce_ms: u64,
972 ) -> crate::lifecycle::NetworkEventHandle {
973 self.attachment
974 .as_mut()
975 .expect("Node<Registered> without attachment")
976 .node
977 .create_network_event_handle(debounce_ms)
978 }
979}
980
981#[cfg(not(target_arch = "wasm32"))]
986impl Hyper {
987 pub fn resolve_storage_path(&self, manifest: &PackageManifest) -> HyperResult<PathBuf> {
991 resolve_storage_path_for(&self.inner, manifest)
992 }
993
994 pub async fn bootstrap_credential(
1016 &self,
1017 verified: &VerifiedPackage,
1018 ais_endpoint: &str,
1019 realm_id: u32,
1020 service_spec: Option<ServiceSpec>,
1021 acl: Option<Acl>,
1022 ) -> HyperResult<register_response::RegisterOk> {
1023 bootstrap_credential_inner(
1024 &self.inner,
1025 verified,
1026 ais_endpoint,
1027 realm_id,
1028 service_spec,
1029 acl,
1030 None,
1031 )
1032 .await
1033 }
1034
1035 pub fn instance_id(&self) -> &str {
1037 &self.inner.instance_id
1038 }
1039
1040 pub fn config(&self) -> &HyperConfig {
1042 &self.inner.config
1043 }
1044}
1045
1046#[cfg(not(target_arch = "wasm32"))]
1047fn resolve_storage_path_for(
1048 inner: &HyperInner,
1049 manifest: &PackageManifest,
1050) -> HyperResult<PathBuf> {
1051 let resolver = config::NamespaceResolver::new(&inner.config, &inner.instance_id)?
1052 .with_actor_type(&manifest.manufacturer, &manifest.name, &manifest.version);
1053 resolver.resolve(&inner.config.storage_path_template)
1054}
1055
1056#[cfg(not(target_arch = "wasm32"))]
1060pub(crate) async fn load_workload_package_inner(
1061 inner: &HyperInner,
1062 package: &WorkloadPackage,
1063) -> HyperResult<LoadedWorkload> {
1064 let bytes = package.bytes();
1065 let verified = inner.config.trust_provider.verify_package(bytes).await?;
1066 let binary_kind = detect_binary_kind(&verified.manifest)?;
1067 let workload = match binary_kind {
1068 BinaryKind::Wasm => load_wasm_workload_inner(inner, bytes, &verified.manifest).await?,
1069 BinaryKind::DynClib => load_dynclib_workload_inner(inner, bytes, &verified.manifest)?,
1070 };
1071 Ok(LoadedWorkload {
1072 verified,
1073 binary_kind,
1074 workload,
1075 })
1076}
1077
1078#[cfg(not(target_arch = "wasm32"))]
1079async fn load_wasm_workload_inner(
1080 _inner: &HyperInner,
1081 bytes: &[u8],
1082 manifest: &PackageManifest,
1083) -> HyperResult<crate::workload::Workload> {
1084 #[cfg(feature = "wasm-engine")]
1085 {
1086 if matches!(
1091 manifest.binary.resolved_kind(),
1092 actr_pack::BinaryKind::CoreModule
1093 ) {
1094 return Err(HyperError::InvalidManifest(format!(
1095 "package `{}` uses the legacy core wasm module format, which was retired in Phase 1. \
1096 Rebuild with actr 0.2+ (`actr build`, target wasm32-wasip2 + wasm-component-ld 0.5.22+) \
1097 to produce a Component Model binary, and set `binary.kind = \"component\"` in manifest.toml.",
1098 manifest.actr_type_str()
1099 )));
1100 }
1101
1102 let wasm_bytes = actr_pack::load_binary(bytes).map_err(|e| {
1103 HyperError::Runtime(format!(
1104 "failed to extract package binary `{}` for target `{}`: {e}",
1105 manifest.binary.path, manifest.binary.target
1106 ))
1107 })?;
1108 let host = crate::wasm::WasmHost::compile(&wasm_bytes).map_err(|e| {
1109 HyperError::Runtime(format!(
1110 "failed to compile WASM package target `{}`: {e}",
1111 manifest.binary.target
1112 ))
1113 })?;
1114 let mut instance = host.instantiate().await.map_err(|e| {
1115 HyperError::Runtime(format!(
1116 "failed to instantiate WASM package target `{}`: {e}",
1117 manifest.binary.target
1118 ))
1119 })?;
1120 instance
1121 .init(&actr_framework::guest::dynclib_abi::InitPayloadV1 {
1122 version: actr_framework::guest::dynclib_abi::version::V1,
1123 actr_type: manifest.actr_type_str(),
1124 credential: Vec::new(),
1125 actor_id: Vec::new(),
1126 realm_id: 0,
1127 })
1128 .map_err(|e| {
1129 HyperError::Runtime(format!(
1130 "failed to initialize WASM package target `{}`: {e}",
1131 manifest.binary.target
1132 ))
1133 })?;
1134 Ok(crate::workload::Workload::Wasm(instance))
1135 }
1136
1137 #[cfg(not(feature = "wasm-engine"))]
1138 {
1139 let _ = (bytes, manifest);
1140 Err(HyperError::Runtime(
1141 "package target requires the `wasm-engine` feature, but it is not enabled".to_string(),
1142 ))
1143 }
1144}
1145
1146#[cfg(not(target_arch = "wasm32"))]
1147fn load_dynclib_workload_inner(
1148 _inner: &HyperInner,
1149 bytes: &[u8],
1150 manifest: &PackageManifest,
1151) -> HyperResult<crate::workload::Workload> {
1152 #[cfg(feature = "dynclib-engine")]
1153 {
1154 let cache_path = ensure_dynclib_cache_path(&_inner.config.data_dir, bytes, manifest)?;
1155 let host = load_dynclib_host_with_rebuild(&cache_path, bytes, manifest)?;
1156 let instance = host
1157 .instantiate(&actr_framework::guest::dynclib_abi::InitPayloadV1 {
1158 version: actr_framework::guest::dynclib_abi::version::V1,
1159 actr_type: manifest.actr_type_str(),
1160 credential: Vec::new(),
1161 actor_id: Vec::new(),
1162 realm_id: 0,
1163 })
1164 .map_err(|e| {
1165 HyperError::Runtime(format!(
1166 "failed to initialize dynclib package target `{}`: {e}",
1167 manifest.binary.target
1168 ))
1169 })?;
1170
1171 Ok(crate::workload::Workload::DynClib(
1172 crate::dynclib::DynClibWorkload::new(host, instance),
1173 ))
1174 }
1175
1176 #[cfg(not(feature = "dynclib-engine"))]
1177 {
1178 let _ = (bytes, manifest);
1179 Err(HyperError::Runtime(
1180 "package target requires the `dynclib-engine` feature, but it is not enabled"
1181 .to_string(),
1182 ))
1183 }
1184}
1185
1186#[cfg(not(target_arch = "wasm32"))]
1187async fn bootstrap_credential_inner(
1188 inner: &HyperInner,
1189 verified: &VerifiedPackage,
1190 ais_endpoint: &str,
1191 realm_id: u32,
1192 service_spec: Option<ServiceSpec>,
1193 acl: Option<Acl>,
1194 realm_secret: Option<&str>,
1195) -> HyperResult<register_response::RegisterOk> {
1196 let manifest = &verified.manifest;
1197 info!(
1198 actr_type = manifest.actr_type_str(),
1199 ais_endpoint, realm_id, "starting credential bootstrap with AIS"
1200 );
1201
1202 let storage_path = resolve_storage_path_for(inner, manifest)?;
1204 let store: Arc<dyn KvStore> = if let Some(ref platform) = inner.platform {
1205 let ns = storage_path.to_string_lossy().to_string();
1206 platform
1207 .secret_store(&ns)
1208 .await
1209 .map_err(|e| HyperError::Storage(format!("failed to open secret store: {e}")))?
1210 } else {
1211 Arc::new(ActorStore::open(&storage_path).await?)
1212 };
1213
1214 let valid_psk = load_valid_psk_dyn(&*store).await?;
1216
1217 let mut ais = AisClient::new(ais_endpoint);
1219 if let Some(secret) = realm_secret {
1220 ais = ais.with_realm_secret(secret);
1221 }
1222
1223 let actr_type = ActrType {
1224 manufacturer: manifest.manufacturer.clone(),
1225 name: manifest.name.clone(),
1226 version: manifest.version.clone(),
1227 };
1228 let realm = Realm { realm_id };
1229
1230 let response = if let Some(psk_token) = valid_psk {
1231 debug!(
1233 actr_type = manifest.actr_type_str(),
1234 "renewing credential using PSK"
1235 );
1236 let req = RegisterRequest {
1237 actr_type,
1238 realm,
1239 service_spec,
1240 acl,
1241 service: None,
1242 ws_address: None,
1243 manifest_raw: None,
1244 mfr_signature: None,
1245 psk_token: Some(psk_token.into()),
1246 target: Some(manifest.binary.target.clone()),
1247 auth_mode: Some(RegisterAuthMode::Package as i32),
1248 };
1249 ais.register_with_psk(req).await?
1250 } else {
1251 info!(
1253 actr_type = manifest.actr_type_str(),
1254 "first registration: registering with AIS using MFR manifest"
1255 );
1256
1257 let req = RegisterRequest {
1258 actr_type,
1259 realm,
1260 service_spec,
1261 acl,
1262 service: None,
1263 ws_address: None,
1264 manifest_raw: Some(verified.manifest_raw.clone().into()),
1265 mfr_signature: Some(verified.sig_raw.clone().into()),
1266 psk_token: None,
1267 target: Some(manifest.binary.target.clone()),
1268 auth_mode: Some(RegisterAuthMode::Package as i32),
1269 };
1270 ais.register_with_manifest(req).await?
1271 };
1272
1273 let ok = match response.result {
1275 Some(register_response::Result::Success(ok)) => ok,
1276 Some(register_response::Result::Error(e)) => {
1277 error!(
1278 actr_type = manifest.actr_type_str(),
1279 error_code = e.code,
1280 error_message = %e.message,
1281 "AIS registration returned error"
1282 );
1283 return Err(HyperError::AisBootstrapFailed(format!(
1284 "AIS rejected registration (code={}): {}",
1285 e.code, e.message
1286 )));
1287 }
1288 None => {
1289 error!(
1290 actr_type = manifest.actr_type_str(),
1291 "AIS response missing result field"
1292 );
1293 return Err(HyperError::AisBootstrapFailed(
1294 "AIS response missing result field".to_string(),
1295 ));
1296 }
1297 };
1298
1299 if let (Some(psk), Some(psk_expires_at)) = (&ok.psk, ok.psk_expires_at) {
1301 info!(
1302 actr_type = manifest.actr_type_str(),
1303 psk_expires_at, "received PSK from AIS, storing in ActorStore"
1304 );
1305 let expires_at_bytes = (psk_expires_at as u64).to_le_bytes().to_vec();
1306 store
1307 .batch(vec![
1308 KvOp::Set {
1309 key: "hyper:psk:token".to_string(),
1310 value: psk.to_vec(),
1311 },
1312 KvOp::Set {
1313 key: "hyper:psk:expires_at".to_string(),
1314 value: expires_at_bytes,
1315 },
1316 ])
1317 .await
1318 .map_err(|e| HyperError::Storage(format!("failed to store PSK: {e}")))?;
1319 debug!(
1320 actr_type = manifest.actr_type_str(),
1321 "PSK successfully persisted to ActorStore"
1322 );
1323 }
1324
1325 let pubkey_bytes = ok.signing_pubkey.to_vec();
1327 let key_id_bytes = ok.signing_key_id.to_le_bytes().to_vec();
1328 store
1329 .batch(vec![
1330 KvOp::Set {
1331 key: "hyper:ais:signing_pubkey".to_string(),
1332 value: pubkey_bytes,
1333 },
1334 KvOp::Set {
1335 key: "hyper:ais:signing_key_id".to_string(),
1336 value: key_id_bytes,
1337 },
1338 ])
1339 .await
1340 .map_err(|e| HyperError::Storage(format!("failed to store signing key: {e}")))?;
1341 debug!(
1342 actr_type = manifest.actr_type_str(),
1343 signing_key_id = ok.signing_key_id,
1344 "AIS signing public key persisted to ActorStore"
1345 );
1346
1347 info!(
1348 actr_type = manifest.actr_type_str(),
1349 credential_len = ok.credential.encode_to_vec().len(),
1350 "AIS credential bootstrap succeeded"
1351 );
1352
1353 Ok(ok)
1354}
1355
1356#[cfg(not(target_arch = "wasm32"))]
1357async fn bootstrap_linked_credential_inner(
1358 config: &actr_config::RuntimeConfig,
1359 ais_endpoint: &str,
1360 service_spec: Option<ServiceSpec>,
1361) -> HyperResult<register_response::RegisterOk> {
1362 let mut ais = AisClient::new(ais_endpoint);
1363 if let Some(ref secret) = config.realm_secret {
1364 ais = ais.with_realm_secret(secret.clone());
1365 }
1366
1367 let req = build_linked_register_request(config, service_spec);
1368 let response = ais.register_linked(req).await?;
1369 match response.result {
1370 Some(register_response::Result::Success(ok)) => Ok(ok),
1371 Some(register_response::Result::Error(e)) => Err(HyperError::AisBootstrapFailed(format!(
1372 "AIS rejected registration (code={}): {}",
1373 e.code, e.message
1374 ))),
1375 None => Err(HyperError::AisBootstrapFailed(
1376 "AIS response missing result field".to_string(),
1377 )),
1378 }
1379}
1380
1381#[cfg(not(target_arch = "wasm32"))]
1382fn build_linked_register_request(
1383 config: &actr_config::RuntimeConfig,
1384 service_spec: Option<ServiceSpec>,
1385) -> RegisterRequest {
1386 let ws_address = if let Some(port) = config.websocket_listen_port {
1387 let host = config
1388 .websocket_advertised_host
1389 .as_deref()
1390 .unwrap_or("127.0.0.1");
1391 Some(format!("ws://{}:{}", host, port))
1392 } else {
1393 None
1394 };
1395
1396 RegisterRequest {
1397 actr_type: config.actr_type().clone(),
1398 realm: config.realm,
1399 service_spec,
1400 acl: config.acl.clone(),
1401 service: None,
1402 ws_address,
1403 auth_mode: Some(RegisterAuthMode::Linked as i32),
1404 ..Default::default()
1405 }
1406}
1407
1408#[cfg(not(target_arch = "wasm32"))]
1411async fn load_valid_psk_dyn(store: &dyn KvStore) -> HyperResult<Option<Vec<u8>>> {
1415 let token = store
1416 .get("hyper:psk:token")
1417 .await
1418 .map_err(|e| HyperError::Storage(format!("failed to read PSK token: {e}")))?;
1419 let expires_at_raw = store
1420 .get("hyper:psk:expires_at")
1421 .await
1422 .map_err(|e| HyperError::Storage(format!("failed to read PSK expires_at: {e}")))?;
1423
1424 check_psk_expiry(token, expires_at_raw)
1425}
1426
1427#[cfg(all(not(target_arch = "wasm32"), test))]
1431async fn load_valid_psk(store: &ActorStore) -> HyperResult<Option<Vec<u8>>> {
1432 let token = store.kv_get("hyper:psk:token").await?;
1433 let expires_at_raw = store.kv_get("hyper:psk:expires_at").await?;
1434
1435 check_psk_expiry(token, expires_at_raw)
1436}
1437
1438#[cfg(not(target_arch = "wasm32"))]
1439fn check_psk_expiry(
1441 token: Option<Vec<u8>>,
1442 expires_at_raw: Option<Vec<u8>>,
1443) -> HyperResult<Option<Vec<u8>>> {
1444 match (token, expires_at_raw) {
1445 (Some(token), Some(expires_bytes)) => {
1446 if expires_bytes.len() != 8 {
1448 warn!("PSK expires_at has unexpected format, falling back to first registration");
1449 return Ok(None);
1450 }
1451 let expires_at = u64::from_le_bytes(expires_bytes.as_slice().try_into().unwrap());
1452
1453 let now_secs = SystemTime::now()
1455 .duration_since(UNIX_EPOCH)
1456 .unwrap_or_default()
1457 .as_secs();
1458
1459 if now_secs >= expires_at {
1460 warn!(
1461 psk_expires_at = expires_at,
1462 now = now_secs,
1463 "PSK expired, falling back to first registration"
1464 );
1465 Ok(None)
1466 } else {
1467 debug!(
1468 psk_expires_at = expires_at,
1469 now = now_secs,
1470 remaining_secs = expires_at - now_secs,
1471 "PSK valid, using PSK renewal path"
1472 );
1473 Ok(Some(token))
1474 }
1475 }
1476 _ => {
1477 debug!("no PSK in ActorStore, proceeding with first registration");
1478 Ok(None)
1479 }
1480 }
1481}
1482
1483#[cfg(not(target_arch = "wasm32"))]
1484#[cfg(not(target_arch = "wasm32"))]
1485fn detect_binary_kind(manifest: &PackageManifest) -> HyperResult<BinaryKind> {
1486 if manifest.binary.is_wasm_target() {
1487 return Ok(BinaryKind::Wasm);
1488 }
1489
1490 if is_compatible_native_target(&manifest.binary.target) {
1491 return Ok(BinaryKind::DynClib);
1492 }
1493
1494 Err(HyperError::InvalidManifest(format!(
1495 "unsupported binary target `{}` for host `{}-{}`; expected `wasm32-*` or a native target matching this host",
1496 manifest.binary.target,
1497 std::env::consts::ARCH,
1498 std::env::consts::OS,
1499 )))
1500}
1501
1502#[cfg(not(target_arch = "wasm32"))]
1508fn is_compatible_native_target(target: &str) -> bool {
1509 let segments: Vec<&str> = target.split('-').filter(|s| !s.is_empty()).collect();
1510 if segments.len() < 3 {
1511 return false;
1512 }
1513
1514 let target_arch = segments[0];
1515 let target_os = segments[2];
1517
1518 let arch_matches = match (target_arch, std::env::consts::ARCH) {
1520 (a, b) if a == b => true,
1521 ("x86_64", "x86_64") => true,
1522 ("aarch64", "aarch64") => true,
1523 _ => false,
1524 };
1525
1526 let os_matches = match (target_os, std::env::consts::OS) {
1528 (a, b) if a == b => true,
1529 ("darwin", "macos") | ("macos", "darwin") => true,
1530 _ => false,
1531 };
1532
1533 arch_matches && os_matches
1534}
1535
1536#[cfg(all(
1537 not(target_arch = "wasm32"),
1538 feature = "dynclib-engine",
1539 target_os = "macos"
1540))]
1541fn dynclib_tempfile_suffix() -> &'static str {
1542 ".dylib"
1543}
1544
1545#[cfg(all(
1546 not(target_arch = "wasm32"),
1547 feature = "dynclib-engine",
1548 target_os = "linux"
1549))]
1550fn dynclib_tempfile_suffix() -> &'static str {
1551 ".so"
1552}
1553
1554#[cfg(all(
1555 not(target_arch = "wasm32"),
1556 feature = "dynclib-engine",
1557 target_os = "windows"
1558))]
1559fn dynclib_tempfile_suffix() -> &'static str {
1560 ".dll"
1561}
1562
1563#[cfg(all(
1564 not(target_arch = "wasm32"),
1565 feature = "dynclib-engine",
1566 not(any(target_os = "macos", target_os = "linux", target_os = "windows"))
1567))]
1568fn dynclib_tempfile_suffix() -> &'static str {
1569 ".dynlib"
1570}
1571
1572#[cfg(all(not(target_arch = "wasm32"), feature = "dynclib-engine"))]
1573const DYNCLIB_CACHE_DIR: &str = "dynclib-cache";
1574
1575#[cfg(all(not(target_arch = "wasm32"), feature = "dynclib-engine"))]
1576fn dynclib_cache_dir(data_dir: &Path) -> PathBuf {
1577 data_dir.join(DYNCLIB_CACHE_DIR)
1578}
1579
1580#[cfg(all(not(target_arch = "wasm32"), feature = "dynclib-engine"))]
1581fn dynclib_cache_path(data_dir: &Path, binary_hash: &[u8; 32]) -> PathBuf {
1582 dynclib_cache_dir(data_dir).join(format!(
1583 "{}{}",
1584 hex::encode(binary_hash),
1585 dynclib_tempfile_suffix()
1586 ))
1587}
1588
1589#[cfg(all(not(target_arch = "wasm32"), feature = "dynclib-engine"))]
1590fn extract_dynclib_binary(bytes: &[u8], manifest: &PackageManifest) -> HyperResult<Vec<u8>> {
1591 actr_pack::load_binary(bytes).map_err(|e| {
1592 HyperError::Runtime(format!(
1593 "failed to extract package binary `{}` for target `{}`: {e}",
1594 manifest.binary.path, manifest.binary.target
1595 ))
1596 })
1597}
1598
1599#[cfg(all(not(target_arch = "wasm32"), feature = "dynclib-engine"))]
1600fn write_dynclib_cache_file(cache_path: &Path, binary_bytes: &[u8]) -> HyperResult<()> {
1601 let cache_dir = cache_path.parent().ok_or_else(|| {
1602 HyperError::Runtime("dynclib cache path has no parent directory".to_string())
1603 })?;
1604 std::fs::create_dir_all(cache_dir).map_err(|e| {
1605 HyperError::Runtime(format!(
1606 "failed to create dynclib cache directory `{}`: {e}",
1607 cache_dir.display()
1608 ))
1609 })?;
1610
1611 let mut temp_file = tempfile::Builder::new()
1612 .prefix("actr-dynclib-")
1613 .tempfile_in(cache_dir)
1614 .map_err(|e| {
1615 HyperError::Runtime(format!(
1616 "failed to allocate dynclib cache temp file in `{}`: {e}",
1617 cache_dir.display()
1618 ))
1619 })?;
1620
1621 temp_file.write_all(binary_bytes).map_err(|e| {
1622 HyperError::Runtime(format!(
1623 "failed to write dynclib cache temp file `{}`: {e}",
1624 temp_file.path().display()
1625 ))
1626 })?;
1627 temp_file.flush().map_err(|e| {
1628 HyperError::Runtime(format!(
1629 "failed to flush dynclib cache temp file `{}`: {e}",
1630 temp_file.path().display()
1631 ))
1632 })?;
1633
1634 match temp_file.persist_noclobber(cache_path) {
1635 Ok(_) => Ok(()),
1636 Err(err) if err.error.kind() == std::io::ErrorKind::AlreadyExists => Ok(()),
1637 Err(err) => Err(HyperError::Runtime(format!(
1638 "failed to persist dynclib cache file `{}`: {}",
1639 cache_path.display(),
1640 err.error
1641 ))),
1642 }
1643}
1644
1645#[cfg(all(not(target_arch = "wasm32"), feature = "dynclib-engine"))]
1646fn ensure_dynclib_cache_path(
1647 data_dir: &Path,
1648 bytes: &[u8],
1649 manifest: &PackageManifest,
1650) -> HyperResult<PathBuf> {
1651 let binary_hash = manifest
1652 .binary
1653 .hash_bytes()
1654 .map_err(|e| HyperError::InvalidManifest(e.to_string()))?;
1655 let cache_path = dynclib_cache_path(data_dir, &binary_hash);
1656 if cache_path.exists() {
1657 return Ok(cache_path);
1658 }
1659
1660 let binary_bytes = extract_dynclib_binary(bytes, manifest)?;
1661 write_dynclib_cache_file(&cache_path, &binary_bytes)?;
1662 Ok(cache_path)
1663}
1664
1665#[cfg(all(not(target_arch = "wasm32"), feature = "dynclib-engine"))]
1666fn rebuild_dynclib_cache_file(
1667 cache_path: &Path,
1668 bytes: &[u8],
1669 manifest: &PackageManifest,
1670) -> HyperResult<()> {
1671 match std::fs::remove_file(cache_path) {
1672 Ok(()) => {}
1673 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
1674 Err(err) => {
1675 return Err(HyperError::Runtime(format!(
1676 "failed to remove corrupt dynclib cache file `{}`: {err}",
1677 cache_path.display()
1678 )));
1679 }
1680 }
1681
1682 let binary_bytes = extract_dynclib_binary(bytes, manifest)?;
1683 write_dynclib_cache_file(cache_path, &binary_bytes)
1684}
1685
1686#[cfg(all(not(target_arch = "wasm32"), feature = "dynclib-engine"))]
1687fn load_dynclib_host_with_rebuild(
1688 cache_path: &Path,
1689 bytes: &[u8],
1690 manifest: &PackageManifest,
1691) -> HyperResult<crate::dynclib::DynclibHost> {
1692 match crate::dynclib::DynclibHost::load(cache_path) {
1693 Ok(host) => Ok(host),
1694 Err(first_err) => {
1695 warn!(
1696 path = %cache_path.display(),
1697 target = %manifest.binary.target,
1698 error = %first_err,
1699 "cached dynclib load failed, rebuilding cache once"
1700 );
1701 rebuild_dynclib_cache_file(cache_path, bytes, manifest)?;
1702 crate::dynclib::DynclibHost::load(cache_path).map_err(|second_err| {
1703 HyperError::Runtime(format!(
1704 "failed to load dynclib package target `{}` from cache `{}` after rebuild; first load error: {first_err}; second load error: {second_err}",
1705 manifest.binary.target,
1706 cache_path.display()
1707 ))
1708 })
1709 }
1710 }
1711}
1712
1713#[cfg(not(target_arch = "wasm32"))]
1714async fn load_or_create_instance_uid_local(data_dir: &std::path::Path) -> HyperResult<String> {
1719 let id_file = data_dir.join(".hyper-instance-uid");
1720
1721 if id_file.exists() {
1722 let id = tokio::fs::read_to_string(&id_file)
1723 .await
1724 .map_err(|e| HyperError::Storage(format!("failed to read instance_uid file: {e}")))?;
1725 let id = id.trim().to_string();
1726 if !id.is_empty() {
1727 return Ok(id);
1728 }
1729 warn!("instance_uid file is empty; generating a new one");
1730 }
1731
1732 let new_id = Uuid::new_v4().to_string();
1733 tokio::fs::write(&id_file, &new_id)
1734 .await
1735 .map_err(|e| HyperError::Storage(format!("failed to write instance_uid file: {e}")))?;
1736 info!(instance_uid = %new_id, "generated a new Hyper instance_uid");
1737 Ok(new_id)
1738}
1739
1740#[cfg(all(not(target_arch = "wasm32"), test))]
1741mod tests {
1742 use super::*;
1743 use ed25519_dalek::SigningKey;
1744 use rand::rngs::OsRng;
1745 #[cfg(feature = "dynclib-engine")]
1746 use std::sync::{Arc, Barrier};
1747 use tempfile::TempDir;
1748
1749 fn dev_config(dir: &TempDir) -> HyperConfig {
1750 let signing_key = SigningKey::generate(&mut OsRng);
1751 let pubkey = signing_key.verifying_key().to_bytes();
1752 HyperConfig::new(
1753 dir.path(),
1754 Arc::new(crate::verify::StaticTrust::new(pubkey).unwrap()),
1755 )
1756 }
1757
1758 #[tokio::test]
1759 async fn init_creates_data_dir_and_instance_id() {
1760 let dir = TempDir::new().unwrap();
1761 let sub = dir.path().join("subdir/nested");
1762 let signing_key = SigningKey::generate(&mut OsRng);
1763 let config = HyperConfig::new(
1764 &sub,
1765 Arc::new(
1766 crate::verify::StaticTrust::new(signing_key.verifying_key().to_bytes()).unwrap(),
1767 ),
1768 );
1769
1770 let hyper = Hyper::new(config).await.unwrap();
1771 assert!(sub.exists());
1772 assert!(!hyper.instance_id().is_empty());
1773 }
1774
1775 #[tokio::test]
1776 async fn instance_id_is_stable_across_reinit() {
1777 let dir = TempDir::new().unwrap();
1778 let config1 = dev_config(&dir);
1779 let hyper1 = Hyper::new(config1).await.unwrap();
1780 let id1 = hyper1.instance_id().to_string();
1781
1782 let config2 = dev_config(&dir);
1783 let hyper2 = Hyper::new(config2).await.unwrap();
1784 let id2 = hyper2.instance_id().to_string();
1785
1786 assert_eq!(id1, id2, "instance_id should remain stable across restarts");
1787 }
1788
1789 #[tokio::test]
1790 async fn verify_package_rejects_non_wasm() {
1791 let dir = TempDir::new().unwrap();
1792 let hyper = Hyper::new(dev_config(&dir)).await.unwrap();
1793 let result = hyper
1794 .verify_package(&WorkloadPackage::new(b"not a wasm file".to_vec()))
1795 .await;
1796 assert!(matches!(result, Err(HyperError::InvalidManifest(_))));
1797 }
1798
1799 #[tokio::test]
1800 async fn verify_package_rejects_non_actr_format() {
1801 let dir = TempDir::new().unwrap();
1802 let hyper = Hyper::new(dev_config(&dir)).await.unwrap();
1803
1804 let result = hyper
1806 .verify_package(&WorkloadPackage::new(b"\0asm\x01\x00\x00\x00".to_vec()))
1807 .await;
1808 assert!(matches!(result, Err(HyperError::InvalidManifest(_))));
1809 }
1810
1811 async fn open_test_store(dir: &TempDir) -> ActorStore {
1814 let db_path = dir.path().join("test.db");
1815 ActorStore::open(&db_path).await.unwrap()
1816 }
1817
1818 #[tokio::test]
1820 async fn psk_valid_returns_token() {
1821 let dir = TempDir::new().unwrap();
1822 let store = open_test_store(&dir).await;
1823
1824 let psk_token = b"test-psk-secret".to_vec();
1825 let expires_at = SystemTime::now()
1827 .duration_since(UNIX_EPOCH)
1828 .unwrap()
1829 .as_secs()
1830 + 3600;
1831
1832 store.kv_set("hyper:psk:token", &psk_token).await.unwrap();
1833 store
1834 .kv_set("hyper:psk:expires_at", &expires_at.to_le_bytes())
1835 .await
1836 .unwrap();
1837
1838 let result = load_valid_psk(&store).await.unwrap();
1839 assert_eq!(result, Some(psk_token), "A valid PSK should be returned");
1840 }
1841
1842 #[tokio::test]
1844 async fn psk_expired_returns_none() {
1845 let dir = TempDir::new().unwrap();
1846 let store = open_test_store(&dir).await;
1847
1848 let psk_token = b"expired-psk".to_vec();
1849 let expires_at = SystemTime::now()
1851 .duration_since(UNIX_EPOCH)
1852 .unwrap()
1853 .as_secs()
1854 .saturating_sub(1);
1855
1856 store.kv_set("hyper:psk:token", &psk_token).await.unwrap();
1857 store
1858 .kv_set("hyper:psk:expires_at", &expires_at.to_le_bytes())
1859 .await
1860 .unwrap();
1861
1862 let result = load_valid_psk(&store).await.unwrap();
1863 assert_eq!(result, None, "An expired PSK should return None");
1864 }
1865
1866 #[tokio::test]
1868 async fn psk_absent_returns_none() {
1869 let dir = TempDir::new().unwrap();
1870 let store = open_test_store(&dir).await;
1871
1872 let result = load_valid_psk(&store).await.unwrap();
1873 assert_eq!(result, None, "Missing PSK should return None");
1874 }
1875
1876 #[tokio::test]
1878 async fn psk_missing_expires_at_returns_none() {
1879 let dir = TempDir::new().unwrap();
1880 let store = open_test_store(&dir).await;
1881
1882 store
1883 .kv_set("hyper:psk:token", b"orphan-token")
1884 .await
1885 .unwrap();
1886 let result = load_valid_psk(&store).await.unwrap();
1889 assert_eq!(result, None, "Missing expires_at should return None");
1890 }
1891
1892 fn fake_manifest() -> VerifiedPackage {
1900 VerifiedPackage {
1901 manifest: actr_pack::PackageManifest {
1902 manufacturer: "test-mfr".to_string(),
1903 name: "TestActor".to_string(),
1904 version: "0.1.0".to_string(),
1905 binary: actr_pack::BinaryEntry {
1906 path: "bin/actor.wasm".to_string(),
1907 target: "wasm32-wasip1".to_string(),
1908 hash: "0".repeat(64),
1909 size: None,
1910 kind: None,
1911 },
1912 signature_algorithm: "ed25519".to_string(),
1913 signing_key_id: None,
1914 resources: vec![],
1915 proto_files: vec![],
1916 lock_file: None,
1917 metadata: actr_pack::ManifestMetadata::default(),
1918 },
1919 manifest_raw: vec![],
1920 sig_raw: vec![0u8; 64],
1921 }
1922 }
1923
1924 fn fake_register_response_bytes(with_psk: bool) -> Vec<u8> {
1926 use actr_protocol::{
1927 AIdCredential, ActrId, ActrType, IdentityClaims, Realm, RegisterResponse,
1928 TurnCredential, register_response,
1929 };
1930
1931 let claims = IdentityClaims {
1932 realm_id: 1,
1933 actor_id: "test-actor-id".to_string(),
1934 expires_at: u64::MAX,
1935 };
1936 let claims_bytes = claims.encode_to_vec();
1937
1938 let credential = AIdCredential {
1939 key_id: 1,
1940 claims: claims_bytes.into(),
1941 signature: vec![0u8; 64].into(),
1942 };
1943
1944 let actr_id = ActrId {
1945 realm: Realm { realm_id: 1 },
1946 serial_number: 42,
1947 r#type: ActrType {
1948 manufacturer: "test-mfr".to_string(),
1949 name: "TestActor".to_string(),
1950 version: "0.1.0".to_string(),
1951 },
1952 };
1953
1954 let turn = TurnCredential {
1955 username: "user".to_string(),
1956 password: "pass".to_string(),
1957 expires_at: u64::MAX,
1958 };
1959
1960 let mut ok = register_response::RegisterOk {
1961 actr_id,
1962 credential,
1963 turn_credential: turn,
1964 credential_expires_at: None,
1965 signaling_heartbeat_interval_secs: 30,
1966 signing_pubkey: vec![0u8; 32].into(),
1967 signing_key_id: 1,
1968 psk: None,
1969 psk_expires_at: None,
1970 };
1971
1972 if with_psk {
1973 ok.psk = Some(b"fresh-psk-from-ais".to_vec().into());
1974 ok.psk_expires_at = Some(
1975 (SystemTime::now()
1976 .duration_since(UNIX_EPOCH)
1977 .unwrap()
1978 .as_secs()
1979 + 86400) as i64,
1980 );
1981 }
1982
1983 RegisterResponse {
1984 result: Some(register_response::Result::Success(ok)),
1985 }
1986 .encode_to_vec()
1987 }
1988
1989 fn test_service_spec() -> Option<ServiceSpec> {
1990 Some(ServiceSpec {
1991 name: "EchoService".to_string(),
1992 description: Some("test service".to_string()),
1993 fingerprint: "fp-123".to_string(),
1994 protobufs: vec![],
1995 published_at: None,
1996 tags: vec!["latest".to_string()],
1997 })
1998 }
1999
2000 fn test_acl() -> Option<Acl> {
2001 Some(Acl { rules: vec![] })
2002 }
2003
2004 fn linked_runtime_config(dir: &TempDir) -> actr_config::RuntimeConfig {
2005 actr_config::RuntimeConfig {
2006 package: actr_config::PackageInfo {
2007 name: "LinkedActor".to_string(),
2008 actr_type: actr_protocol::ActrType {
2009 manufacturer: "test-mfr".to_string(),
2010 name: "LinkedActor".to_string(),
2011 version: "0.1.0".to_string(),
2012 },
2013 description: None,
2014 authors: vec![],
2015 license: None,
2016 },
2017 signaling_url: url::Url::parse("ws://localhost:8081/signaling/ws").unwrap(),
2018 realm: Realm { realm_id: 7 },
2019 ais_endpoint: "http://localhost:8081/ais".to_string(),
2020 realm_secret: Some("test-realm-secret".to_string()),
2021 visible_in_discovery: true,
2022 acl: test_acl(),
2023 mailbox_path: None,
2024 scripts: std::collections::HashMap::new(),
2025 webrtc: actr_config::WebRtcConfig::default(),
2026 websocket_listen_port: Some(9100),
2027 websocket_advertised_host: Some("127.0.0.1".to_string()),
2028 observability: actr_config::ObservabilityConfig {
2029 filter_level: "info".to_string(),
2030 tracing_enabled: false,
2031 tracing_endpoint: "http://localhost:4317".to_string(),
2032 tracing_service_name: "linked-test".to_string(),
2033 },
2034 config_dir: dir.path().to_path_buf(),
2035 trust: vec![],
2036 package_path: None,
2037 web: None,
2038 }
2039 }
2040
2041 #[test]
2042 fn linked_register_request_uses_linked_auth_mode() {
2043 let dir = TempDir::new().unwrap();
2044 let req = build_linked_register_request(&linked_runtime_config(&dir), test_service_spec());
2045
2046 assert_eq!(req.auth_mode, Some(RegisterAuthMode::Linked as i32));
2047 assert_eq!(req.manifest_raw, None);
2048 assert_eq!(req.mfr_signature, None);
2049 assert_eq!(req.psk_token, None);
2050 assert_eq!(req.ws_address.as_deref(), Some("ws://127.0.0.1:9100"));
2051 }
2052
2053 #[tokio::test]
2054 async fn with_actor_type_overrides_pending_runtime_metadata() {
2055 let dir = TempDir::new().unwrap();
2056 let hyper = Hyper::new(dev_config(&dir)).await.unwrap();
2057 let node = Node::from_hyper(hyper, linked_runtime_config(&dir)).with_actor_type(
2058 actr_protocol::ActrType {
2059 manufacturer: "acme".into(),
2060 name: "UnifiedActor".into(),
2061 version: "1.0.0".into(),
2062 },
2063 );
2064
2065 let actr_type = node.runtime_config().actr_type();
2066 assert_eq!(actr_type.manufacturer, "acme");
2067 assert_eq!(actr_type.name, "UnifiedActor");
2068 assert_eq!(actr_type.version, "1.0.0");
2069 }
2070
2071 #[test]
2072 fn compatible_native_target_matches_current_host() {
2073 let current = format!(
2075 "{}-unknown-{}",
2076 std::env::consts::ARCH,
2077 if std::env::consts::OS == "macos" {
2078 "darwin"
2079 } else {
2080 std::env::consts::OS
2081 }
2082 );
2083 assert!(
2084 is_compatible_native_target(¤t),
2085 "current host target `{current}` should be compatible"
2086 );
2087 }
2088
2089 #[test]
2090 fn compatible_native_target_rejects_cross_platform() {
2091 assert!(!is_compatible_native_target("riscv64gc-unknown-linux-gnu"));
2093 assert!(!is_compatible_native_target("s390x-unknown-linux-gnu"));
2094 }
2095
2096 #[test]
2097 fn compatible_native_target_rejects_short_triples() {
2098 assert!(!is_compatible_native_target("invalid-target"));
2099 assert!(!is_compatible_native_target("single"));
2100 assert!(!is_compatible_native_target(""));
2101 }
2102
2103 #[cfg(feature = "dynclib-engine")]
2104 fn fake_dynclib_manifest() -> PackageManifest {
2105 let target = format!(
2106 "{}-unknown-{}",
2107 std::env::consts::ARCH,
2108 if std::env::consts::OS == "macos" {
2109 "darwin"
2110 } else {
2111 std::env::consts::OS
2112 }
2113 );
2114 PackageManifest {
2115 manufacturer: "test-mfr".to_string(),
2116 name: "DynActor".to_string(),
2117 version: "1.0.0".to_string(),
2118 binary: actr_pack::BinaryEntry {
2119 path: format!("bin/actor{}", dynclib_tempfile_suffix()),
2120 target,
2121 hash: String::new(),
2122 size: None,
2123 kind: None,
2124 },
2125 signature_algorithm: "ed25519".to_string(),
2126 signing_key_id: None,
2127 resources: vec![],
2128 proto_files: vec![],
2129 lock_file: None,
2130 metadata: actr_pack::ManifestMetadata::default(),
2131 }
2132 }
2133
2134 #[cfg(feature = "dynclib-engine")]
2135 fn fake_dynclib_package_bytes(binary_bytes: &[u8]) -> (Vec<u8>, PackageManifest) {
2136 let manifest = fake_dynclib_manifest();
2137 let signing_key = SigningKey::generate(&mut OsRng);
2138 let package_bytes = actr_pack::pack(&actr_pack::PackOptions {
2139 manifest: manifest.clone(),
2140 binary_bytes: binary_bytes.to_vec(),
2141 resources: vec![],
2142 proto_files: vec![],
2143 lock_file: None,
2144 signing_key,
2145 })
2146 .unwrap();
2147 let packed_manifest = actr_pack::read_manifest(&package_bytes).unwrap();
2150 (package_bytes, packed_manifest)
2151 }
2152
2153 #[cfg(feature = "dynclib-engine")]
2154 #[test]
2155 fn dynclib_cache_path_uses_hash_and_platform_suffix() {
2156 let dir = TempDir::new().unwrap();
2157 let path = dynclib_cache_path(dir.path(), &[0xAB; 32]);
2158
2159 assert_eq!(path.parent().unwrap(), dynclib_cache_dir(dir.path()));
2160 assert_eq!(
2161 path.file_name().unwrap().to_string_lossy(),
2162 format!("{}{}", hex::encode([0xAB; 32]), dynclib_tempfile_suffix())
2163 );
2164 }
2165
2166 #[cfg(feature = "dynclib-engine")]
2167 #[test]
2168 fn ensure_dynclib_cache_path_preserves_existing_file() {
2169 let dir = TempDir::new().unwrap();
2170 let initial_binary_bytes = b"initial dylib bytes";
2171 let (initial_package_bytes, manifest) = fake_dynclib_package_bytes(initial_binary_bytes);
2172 let cache_path =
2173 ensure_dynclib_cache_path(dir.path(), &initial_package_bytes, &manifest).unwrap();
2174
2175 let second_path =
2179 ensure_dynclib_cache_path(dir.path(), &initial_package_bytes, &manifest).unwrap();
2180
2181 assert_eq!(cache_path, second_path);
2182 assert_eq!(std::fs::read(&cache_path).unwrap(), initial_binary_bytes);
2183 }
2184
2185 #[cfg(feature = "dynclib-engine")]
2186 #[test]
2187 fn ensure_dynclib_cache_path_handles_concurrent_creation() {
2188 let dir = TempDir::new().unwrap();
2189 let binary_bytes = b"shared dylib bytes".to_vec();
2190 let (package_bytes, manifest) = fake_dynclib_package_bytes(&binary_bytes);
2191 let package_bytes = Arc::new(package_bytes);
2192 let binary_bytes = Arc::new(binary_bytes);
2193 let data_dir = Arc::new(dir.path().to_path_buf());
2194 let barrier = Arc::new(Barrier::new(3));
2195
2196 let handles: Vec<_> = (0..2)
2197 .map(|_| {
2198 let barrier = Arc::clone(&barrier);
2199 let data_dir = Arc::clone(&data_dir);
2200 let manifest = manifest.clone();
2201 let package_bytes = Arc::clone(&package_bytes);
2202 std::thread::spawn(move || {
2203 barrier.wait();
2204 ensure_dynclib_cache_path(&data_dir, &package_bytes, &manifest)
2205 })
2206 })
2207 .collect();
2208
2209 barrier.wait();
2210
2211 let results: Vec<_> = handles
2212 .into_iter()
2213 .map(|handle| handle.join().unwrap().unwrap())
2214 .collect();
2215
2216 assert_eq!(results[0], results[1]);
2217 assert_eq!(
2218 std::fs::read(&results[0]).unwrap(),
2219 binary_bytes.as_ref().as_slice()
2220 );
2221 }
2222
2223 #[tokio::test]
2225 async fn bootstrap_first_registration_stores_psk() {
2226 let response_body = fake_register_response_bytes(true);
2227
2228 let mut server = mockito::Server::new_async().await;
2229 let mock = server
2230 .mock("POST", "/register")
2231 .with_status(200)
2232 .with_header("content-type", "application/x-protobuf")
2233 .with_body(response_body)
2234 .create_async()
2235 .await;
2236
2237 let dir = TempDir::new().unwrap();
2238 let config = dev_config(&dir);
2239 let hyper = Hyper::new(config).await.unwrap();
2240
2241 let manifest = fake_manifest();
2242 let result = hyper
2243 .bootstrap_credential(&manifest, &server.url(), 1, test_service_spec(), test_acl())
2244 .await;
2245
2246 mock.assert_async().await;
2247 assert!(
2248 result.is_ok(),
2249 "Initial registration should succeed, got: {:?}",
2250 result.err()
2251 );
2252
2253 let storage_path = hyper.resolve_storage_path(&manifest.manifest).unwrap();
2255 let store = ActorStore::open(&storage_path).await.unwrap();
2256 let psk = store.kv_get("hyper:psk:token").await.unwrap();
2257 assert!(
2258 psk.is_some(),
2259 "PSK should be stored in ActorStore after initial registration"
2260 );
2261 assert_eq!(psk.unwrap(), b"fresh-psk-from-ais".to_vec());
2262 }
2263
2264 #[tokio::test]
2266 async fn bootstrap_psk_renewal_skips_manifest() {
2267 let response_body = fake_register_response_bytes(false);
2268
2269 let mut server = mockito::Server::new_async().await;
2270 let mock = server
2271 .mock("POST", "/register")
2272 .with_status(200)
2273 .with_header("content-type", "application/x-protobuf")
2274 .with_body(response_body)
2275 .expect(1) .create_async()
2277 .await;
2278
2279 let dir = TempDir::new().unwrap();
2280 let config = dev_config(&dir);
2281 let hyper = Hyper::new(config).await.unwrap();
2282
2283 let manifest = fake_manifest();
2285 let storage_path = hyper.resolve_storage_path(&manifest.manifest).unwrap();
2286 let store = ActorStore::open(&storage_path).await.unwrap();
2287
2288 let expires_at = SystemTime::now()
2289 .duration_since(UNIX_EPOCH)
2290 .unwrap()
2291 .as_secs()
2292 + 3600;
2293 store
2294 .kv_set("hyper:psk:token", b"existing-valid-psk")
2295 .await
2296 .unwrap();
2297 store
2298 .kv_set("hyper:psk:expires_at", &expires_at.to_le_bytes())
2299 .await
2300 .unwrap();
2301
2302 let result = hyper
2303 .bootstrap_credential(&manifest, &server.url(), 1, test_service_spec(), test_acl())
2304 .await;
2305
2306 mock.assert_async().await;
2307 assert!(
2308 result.is_ok(),
2309 "PSK renewal should succeed, got: {:?}",
2310 result.err()
2311 );
2312 }
2313
2314 #[tokio::test]
2316 async fn bootstrap_expired_psk_falls_back_to_manifest() {
2317 let response_body = fake_register_response_bytes(true);
2318
2319 let mut server = mockito::Server::new_async().await;
2320 let mock = server
2321 .mock("POST", "/register")
2322 .with_status(200)
2323 .with_header("content-type", "application/x-protobuf")
2324 .with_body(response_body)
2325 .expect(1)
2326 .create_async()
2327 .await;
2328
2329 let dir = TempDir::new().unwrap();
2330 let config = dev_config(&dir);
2331 let hyper = Hyper::new(config).await.unwrap();
2332
2333 let manifest = fake_manifest();
2335 let storage_path = hyper.resolve_storage_path(&manifest.manifest).unwrap();
2336 let store = ActorStore::open(&storage_path).await.unwrap();
2337
2338 let expired_at = SystemTime::now()
2339 .duration_since(UNIX_EPOCH)
2340 .unwrap()
2341 .as_secs()
2342 .saturating_sub(10); store
2344 .kv_set("hyper:psk:token", b"expired-psk")
2345 .await
2346 .unwrap();
2347 store
2348 .kv_set("hyper:psk:expires_at", &expired_at.to_le_bytes())
2349 .await
2350 .unwrap();
2351
2352 let result = hyper
2353 .bootstrap_credential(&manifest, &server.url(), 1, test_service_spec(), test_acl())
2354 .await;
2355
2356 mock.assert_async().await;
2357 assert!(
2358 result.is_ok(),
2359 "Manifest registration should succeed after PSK expiration, got: {:?}",
2360 result.err()
2361 );
2362 }
2363
2364 #[tokio::test]
2366 async fn bootstrap_ais_error_propagates() {
2367 use actr_protocol::{ErrorResponse, RegisterResponse, register_response};
2368
2369 let error_resp = RegisterResponse {
2370 result: Some(register_response::Result::Error(ErrorResponse {
2371 code: 403,
2372 message: "manufacturer not trusted".to_string(),
2373 })),
2374 }
2375 .encode_to_vec();
2376
2377 let mut server = mockito::Server::new_async().await;
2378 let _mock = server
2379 .mock("POST", "/register")
2380 .with_status(200)
2381 .with_header("content-type", "application/x-protobuf")
2382 .with_body(error_resp)
2383 .create_async()
2384 .await;
2385
2386 let dir = TempDir::new().unwrap();
2387 let config = dev_config(&dir);
2388 let hyper = Hyper::new(config).await.unwrap();
2389
2390 let manifest = fake_manifest();
2391 let result = hyper
2392 .bootstrap_credential(&manifest, &server.url(), 1, test_service_spec(), test_acl())
2393 .await;
2394
2395 assert!(
2396 matches!(result, Err(HyperError::AisBootstrapFailed(_))),
2397 "AIS errors should propagate as AisBootstrapFailed, got: {:?}",
2398 result
2399 );
2400 }
2401}