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 fn from_hyper(hyper: Hyper, runtime_config: actr_config::RuntimeConfig) -> Node<Init> {
636 Node {
637 hyper: hyper.inner,
638 attachment: None,
639 pending_runtime_config: Some(runtime_config),
640 _state: std::marker::PhantomData,
641 }
642 }
643
644 pub async fn run_from_config(
653 path: impl AsRef<std::path::Path>,
654 package: &WorkloadPackage,
655 ) -> HyperResult<ActrRef> {
656 let init = Self::from_config_file(path).await?;
657 let ais_endpoint = init
658 .pending_runtime_config
659 .as_ref()
660 .map(|c| c.ais_endpoint.clone())
661 .expect("Node<Init> without pending runtime config");
662 let attached = init.attach(package).await?;
663 let registered = attached.register(&ais_endpoint).await?;
664 registered
665 .start()
666 .await
667 .map_err(|e| HyperError::Runtime(format!("failed to start node: {e}")))
668 }
669}
670
671#[cfg(not(target_arch = "wasm32"))]
674impl Node<Init> {
675 pub fn runtime_config(&self) -> &actr_config::RuntimeConfig {
679 self.pending_runtime_config
680 .as_ref()
681 .expect("Node<Init> without pending runtime config")
682 }
683
684 pub fn with_actor_type(mut self, actor_type: actr_protocol::ActrType) -> Self {
690 let runtime_config = self
691 .pending_runtime_config
692 .as_mut()
693 .expect("Node<Init> without pending runtime config");
694 runtime_config.package.name = actor_type.name.clone();
695 runtime_config.package.actr_type = actor_type;
696 self
697 }
698}
699
700#[cfg(not(target_arch = "wasm32"))]
701impl Node<Init> {
702 pub async fn attach(self, package: &WorkloadPackage) -> HyperResult<Node<Attached>> {
710 let runtime_config = self
711 .pending_runtime_config
712 .expect("Node<Init> without pending runtime config");
713 let hyper_inner = self.hyper;
714 let loaded = load_workload_package_inner(&hyper_inner, package).await?;
715 let packaged_lock = actr_pack::read_lock_file(package.bytes())
716 .map_err(|e| HyperError::Runtime(e.to_string()))?
717 .map(|bytes| {
718 let raw = std::str::from_utf8(&bytes).map_err(|e| {
719 HyperError::Runtime(format!("manifest.lock.toml is not valid UTF-8: {e}"))
720 })?;
721 actr_config::lock::LockFile::from_str(raw).map_err(|e| {
722 HyperError::Runtime(format!("failed to parse manifest.lock.toml: {e}"))
723 })
724 })
725 .transpose()?;
726 let mailbox_backpressure_threshold =
727 hyper_inner.config.resolved_mailbox_backpressure_threshold();
728 let credential_expiry_warning = hyper_inner.config.credential_expiry_warning;
729 let mut node_inner = crate::lifecycle::node::Inner::build(
730 runtime_config,
731 loaded.workload,
732 Some(loaded.verified.manifest.clone()),
733 packaged_lock,
734 mailbox_backpressure_threshold,
735 credential_expiry_warning,
736 )
737 .await
738 .map_err(|e| HyperError::Runtime(e.to_string()))?;
739 let observer: Arc<dyn crate::lifecycle::hooks::WorkloadHookObserver> =
740 Arc::new(crate::workload::PackageHookObserver {
741 workload_dispatch: node_inner.workload_dispatch.clone(),
742 });
743 node_inner.hook_observer = Some(observer);
744 Ok(Node {
745 hyper: hyper_inner,
746 attachment: Some(Attachment {
747 node: node_inner,
748 verified: Some(loaded.verified),
749 package_bytes: package.bytes.clone(),
750 }),
751 pending_runtime_config: None,
752 _state: std::marker::PhantomData,
753 })
754 }
755
756 pub(crate) async fn link_handle(
766 self,
767 handle: Arc<dyn workload::LinkedWorkloadHandle>,
768 ) -> HyperResult<Node<Attached>> {
769 let runtime_config = self
770 .pending_runtime_config
771 .expect("Node<Init> without pending runtime config");
772 let hyper_inner = self.hyper;
773 let mailbox_backpressure_threshold =
774 hyper_inner.config.resolved_mailbox_backpressure_threshold();
775 let credential_expiry_warning = hyper_inner.config.credential_expiry_warning;
776 let mut node_inner = crate::lifecycle::node::Inner::build(
777 runtime_config,
778 crate::workload::Workload::Linked(handle.clone()),
779 None,
780 None,
781 mailbox_backpressure_threshold,
782 credential_expiry_warning,
783 )
784 .await
785 .map_err(|e| HyperError::Runtime(e.to_string()))?;
786 let observer: Arc<dyn crate::lifecycle::hooks::WorkloadHookObserver> =
787 Arc::new(crate::workload::LinkedHandleObserver { handle });
788 node_inner.hook_observer = Some(observer);
789 Ok(Node {
790 hyper: hyper_inner,
791 attachment: Some(Attachment {
792 node: node_inner,
793 verified: None,
794 package_bytes: bytes::Bytes::new(),
795 }),
796 pending_runtime_config: None,
797 _state: std::marker::PhantomData,
798 })
799 }
800
801 pub async fn link<W: actr_framework::Workload>(
810 self,
811 workload: W,
812 ) -> HyperResult<Node<Attached>> {
813 let handle: Arc<dyn workload::LinkedWorkloadHandle> =
814 workload::WorkloadAdapter::new(workload);
815 self.link_handle(handle).await
816 }
817}
818
819#[cfg(not(target_arch = "wasm32"))]
822impl Node<Attached> {
823 pub fn with_hook_observer<W: actr_framework::Workload>(mut self, observer: W) -> Self {
830 let attachment = self
831 .attachment
832 .as_mut()
833 .expect("Node<Attached> without attachment");
834 let handle: Arc<dyn workload::LinkedWorkloadHandle> =
835 workload::WorkloadAdapter::new(observer);
836 let observer: Arc<dyn crate::lifecycle::hooks::WorkloadHookObserver> =
837 Arc::new(crate::workload::LinkedHandleObserver { handle });
838 attachment.node.hook_observer = crate::lifecycle::hooks::chain_observers(
839 attachment.node.hook_observer.take(),
840 Some(observer),
841 );
842 self
843 }
844
845 pub async fn register(self, ais_endpoint: &str) -> HyperResult<Node<Registered>> {
853 let attachment = self
854 .attachment
855 .as_ref()
856 .expect("Node<Attached> without attachment");
857 let service_spec = if let Some(verified) = attachment.verified.as_ref() {
858 crate::service_spec::calculate_service_spec_from_package(
859 &attachment.package_bytes,
860 &verified.manifest,
861 )?
862 } else {
863 None
864 };
865 self.register_with(ais_endpoint, service_spec).await
866 }
867
868 pub async fn register_with(
874 mut self,
875 ais_endpoint: &str,
876 service_spec: Option<ServiceSpec>,
877 ) -> HyperResult<Node<Registered>> {
878 let attachment = self
879 .attachment
880 .as_mut()
881 .expect("Node<Attached> without attachment");
882 let realm_id = attachment.node.config.realm.realm_id;
883 let acl = attachment.node.config.acl.clone();
884 let realm_secret = attachment.node.config.realm_secret.clone();
885
886 let register_ok = if let Some(verified) = attachment.verified.as_ref() {
887 let verified = verified.clone();
888 bootstrap_credential_inner(
889 &self.hyper,
890 &verified,
891 ais_endpoint,
892 realm_id,
893 service_spec,
894 acl,
895 realm_secret.as_deref(),
896 )
897 .await?
898 } else {
899 bootstrap_linked_credential_inner(&attachment.node.config, ais_endpoint, service_spec)
900 .await?
901 };
902
903 attachment.node.set_preregistered_credential(register_ok);
904
905 Ok(Node {
906 hyper: self.hyper,
907 attachment: self.attachment,
908 pending_runtime_config: None,
909 _state: std::marker::PhantomData,
910 })
911 }
912
913 pub fn create_network_event_handle(
916 &mut self,
917 debounce_ms: u64,
918 ) -> crate::lifecycle::NetworkEventHandle {
919 self.attachment
920 .as_mut()
921 .expect("Node<Attached> without attachment")
922 .node
923 .create_network_event_handle(debounce_ms)
924 }
925
926 pub fn ais_endpoint(&self) -> &str {
930 &self
931 .attachment
932 .as_ref()
933 .expect("Node<Attached> without attachment")
934 .node
935 .config
936 .ais_endpoint
937 }
938}
939
940#[cfg(not(target_arch = "wasm32"))]
943impl Node<Registered> {
944 pub async fn start(self) -> actr_protocol::ActorResult<crate::actr_ref::ActrRef> {
946 let Attachment { node, .. } = self
947 .attachment
948 .expect("Node<Registered> without attachment");
949 node.start().await
950 }
951
952 pub fn create_network_event_handle(
955 &mut self,
956 debounce_ms: u64,
957 ) -> crate::lifecycle::NetworkEventHandle {
958 self.attachment
959 .as_mut()
960 .expect("Node<Registered> without attachment")
961 .node
962 .create_network_event_handle(debounce_ms)
963 }
964}
965
966#[cfg(not(target_arch = "wasm32"))]
971impl Hyper {
972 pub fn resolve_storage_path(&self, manifest: &PackageManifest) -> HyperResult<PathBuf> {
976 resolve_storage_path_for(&self.inner, manifest)
977 }
978
979 pub async fn bootstrap_credential(
1001 &self,
1002 verified: &VerifiedPackage,
1003 ais_endpoint: &str,
1004 realm_id: u32,
1005 service_spec: Option<ServiceSpec>,
1006 acl: Option<Acl>,
1007 ) -> HyperResult<register_response::RegisterOk> {
1008 bootstrap_credential_inner(
1009 &self.inner,
1010 verified,
1011 ais_endpoint,
1012 realm_id,
1013 service_spec,
1014 acl,
1015 None,
1016 )
1017 .await
1018 }
1019
1020 pub fn instance_id(&self) -> &str {
1022 &self.inner.instance_id
1023 }
1024
1025 pub fn config(&self) -> &HyperConfig {
1027 &self.inner.config
1028 }
1029}
1030
1031#[cfg(not(target_arch = "wasm32"))]
1032fn resolve_storage_path_for(
1033 inner: &HyperInner,
1034 manifest: &PackageManifest,
1035) -> HyperResult<PathBuf> {
1036 let resolver = config::NamespaceResolver::new(&inner.config, &inner.instance_id)?
1037 .with_actor_type(&manifest.manufacturer, &manifest.name, &manifest.version);
1038 resolver.resolve(&inner.config.storage_path_template)
1039}
1040
1041#[cfg(not(target_arch = "wasm32"))]
1045pub(crate) async fn load_workload_package_inner(
1046 inner: &HyperInner,
1047 package: &WorkloadPackage,
1048) -> HyperResult<LoadedWorkload> {
1049 let bytes = package.bytes();
1050 let verified = inner.config.trust_provider.verify_package(bytes).await?;
1051 let binary_kind = detect_binary_kind(&verified.manifest)?;
1052 let workload = match binary_kind {
1053 BinaryKind::Wasm => load_wasm_workload_inner(inner, bytes, &verified.manifest).await?,
1054 BinaryKind::DynClib => load_dynclib_workload_inner(inner, bytes, &verified.manifest)?,
1055 };
1056 Ok(LoadedWorkload {
1057 verified,
1058 binary_kind,
1059 workload,
1060 })
1061}
1062
1063#[cfg(not(target_arch = "wasm32"))]
1064async fn load_wasm_workload_inner(
1065 _inner: &HyperInner,
1066 bytes: &[u8],
1067 manifest: &PackageManifest,
1068) -> HyperResult<crate::workload::Workload> {
1069 #[cfg(feature = "wasm-engine")]
1070 {
1071 if matches!(
1076 manifest.binary.resolved_kind(),
1077 actr_pack::BinaryKind::CoreModule
1078 ) {
1079 return Err(HyperError::InvalidManifest(format!(
1080 "package `{}` uses the legacy core wasm module format, which was retired in Phase 1. \
1081 Rebuild with actr 0.2+ (`actr build`, target wasm32-wasip2 + wasm-component-ld 0.5.22+) \
1082 to produce a Component Model binary, and set `binary.kind = \"component\"` in manifest.toml.",
1083 manifest.actr_type_str()
1084 )));
1085 }
1086
1087 let wasm_bytes = actr_pack::load_binary(bytes).map_err(|e| {
1088 HyperError::Runtime(format!(
1089 "failed to extract package binary `{}` for target `{}`: {e}",
1090 manifest.binary.path, manifest.binary.target
1091 ))
1092 })?;
1093 let host = crate::wasm::WasmHost::compile(&wasm_bytes).map_err(|e| {
1094 HyperError::Runtime(format!(
1095 "failed to compile WASM package target `{}`: {e}",
1096 manifest.binary.target
1097 ))
1098 })?;
1099 let mut instance = host.instantiate().await.map_err(|e| {
1100 HyperError::Runtime(format!(
1101 "failed to instantiate WASM package target `{}`: {e}",
1102 manifest.binary.target
1103 ))
1104 })?;
1105 instance
1106 .init(&actr_framework::guest::dynclib_abi::InitPayloadV1 {
1107 version: actr_framework::guest::dynclib_abi::version::V1,
1108 actr_type: manifest.actr_type_str(),
1109 credential: Vec::new(),
1110 actor_id: Vec::new(),
1111 realm_id: 0,
1112 })
1113 .map_err(|e| {
1114 HyperError::Runtime(format!(
1115 "failed to initialize WASM package target `{}`: {e}",
1116 manifest.binary.target
1117 ))
1118 })?;
1119 Ok(crate::workload::Workload::Wasm(instance))
1120 }
1121
1122 #[cfg(not(feature = "wasm-engine"))]
1123 {
1124 let _ = (bytes, manifest);
1125 Err(HyperError::Runtime(
1126 "package target requires the `wasm-engine` feature, but it is not enabled".to_string(),
1127 ))
1128 }
1129}
1130
1131#[cfg(not(target_arch = "wasm32"))]
1132fn load_dynclib_workload_inner(
1133 _inner: &HyperInner,
1134 bytes: &[u8],
1135 manifest: &PackageManifest,
1136) -> HyperResult<crate::workload::Workload> {
1137 #[cfg(feature = "dynclib-engine")]
1138 {
1139 let cache_path = ensure_dynclib_cache_path(&_inner.config.data_dir, bytes, manifest)?;
1140 let host = load_dynclib_host_with_rebuild(&cache_path, bytes, manifest)?;
1141 let instance = host
1142 .instantiate(&actr_framework::guest::dynclib_abi::InitPayloadV1 {
1143 version: actr_framework::guest::dynclib_abi::version::V1,
1144 actr_type: manifest.actr_type_str(),
1145 credential: Vec::new(),
1146 actor_id: Vec::new(),
1147 realm_id: 0,
1148 })
1149 .map_err(|e| {
1150 HyperError::Runtime(format!(
1151 "failed to initialize dynclib package target `{}`: {e}",
1152 manifest.binary.target
1153 ))
1154 })?;
1155
1156 Ok(crate::workload::Workload::DynClib(
1157 crate::dynclib::DynClibWorkload::new(host, instance),
1158 ))
1159 }
1160
1161 #[cfg(not(feature = "dynclib-engine"))]
1162 {
1163 let _ = (bytes, manifest);
1164 Err(HyperError::Runtime(
1165 "package target requires the `dynclib-engine` feature, but it is not enabled"
1166 .to_string(),
1167 ))
1168 }
1169}
1170
1171#[cfg(not(target_arch = "wasm32"))]
1172async fn bootstrap_credential_inner(
1173 inner: &HyperInner,
1174 verified: &VerifiedPackage,
1175 ais_endpoint: &str,
1176 realm_id: u32,
1177 service_spec: Option<ServiceSpec>,
1178 acl: Option<Acl>,
1179 realm_secret: Option<&str>,
1180) -> HyperResult<register_response::RegisterOk> {
1181 let manifest = &verified.manifest;
1182 info!(
1183 actr_type = manifest.actr_type_str(),
1184 ais_endpoint, realm_id, "starting credential bootstrap with AIS"
1185 );
1186
1187 let storage_path = resolve_storage_path_for(inner, manifest)?;
1189 let store: Arc<dyn KvStore> = if let Some(ref platform) = inner.platform {
1190 let ns = storage_path.to_string_lossy().to_string();
1191 platform
1192 .secret_store(&ns)
1193 .await
1194 .map_err(|e| HyperError::Storage(format!("failed to open secret store: {e}")))?
1195 } else {
1196 Arc::new(ActorStore::open(&storage_path).await?)
1197 };
1198
1199 let valid_psk = load_valid_psk_dyn(&*store).await?;
1201
1202 let mut ais = AisClient::new(ais_endpoint);
1204 if let Some(secret) = realm_secret {
1205 ais = ais.with_realm_secret(secret);
1206 }
1207
1208 let actr_type = ActrType {
1209 manufacturer: manifest.manufacturer.clone(),
1210 name: manifest.name.clone(),
1211 version: manifest.version.clone(),
1212 };
1213 let realm = Realm { realm_id };
1214
1215 let response = if let Some(psk_token) = valid_psk {
1216 debug!(
1218 actr_type = manifest.actr_type_str(),
1219 "renewing credential using PSK"
1220 );
1221 let req = RegisterRequest {
1222 actr_type,
1223 realm,
1224 service_spec,
1225 acl,
1226 service: None,
1227 ws_address: None,
1228 manifest_raw: None,
1229 mfr_signature: None,
1230 psk_token: Some(psk_token.into()),
1231 target: Some(manifest.binary.target.clone()),
1232 auth_mode: Some(RegisterAuthMode::Package as i32),
1233 };
1234 ais.register_with_psk(req).await?
1235 } else {
1236 info!(
1238 actr_type = manifest.actr_type_str(),
1239 "first registration: registering with AIS using MFR manifest"
1240 );
1241
1242 let req = RegisterRequest {
1243 actr_type,
1244 realm,
1245 service_spec,
1246 acl,
1247 service: None,
1248 ws_address: None,
1249 manifest_raw: Some(verified.manifest_raw.clone().into()),
1250 mfr_signature: Some(verified.sig_raw.clone().into()),
1251 psk_token: None,
1252 target: Some(manifest.binary.target.clone()),
1253 auth_mode: Some(RegisterAuthMode::Package as i32),
1254 };
1255 ais.register_with_manifest(req).await?
1256 };
1257
1258 let ok = match response.result {
1260 Some(register_response::Result::Success(ok)) => ok,
1261 Some(register_response::Result::Error(e)) => {
1262 error!(
1263 actr_type = manifest.actr_type_str(),
1264 error_code = e.code,
1265 error_message = %e.message,
1266 "AIS registration returned error"
1267 );
1268 return Err(HyperError::AisBootstrapFailed(format!(
1269 "AIS rejected registration (code={}): {}",
1270 e.code, e.message
1271 )));
1272 }
1273 None => {
1274 error!(
1275 actr_type = manifest.actr_type_str(),
1276 "AIS response missing result field"
1277 );
1278 return Err(HyperError::AisBootstrapFailed(
1279 "AIS response missing result field".to_string(),
1280 ));
1281 }
1282 };
1283
1284 if let (Some(psk), Some(psk_expires_at)) = (&ok.psk, ok.psk_expires_at) {
1286 info!(
1287 actr_type = manifest.actr_type_str(),
1288 psk_expires_at, "received PSK from AIS, storing in ActorStore"
1289 );
1290 let expires_at_bytes = (psk_expires_at as u64).to_le_bytes().to_vec();
1291 store
1292 .batch(vec![
1293 KvOp::Set {
1294 key: "hyper:psk:token".to_string(),
1295 value: psk.to_vec(),
1296 },
1297 KvOp::Set {
1298 key: "hyper:psk:expires_at".to_string(),
1299 value: expires_at_bytes,
1300 },
1301 ])
1302 .await
1303 .map_err(|e| HyperError::Storage(format!("failed to store PSK: {e}")))?;
1304 debug!(
1305 actr_type = manifest.actr_type_str(),
1306 "PSK successfully persisted to ActorStore"
1307 );
1308 }
1309
1310 let pubkey_bytes = ok.signing_pubkey.to_vec();
1312 let key_id_bytes = ok.signing_key_id.to_le_bytes().to_vec();
1313 store
1314 .batch(vec![
1315 KvOp::Set {
1316 key: "hyper:ais:signing_pubkey".to_string(),
1317 value: pubkey_bytes,
1318 },
1319 KvOp::Set {
1320 key: "hyper:ais:signing_key_id".to_string(),
1321 value: key_id_bytes,
1322 },
1323 ])
1324 .await
1325 .map_err(|e| HyperError::Storage(format!("failed to store signing key: {e}")))?;
1326 debug!(
1327 actr_type = manifest.actr_type_str(),
1328 signing_key_id = ok.signing_key_id,
1329 "AIS signing public key persisted to ActorStore"
1330 );
1331
1332 info!(
1333 actr_type = manifest.actr_type_str(),
1334 credential_len = ok.credential.encode_to_vec().len(),
1335 "AIS credential bootstrap succeeded"
1336 );
1337
1338 Ok(ok)
1339}
1340
1341#[cfg(not(target_arch = "wasm32"))]
1342async fn bootstrap_linked_credential_inner(
1343 config: &actr_config::RuntimeConfig,
1344 ais_endpoint: &str,
1345 service_spec: Option<ServiceSpec>,
1346) -> HyperResult<register_response::RegisterOk> {
1347 let mut ais = AisClient::new(ais_endpoint);
1348 if let Some(ref secret) = config.realm_secret {
1349 ais = ais.with_realm_secret(secret.clone());
1350 }
1351
1352 let req = build_linked_register_request(config, service_spec);
1353 let response = ais.register_linked(req).await?;
1354 match response.result {
1355 Some(register_response::Result::Success(ok)) => Ok(ok),
1356 Some(register_response::Result::Error(e)) => Err(HyperError::AisBootstrapFailed(format!(
1357 "AIS rejected registration (code={}): {}",
1358 e.code, e.message
1359 ))),
1360 None => Err(HyperError::AisBootstrapFailed(
1361 "AIS response missing result field".to_string(),
1362 )),
1363 }
1364}
1365
1366#[cfg(not(target_arch = "wasm32"))]
1367fn build_linked_register_request(
1368 config: &actr_config::RuntimeConfig,
1369 service_spec: Option<ServiceSpec>,
1370) -> RegisterRequest {
1371 let ws_address = if let Some(port) = config.websocket_listen_port {
1372 let host = config
1373 .websocket_advertised_host
1374 .as_deref()
1375 .unwrap_or("127.0.0.1");
1376 Some(format!("ws://{}:{}", host, port))
1377 } else {
1378 None
1379 };
1380
1381 RegisterRequest {
1382 actr_type: config.actr_type().clone(),
1383 realm: config.realm,
1384 service_spec,
1385 acl: config.acl.clone(),
1386 service: None,
1387 ws_address,
1388 auth_mode: Some(RegisterAuthMode::Linked as i32),
1389 ..Default::default()
1390 }
1391}
1392
1393#[cfg(not(target_arch = "wasm32"))]
1396async fn load_valid_psk_dyn(store: &dyn KvStore) -> HyperResult<Option<Vec<u8>>> {
1400 let token = store
1401 .get("hyper:psk:token")
1402 .await
1403 .map_err(|e| HyperError::Storage(format!("failed to read PSK token: {e}")))?;
1404 let expires_at_raw = store
1405 .get("hyper:psk:expires_at")
1406 .await
1407 .map_err(|e| HyperError::Storage(format!("failed to read PSK expires_at: {e}")))?;
1408
1409 check_psk_expiry(token, expires_at_raw)
1410}
1411
1412#[cfg(all(not(target_arch = "wasm32"), test))]
1416async fn load_valid_psk(store: &ActorStore) -> HyperResult<Option<Vec<u8>>> {
1417 let token = store.kv_get("hyper:psk:token").await?;
1418 let expires_at_raw = store.kv_get("hyper:psk:expires_at").await?;
1419
1420 check_psk_expiry(token, expires_at_raw)
1421}
1422
1423#[cfg(not(target_arch = "wasm32"))]
1424fn check_psk_expiry(
1426 token: Option<Vec<u8>>,
1427 expires_at_raw: Option<Vec<u8>>,
1428) -> HyperResult<Option<Vec<u8>>> {
1429 match (token, expires_at_raw) {
1430 (Some(token), Some(expires_bytes)) => {
1431 if expires_bytes.len() != 8 {
1433 warn!("PSK expires_at has unexpected format, falling back to first registration");
1434 return Ok(None);
1435 }
1436 let expires_at = u64::from_le_bytes(expires_bytes.as_slice().try_into().unwrap());
1437
1438 let now_secs = SystemTime::now()
1440 .duration_since(UNIX_EPOCH)
1441 .unwrap_or_default()
1442 .as_secs();
1443
1444 if now_secs >= expires_at {
1445 warn!(
1446 psk_expires_at = expires_at,
1447 now = now_secs,
1448 "PSK expired, falling back to first registration"
1449 );
1450 Ok(None)
1451 } else {
1452 debug!(
1453 psk_expires_at = expires_at,
1454 now = now_secs,
1455 remaining_secs = expires_at - now_secs,
1456 "PSK valid, using PSK renewal path"
1457 );
1458 Ok(Some(token))
1459 }
1460 }
1461 _ => {
1462 debug!("no PSK in ActorStore, proceeding with first registration");
1463 Ok(None)
1464 }
1465 }
1466}
1467
1468#[cfg(not(target_arch = "wasm32"))]
1469#[cfg(not(target_arch = "wasm32"))]
1470fn detect_binary_kind(manifest: &PackageManifest) -> HyperResult<BinaryKind> {
1471 if manifest.binary.is_wasm_target() {
1472 return Ok(BinaryKind::Wasm);
1473 }
1474
1475 if is_compatible_native_target(&manifest.binary.target) {
1476 return Ok(BinaryKind::DynClib);
1477 }
1478
1479 Err(HyperError::InvalidManifest(format!(
1480 "unsupported binary target `{}` for host `{}-{}`; expected `wasm32-*` or a native target matching this host",
1481 manifest.binary.target,
1482 std::env::consts::ARCH,
1483 std::env::consts::OS,
1484 )))
1485}
1486
1487#[cfg(not(target_arch = "wasm32"))]
1493fn is_compatible_native_target(target: &str) -> bool {
1494 let segments: Vec<&str> = target.split('-').filter(|s| !s.is_empty()).collect();
1495 if segments.len() < 3 {
1496 return false;
1497 }
1498
1499 let target_arch = segments[0];
1500 let target_os = segments[2];
1502
1503 let arch_matches = match (target_arch, std::env::consts::ARCH) {
1505 (a, b) if a == b => true,
1506 ("x86_64", "x86_64") => true,
1507 ("aarch64", "aarch64") => true,
1508 _ => false,
1509 };
1510
1511 let os_matches = match (target_os, std::env::consts::OS) {
1513 (a, b) if a == b => true,
1514 ("darwin", "macos") | ("macos", "darwin") => true,
1515 _ => false,
1516 };
1517
1518 arch_matches && os_matches
1519}
1520
1521#[cfg(all(
1522 not(target_arch = "wasm32"),
1523 feature = "dynclib-engine",
1524 target_os = "macos"
1525))]
1526fn dynclib_tempfile_suffix() -> &'static str {
1527 ".dylib"
1528}
1529
1530#[cfg(all(
1531 not(target_arch = "wasm32"),
1532 feature = "dynclib-engine",
1533 target_os = "linux"
1534))]
1535fn dynclib_tempfile_suffix() -> &'static str {
1536 ".so"
1537}
1538
1539#[cfg(all(
1540 not(target_arch = "wasm32"),
1541 feature = "dynclib-engine",
1542 target_os = "windows"
1543))]
1544fn dynclib_tempfile_suffix() -> &'static str {
1545 ".dll"
1546}
1547
1548#[cfg(all(
1549 not(target_arch = "wasm32"),
1550 feature = "dynclib-engine",
1551 not(any(target_os = "macos", target_os = "linux", target_os = "windows"))
1552))]
1553fn dynclib_tempfile_suffix() -> &'static str {
1554 ".dynlib"
1555}
1556
1557#[cfg(all(not(target_arch = "wasm32"), feature = "dynclib-engine"))]
1558const DYNCLIB_CACHE_DIR: &str = "dynclib-cache";
1559
1560#[cfg(all(not(target_arch = "wasm32"), feature = "dynclib-engine"))]
1561fn dynclib_cache_dir(data_dir: &Path) -> PathBuf {
1562 data_dir.join(DYNCLIB_CACHE_DIR)
1563}
1564
1565#[cfg(all(not(target_arch = "wasm32"), feature = "dynclib-engine"))]
1566fn dynclib_cache_path(data_dir: &Path, binary_hash: &[u8; 32]) -> PathBuf {
1567 dynclib_cache_dir(data_dir).join(format!(
1568 "{}{}",
1569 hex::encode(binary_hash),
1570 dynclib_tempfile_suffix()
1571 ))
1572}
1573
1574#[cfg(all(not(target_arch = "wasm32"), feature = "dynclib-engine"))]
1575fn extract_dynclib_binary(bytes: &[u8], manifest: &PackageManifest) -> HyperResult<Vec<u8>> {
1576 actr_pack::load_binary(bytes).map_err(|e| {
1577 HyperError::Runtime(format!(
1578 "failed to extract package binary `{}` for target `{}`: {e}",
1579 manifest.binary.path, manifest.binary.target
1580 ))
1581 })
1582}
1583
1584#[cfg(all(not(target_arch = "wasm32"), feature = "dynclib-engine"))]
1585fn write_dynclib_cache_file(cache_path: &Path, binary_bytes: &[u8]) -> HyperResult<()> {
1586 let cache_dir = cache_path.parent().ok_or_else(|| {
1587 HyperError::Runtime("dynclib cache path has no parent directory".to_string())
1588 })?;
1589 std::fs::create_dir_all(cache_dir).map_err(|e| {
1590 HyperError::Runtime(format!(
1591 "failed to create dynclib cache directory `{}`: {e}",
1592 cache_dir.display()
1593 ))
1594 })?;
1595
1596 let mut temp_file = tempfile::Builder::new()
1597 .prefix("actr-dynclib-")
1598 .tempfile_in(cache_dir)
1599 .map_err(|e| {
1600 HyperError::Runtime(format!(
1601 "failed to allocate dynclib cache temp file in `{}`: {e}",
1602 cache_dir.display()
1603 ))
1604 })?;
1605
1606 temp_file.write_all(binary_bytes).map_err(|e| {
1607 HyperError::Runtime(format!(
1608 "failed to write dynclib cache temp file `{}`: {e}",
1609 temp_file.path().display()
1610 ))
1611 })?;
1612 temp_file.flush().map_err(|e| {
1613 HyperError::Runtime(format!(
1614 "failed to flush dynclib cache temp file `{}`: {e}",
1615 temp_file.path().display()
1616 ))
1617 })?;
1618
1619 match temp_file.persist_noclobber(cache_path) {
1620 Ok(_) => Ok(()),
1621 Err(err) if err.error.kind() == std::io::ErrorKind::AlreadyExists => Ok(()),
1622 Err(err) => Err(HyperError::Runtime(format!(
1623 "failed to persist dynclib cache file `{}`: {}",
1624 cache_path.display(),
1625 err.error
1626 ))),
1627 }
1628}
1629
1630#[cfg(all(not(target_arch = "wasm32"), feature = "dynclib-engine"))]
1631fn ensure_dynclib_cache_path(
1632 data_dir: &Path,
1633 bytes: &[u8],
1634 manifest: &PackageManifest,
1635) -> HyperResult<PathBuf> {
1636 let binary_hash = manifest
1637 .binary
1638 .hash_bytes()
1639 .map_err(|e| HyperError::InvalidManifest(e.to_string()))?;
1640 let cache_path = dynclib_cache_path(data_dir, &binary_hash);
1641 if cache_path.exists() {
1642 return Ok(cache_path);
1643 }
1644
1645 let binary_bytes = extract_dynclib_binary(bytes, manifest)?;
1646 write_dynclib_cache_file(&cache_path, &binary_bytes)?;
1647 Ok(cache_path)
1648}
1649
1650#[cfg(all(not(target_arch = "wasm32"), feature = "dynclib-engine"))]
1651fn rebuild_dynclib_cache_file(
1652 cache_path: &Path,
1653 bytes: &[u8],
1654 manifest: &PackageManifest,
1655) -> HyperResult<()> {
1656 match std::fs::remove_file(cache_path) {
1657 Ok(()) => {}
1658 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
1659 Err(err) => {
1660 return Err(HyperError::Runtime(format!(
1661 "failed to remove corrupt dynclib cache file `{}`: {err}",
1662 cache_path.display()
1663 )));
1664 }
1665 }
1666
1667 let binary_bytes = extract_dynclib_binary(bytes, manifest)?;
1668 write_dynclib_cache_file(cache_path, &binary_bytes)
1669}
1670
1671#[cfg(all(not(target_arch = "wasm32"), feature = "dynclib-engine"))]
1672fn load_dynclib_host_with_rebuild(
1673 cache_path: &Path,
1674 bytes: &[u8],
1675 manifest: &PackageManifest,
1676) -> HyperResult<crate::dynclib::DynclibHost> {
1677 match crate::dynclib::DynclibHost::load(cache_path) {
1678 Ok(host) => Ok(host),
1679 Err(first_err) => {
1680 warn!(
1681 path = %cache_path.display(),
1682 target = %manifest.binary.target,
1683 error = %first_err,
1684 "cached dynclib load failed, rebuilding cache once"
1685 );
1686 rebuild_dynclib_cache_file(cache_path, bytes, manifest)?;
1687 crate::dynclib::DynclibHost::load(cache_path).map_err(|second_err| {
1688 HyperError::Runtime(format!(
1689 "failed to load dynclib package target `{}` from cache `{}` after rebuild; first load error: {first_err}; second load error: {second_err}",
1690 manifest.binary.target,
1691 cache_path.display()
1692 ))
1693 })
1694 }
1695 }
1696}
1697
1698#[cfg(not(target_arch = "wasm32"))]
1699async fn load_or_create_instance_uid_local(data_dir: &std::path::Path) -> HyperResult<String> {
1704 let id_file = data_dir.join(".hyper-instance-uid");
1705
1706 if id_file.exists() {
1707 let id = tokio::fs::read_to_string(&id_file)
1708 .await
1709 .map_err(|e| HyperError::Storage(format!("failed to read instance_uid file: {e}")))?;
1710 let id = id.trim().to_string();
1711 if !id.is_empty() {
1712 return Ok(id);
1713 }
1714 warn!("instance_uid file is empty; generating a new one");
1715 }
1716
1717 let new_id = Uuid::new_v4().to_string();
1718 tokio::fs::write(&id_file, &new_id)
1719 .await
1720 .map_err(|e| HyperError::Storage(format!("failed to write instance_uid file: {e}")))?;
1721 info!(instance_uid = %new_id, "generated a new Hyper instance_uid");
1722 Ok(new_id)
1723}
1724
1725#[cfg(all(not(target_arch = "wasm32"), test))]
1726mod tests {
1727 use super::*;
1728 use ed25519_dalek::SigningKey;
1729 use rand::rngs::OsRng;
1730 #[cfg(feature = "dynclib-engine")]
1731 use std::sync::{Arc, Barrier};
1732 use tempfile::TempDir;
1733
1734 fn dev_config(dir: &TempDir) -> HyperConfig {
1735 let signing_key = SigningKey::generate(&mut OsRng);
1736 let pubkey = signing_key.verifying_key().to_bytes();
1737 HyperConfig::new(
1738 dir.path(),
1739 Arc::new(crate::verify::StaticTrust::new(pubkey).unwrap()),
1740 )
1741 }
1742
1743 #[tokio::test]
1744 async fn init_creates_data_dir_and_instance_id() {
1745 let dir = TempDir::new().unwrap();
1746 let sub = dir.path().join("subdir/nested");
1747 let signing_key = SigningKey::generate(&mut OsRng);
1748 let config = HyperConfig::new(
1749 &sub,
1750 Arc::new(
1751 crate::verify::StaticTrust::new(signing_key.verifying_key().to_bytes()).unwrap(),
1752 ),
1753 );
1754
1755 let hyper = Hyper::new(config).await.unwrap();
1756 assert!(sub.exists());
1757 assert!(!hyper.instance_id().is_empty());
1758 }
1759
1760 #[tokio::test]
1761 async fn instance_id_is_stable_across_reinit() {
1762 let dir = TempDir::new().unwrap();
1763 let config1 = dev_config(&dir);
1764 let hyper1 = Hyper::new(config1).await.unwrap();
1765 let id1 = hyper1.instance_id().to_string();
1766
1767 let config2 = dev_config(&dir);
1768 let hyper2 = Hyper::new(config2).await.unwrap();
1769 let id2 = hyper2.instance_id().to_string();
1770
1771 assert_eq!(id1, id2, "instance_id should remain stable across restarts");
1772 }
1773
1774 #[tokio::test]
1775 async fn verify_package_rejects_non_wasm() {
1776 let dir = TempDir::new().unwrap();
1777 let hyper = Hyper::new(dev_config(&dir)).await.unwrap();
1778 let result = hyper
1779 .verify_package(&WorkloadPackage::new(b"not a wasm file".to_vec()))
1780 .await;
1781 assert!(matches!(result, Err(HyperError::InvalidManifest(_))));
1782 }
1783
1784 #[tokio::test]
1785 async fn verify_package_rejects_non_actr_format() {
1786 let dir = TempDir::new().unwrap();
1787 let hyper = Hyper::new(dev_config(&dir)).await.unwrap();
1788
1789 let result = hyper
1791 .verify_package(&WorkloadPackage::new(b"\0asm\x01\x00\x00\x00".to_vec()))
1792 .await;
1793 assert!(matches!(result, Err(HyperError::InvalidManifest(_))));
1794 }
1795
1796 async fn open_test_store(dir: &TempDir) -> ActorStore {
1799 let db_path = dir.path().join("test.db");
1800 ActorStore::open(&db_path).await.unwrap()
1801 }
1802
1803 #[tokio::test]
1805 async fn psk_valid_returns_token() {
1806 let dir = TempDir::new().unwrap();
1807 let store = open_test_store(&dir).await;
1808
1809 let psk_token = b"test-psk-secret".to_vec();
1810 let expires_at = SystemTime::now()
1812 .duration_since(UNIX_EPOCH)
1813 .unwrap()
1814 .as_secs()
1815 + 3600;
1816
1817 store.kv_set("hyper:psk:token", &psk_token).await.unwrap();
1818 store
1819 .kv_set("hyper:psk:expires_at", &expires_at.to_le_bytes())
1820 .await
1821 .unwrap();
1822
1823 let result = load_valid_psk(&store).await.unwrap();
1824 assert_eq!(result, Some(psk_token), "A valid PSK should be returned");
1825 }
1826
1827 #[tokio::test]
1829 async fn psk_expired_returns_none() {
1830 let dir = TempDir::new().unwrap();
1831 let store = open_test_store(&dir).await;
1832
1833 let psk_token = b"expired-psk".to_vec();
1834 let expires_at = SystemTime::now()
1836 .duration_since(UNIX_EPOCH)
1837 .unwrap()
1838 .as_secs()
1839 .saturating_sub(1);
1840
1841 store.kv_set("hyper:psk:token", &psk_token).await.unwrap();
1842 store
1843 .kv_set("hyper:psk:expires_at", &expires_at.to_le_bytes())
1844 .await
1845 .unwrap();
1846
1847 let result = load_valid_psk(&store).await.unwrap();
1848 assert_eq!(result, None, "An expired PSK should return None");
1849 }
1850
1851 #[tokio::test]
1853 async fn psk_absent_returns_none() {
1854 let dir = TempDir::new().unwrap();
1855 let store = open_test_store(&dir).await;
1856
1857 let result = load_valid_psk(&store).await.unwrap();
1858 assert_eq!(result, None, "Missing PSK should return None");
1859 }
1860
1861 #[tokio::test]
1863 async fn psk_missing_expires_at_returns_none() {
1864 let dir = TempDir::new().unwrap();
1865 let store = open_test_store(&dir).await;
1866
1867 store
1868 .kv_set("hyper:psk:token", b"orphan-token")
1869 .await
1870 .unwrap();
1871 let result = load_valid_psk(&store).await.unwrap();
1874 assert_eq!(result, None, "Missing expires_at should return None");
1875 }
1876
1877 fn fake_manifest() -> VerifiedPackage {
1885 VerifiedPackage {
1886 manifest: actr_pack::PackageManifest {
1887 manufacturer: "test-mfr".to_string(),
1888 name: "TestActor".to_string(),
1889 version: "0.1.0".to_string(),
1890 binary: actr_pack::BinaryEntry {
1891 path: "bin/actor.wasm".to_string(),
1892 target: "wasm32-wasip1".to_string(),
1893 hash: "0".repeat(64),
1894 size: None,
1895 kind: None,
1896 },
1897 signature_algorithm: "ed25519".to_string(),
1898 signing_key_id: None,
1899 resources: vec![],
1900 proto_files: vec![],
1901 lock_file: None,
1902 metadata: actr_pack::ManifestMetadata::default(),
1903 },
1904 manifest_raw: vec![],
1905 sig_raw: vec![0u8; 64],
1906 }
1907 }
1908
1909 fn fake_register_response_bytes(with_psk: bool) -> Vec<u8> {
1911 use actr_protocol::{
1912 AIdCredential, ActrId, ActrType, IdentityClaims, Realm, RegisterResponse,
1913 TurnCredential, register_response,
1914 };
1915
1916 let claims = IdentityClaims {
1917 realm_id: 1,
1918 actor_id: "test-actor-id".to_string(),
1919 expires_at: u64::MAX,
1920 };
1921 let claims_bytes = claims.encode_to_vec();
1922
1923 let credential = AIdCredential {
1924 key_id: 1,
1925 claims: claims_bytes.into(),
1926 signature: vec![0u8; 64].into(),
1927 };
1928
1929 let actr_id = ActrId {
1930 realm: Realm { realm_id: 1 },
1931 serial_number: 42,
1932 r#type: ActrType {
1933 manufacturer: "test-mfr".to_string(),
1934 name: "TestActor".to_string(),
1935 version: "0.1.0".to_string(),
1936 },
1937 };
1938
1939 let turn = TurnCredential {
1940 username: "user".to_string(),
1941 password: "pass".to_string(),
1942 expires_at: u64::MAX,
1943 };
1944
1945 let mut ok = register_response::RegisterOk {
1946 actr_id,
1947 credential,
1948 turn_credential: turn,
1949 credential_expires_at: None,
1950 signaling_heartbeat_interval_secs: 30,
1951 signing_pubkey: vec![0u8; 32].into(),
1952 signing_key_id: 1,
1953 psk: None,
1954 psk_expires_at: None,
1955 };
1956
1957 if with_psk {
1958 ok.psk = Some(b"fresh-psk-from-ais".to_vec().into());
1959 ok.psk_expires_at = Some(
1960 (SystemTime::now()
1961 .duration_since(UNIX_EPOCH)
1962 .unwrap()
1963 .as_secs()
1964 + 86400) as i64,
1965 );
1966 }
1967
1968 RegisterResponse {
1969 result: Some(register_response::Result::Success(ok)),
1970 }
1971 .encode_to_vec()
1972 }
1973
1974 fn test_service_spec() -> Option<ServiceSpec> {
1975 Some(ServiceSpec {
1976 name: "EchoService".to_string(),
1977 description: Some("test service".to_string()),
1978 fingerprint: "fp-123".to_string(),
1979 protobufs: vec![],
1980 published_at: None,
1981 tags: vec!["latest".to_string()],
1982 })
1983 }
1984
1985 fn test_acl() -> Option<Acl> {
1986 Some(Acl { rules: vec![] })
1987 }
1988
1989 fn linked_runtime_config(dir: &TempDir) -> actr_config::RuntimeConfig {
1990 actr_config::RuntimeConfig {
1991 package: actr_config::PackageInfo {
1992 name: "LinkedActor".to_string(),
1993 actr_type: actr_protocol::ActrType {
1994 manufacturer: "test-mfr".to_string(),
1995 name: "LinkedActor".to_string(),
1996 version: "0.1.0".to_string(),
1997 },
1998 description: None,
1999 authors: vec![],
2000 license: None,
2001 },
2002 signaling_url: url::Url::parse("ws://localhost:8081/signaling/ws").unwrap(),
2003 realm: Realm { realm_id: 7 },
2004 ais_endpoint: "http://localhost:8081/ais".to_string(),
2005 realm_secret: Some("test-realm-secret".to_string()),
2006 visible_in_discovery: true,
2007 acl: test_acl(),
2008 mailbox_path: None,
2009 scripts: std::collections::HashMap::new(),
2010 webrtc: actr_config::WebRtcConfig::default(),
2011 websocket_listen_port: Some(9100),
2012 websocket_advertised_host: Some("127.0.0.1".to_string()),
2013 observability: actr_config::ObservabilityConfig {
2014 filter_level: "info".to_string(),
2015 tracing_enabled: false,
2016 tracing_endpoint: "http://localhost:4317".to_string(),
2017 tracing_service_name: "linked-test".to_string(),
2018 },
2019 config_dir: dir.path().to_path_buf(),
2020 trust: vec![],
2021 package_path: None,
2022 web: None,
2023 }
2024 }
2025
2026 #[test]
2027 fn linked_register_request_uses_linked_auth_mode() {
2028 let dir = TempDir::new().unwrap();
2029 let req = build_linked_register_request(&linked_runtime_config(&dir), test_service_spec());
2030
2031 assert_eq!(req.auth_mode, Some(RegisterAuthMode::Linked as i32));
2032 assert_eq!(req.manifest_raw, None);
2033 assert_eq!(req.mfr_signature, None);
2034 assert_eq!(req.psk_token, None);
2035 assert_eq!(req.ws_address.as_deref(), Some("ws://127.0.0.1:9100"));
2036 }
2037
2038 #[tokio::test]
2039 async fn with_actor_type_overrides_pending_runtime_metadata() {
2040 let dir = TempDir::new().unwrap();
2041 let hyper = Hyper::new(dev_config(&dir)).await.unwrap();
2042 let node = Node::from_hyper(hyper, linked_runtime_config(&dir)).with_actor_type(
2043 actr_protocol::ActrType {
2044 manufacturer: "acme".into(),
2045 name: "UnifiedActor".into(),
2046 version: "1.0.0".into(),
2047 },
2048 );
2049
2050 let actr_type = node.runtime_config().actr_type();
2051 assert_eq!(actr_type.manufacturer, "acme");
2052 assert_eq!(actr_type.name, "UnifiedActor");
2053 assert_eq!(actr_type.version, "1.0.0");
2054 }
2055
2056 #[test]
2057 fn compatible_native_target_matches_current_host() {
2058 let current = format!(
2060 "{}-unknown-{}",
2061 std::env::consts::ARCH,
2062 if std::env::consts::OS == "macos" {
2063 "darwin"
2064 } else {
2065 std::env::consts::OS
2066 }
2067 );
2068 assert!(
2069 is_compatible_native_target(¤t),
2070 "current host target `{current}` should be compatible"
2071 );
2072 }
2073
2074 #[test]
2075 fn compatible_native_target_rejects_cross_platform() {
2076 assert!(!is_compatible_native_target("riscv64gc-unknown-linux-gnu"));
2078 assert!(!is_compatible_native_target("s390x-unknown-linux-gnu"));
2079 }
2080
2081 #[test]
2082 fn compatible_native_target_rejects_short_triples() {
2083 assert!(!is_compatible_native_target("invalid-target"));
2084 assert!(!is_compatible_native_target("single"));
2085 assert!(!is_compatible_native_target(""));
2086 }
2087
2088 #[cfg(feature = "dynclib-engine")]
2089 fn fake_dynclib_manifest() -> PackageManifest {
2090 let target = format!(
2091 "{}-unknown-{}",
2092 std::env::consts::ARCH,
2093 if std::env::consts::OS == "macos" {
2094 "darwin"
2095 } else {
2096 std::env::consts::OS
2097 }
2098 );
2099 PackageManifest {
2100 manufacturer: "test-mfr".to_string(),
2101 name: "DynActor".to_string(),
2102 version: "1.0.0".to_string(),
2103 binary: actr_pack::BinaryEntry {
2104 path: format!("bin/actor{}", dynclib_tempfile_suffix()),
2105 target,
2106 hash: String::new(),
2107 size: None,
2108 kind: None,
2109 },
2110 signature_algorithm: "ed25519".to_string(),
2111 signing_key_id: None,
2112 resources: vec![],
2113 proto_files: vec![],
2114 lock_file: None,
2115 metadata: actr_pack::ManifestMetadata::default(),
2116 }
2117 }
2118
2119 #[cfg(feature = "dynclib-engine")]
2120 fn fake_dynclib_package_bytes(binary_bytes: &[u8]) -> (Vec<u8>, PackageManifest) {
2121 let manifest = fake_dynclib_manifest();
2122 let signing_key = SigningKey::generate(&mut OsRng);
2123 let package_bytes = actr_pack::pack(&actr_pack::PackOptions {
2124 manifest: manifest.clone(),
2125 binary_bytes: binary_bytes.to_vec(),
2126 resources: vec![],
2127 proto_files: vec![],
2128 lock_file: None,
2129 signing_key,
2130 })
2131 .unwrap();
2132 let packed_manifest = actr_pack::read_manifest(&package_bytes).unwrap();
2135 (package_bytes, packed_manifest)
2136 }
2137
2138 #[cfg(feature = "dynclib-engine")]
2139 #[test]
2140 fn dynclib_cache_path_uses_hash_and_platform_suffix() {
2141 let dir = TempDir::new().unwrap();
2142 let path = dynclib_cache_path(dir.path(), &[0xAB; 32]);
2143
2144 assert_eq!(path.parent().unwrap(), dynclib_cache_dir(dir.path()));
2145 assert_eq!(
2146 path.file_name().unwrap().to_string_lossy(),
2147 format!("{}{}", hex::encode([0xAB; 32]), dynclib_tempfile_suffix())
2148 );
2149 }
2150
2151 #[cfg(feature = "dynclib-engine")]
2152 #[test]
2153 fn ensure_dynclib_cache_path_preserves_existing_file() {
2154 let dir = TempDir::new().unwrap();
2155 let initial_binary_bytes = b"initial dylib bytes";
2156 let (initial_package_bytes, manifest) = fake_dynclib_package_bytes(initial_binary_bytes);
2157 let cache_path =
2158 ensure_dynclib_cache_path(dir.path(), &initial_package_bytes, &manifest).unwrap();
2159
2160 let second_path =
2164 ensure_dynclib_cache_path(dir.path(), &initial_package_bytes, &manifest).unwrap();
2165
2166 assert_eq!(cache_path, second_path);
2167 assert_eq!(std::fs::read(&cache_path).unwrap(), initial_binary_bytes);
2168 }
2169
2170 #[cfg(feature = "dynclib-engine")]
2171 #[test]
2172 fn ensure_dynclib_cache_path_handles_concurrent_creation() {
2173 let dir = TempDir::new().unwrap();
2174 let binary_bytes = b"shared dylib bytes".to_vec();
2175 let (package_bytes, manifest) = fake_dynclib_package_bytes(&binary_bytes);
2176 let package_bytes = Arc::new(package_bytes);
2177 let binary_bytes = Arc::new(binary_bytes);
2178 let data_dir = Arc::new(dir.path().to_path_buf());
2179 let barrier = Arc::new(Barrier::new(3));
2180
2181 let handles: Vec<_> = (0..2)
2182 .map(|_| {
2183 let barrier = Arc::clone(&barrier);
2184 let data_dir = Arc::clone(&data_dir);
2185 let manifest = manifest.clone();
2186 let package_bytes = Arc::clone(&package_bytes);
2187 std::thread::spawn(move || {
2188 barrier.wait();
2189 ensure_dynclib_cache_path(&data_dir, &package_bytes, &manifest)
2190 })
2191 })
2192 .collect();
2193
2194 barrier.wait();
2195
2196 let results: Vec<_> = handles
2197 .into_iter()
2198 .map(|handle| handle.join().unwrap().unwrap())
2199 .collect();
2200
2201 assert_eq!(results[0], results[1]);
2202 assert_eq!(
2203 std::fs::read(&results[0]).unwrap(),
2204 binary_bytes.as_ref().as_slice()
2205 );
2206 }
2207
2208 #[tokio::test]
2210 async fn bootstrap_first_registration_stores_psk() {
2211 let response_body = fake_register_response_bytes(true);
2212
2213 let mut server = mockito::Server::new_async().await;
2214 let mock = server
2215 .mock("POST", "/register")
2216 .with_status(200)
2217 .with_header("content-type", "application/x-protobuf")
2218 .with_body(response_body)
2219 .create_async()
2220 .await;
2221
2222 let dir = TempDir::new().unwrap();
2223 let config = dev_config(&dir);
2224 let hyper = Hyper::new(config).await.unwrap();
2225
2226 let manifest = fake_manifest();
2227 let result = hyper
2228 .bootstrap_credential(&manifest, &server.url(), 1, test_service_spec(), test_acl())
2229 .await;
2230
2231 mock.assert_async().await;
2232 assert!(
2233 result.is_ok(),
2234 "Initial registration should succeed, got: {:?}",
2235 result.err()
2236 );
2237
2238 let storage_path = hyper.resolve_storage_path(&manifest.manifest).unwrap();
2240 let store = ActorStore::open(&storage_path).await.unwrap();
2241 let psk = store.kv_get("hyper:psk:token").await.unwrap();
2242 assert!(
2243 psk.is_some(),
2244 "PSK should be stored in ActorStore after initial registration"
2245 );
2246 assert_eq!(psk.unwrap(), b"fresh-psk-from-ais".to_vec());
2247 }
2248
2249 #[tokio::test]
2251 async fn bootstrap_psk_renewal_skips_manifest() {
2252 let response_body = fake_register_response_bytes(false);
2253
2254 let mut server = mockito::Server::new_async().await;
2255 let mock = server
2256 .mock("POST", "/register")
2257 .with_status(200)
2258 .with_header("content-type", "application/x-protobuf")
2259 .with_body(response_body)
2260 .expect(1) .create_async()
2262 .await;
2263
2264 let dir = TempDir::new().unwrap();
2265 let config = dev_config(&dir);
2266 let hyper = Hyper::new(config).await.unwrap();
2267
2268 let manifest = fake_manifest();
2270 let storage_path = hyper.resolve_storage_path(&manifest.manifest).unwrap();
2271 let store = ActorStore::open(&storage_path).await.unwrap();
2272
2273 let expires_at = SystemTime::now()
2274 .duration_since(UNIX_EPOCH)
2275 .unwrap()
2276 .as_secs()
2277 + 3600;
2278 store
2279 .kv_set("hyper:psk:token", b"existing-valid-psk")
2280 .await
2281 .unwrap();
2282 store
2283 .kv_set("hyper:psk:expires_at", &expires_at.to_le_bytes())
2284 .await
2285 .unwrap();
2286
2287 let result = hyper
2288 .bootstrap_credential(&manifest, &server.url(), 1, test_service_spec(), test_acl())
2289 .await;
2290
2291 mock.assert_async().await;
2292 assert!(
2293 result.is_ok(),
2294 "PSK renewal should succeed, got: {:?}",
2295 result.err()
2296 );
2297 }
2298
2299 #[tokio::test]
2301 async fn bootstrap_expired_psk_falls_back_to_manifest() {
2302 let response_body = fake_register_response_bytes(true);
2303
2304 let mut server = mockito::Server::new_async().await;
2305 let mock = server
2306 .mock("POST", "/register")
2307 .with_status(200)
2308 .with_header("content-type", "application/x-protobuf")
2309 .with_body(response_body)
2310 .expect(1)
2311 .create_async()
2312 .await;
2313
2314 let dir = TempDir::new().unwrap();
2315 let config = dev_config(&dir);
2316 let hyper = Hyper::new(config).await.unwrap();
2317
2318 let manifest = fake_manifest();
2320 let storage_path = hyper.resolve_storage_path(&manifest.manifest).unwrap();
2321 let store = ActorStore::open(&storage_path).await.unwrap();
2322
2323 let expired_at = SystemTime::now()
2324 .duration_since(UNIX_EPOCH)
2325 .unwrap()
2326 .as_secs()
2327 .saturating_sub(10); store
2329 .kv_set("hyper:psk:token", b"expired-psk")
2330 .await
2331 .unwrap();
2332 store
2333 .kv_set("hyper:psk:expires_at", &expired_at.to_le_bytes())
2334 .await
2335 .unwrap();
2336
2337 let result = hyper
2338 .bootstrap_credential(&manifest, &server.url(), 1, test_service_spec(), test_acl())
2339 .await;
2340
2341 mock.assert_async().await;
2342 assert!(
2343 result.is_ok(),
2344 "Manifest registration should succeed after PSK expiration, got: {:?}",
2345 result.err()
2346 );
2347 }
2348
2349 #[tokio::test]
2351 async fn bootstrap_ais_error_propagates() {
2352 use actr_protocol::{ErrorResponse, RegisterResponse, register_response};
2353
2354 let error_resp = RegisterResponse {
2355 result: Some(register_response::Result::Error(ErrorResponse {
2356 code: 403,
2357 message: "manufacturer not trusted".to_string(),
2358 })),
2359 }
2360 .encode_to_vec();
2361
2362 let mut server = mockito::Server::new_async().await;
2363 let _mock = server
2364 .mock("POST", "/register")
2365 .with_status(200)
2366 .with_header("content-type", "application/x-protobuf")
2367 .with_body(error_resp)
2368 .create_async()
2369 .await;
2370
2371 let dir = TempDir::new().unwrap();
2372 let config = dev_config(&dir);
2373 let hyper = Hyper::new(config).await.unwrap();
2374
2375 let manifest = fake_manifest();
2376 let result = hyper
2377 .bootstrap_credential(&manifest, &server.url(), 1, test_service_spec(), test_acl())
2378 .await;
2379
2380 assert!(
2381 matches!(result, Err(HyperError::AisBootstrapFailed(_))),
2382 "AIS errors should propagate as AisBootstrapFailed, got: {:?}",
2383 result
2384 );
2385 }
2386}