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