Skip to main content

hara_native/runtime/
session.rs

1/// A process-local kernel that multiplexes isolated evaluator sessions.
2///
3/// Raw HTA exposes the same lifecycle over its wire targets; this native
4/// facade keeps embedding hosts from treating a `Runtime` as the process
5/// boundary when several independent sessions can share one kernel.
6pub struct SessionKernel {
7    session_registry: SessionRegistry,
8    development_resources: DevelopmentResourceCatalog,
9    bundle_catalog: BundleCatalog,
10    mount_registry: MountRegistry,
11    sandbox_provider_registry: SandboxProviderRegistry,
12    sandbox_registry: SandboxRegistry,
13    runtime_factory: Rc<dyn Fn() -> Runtime>,
14    test_runner: String,
15    execution_backend: String,
16    #[cfg(not(target_arch = "wasm32"))]
17    source_catalog: Option<crate::project::SourceCatalog>,
18    #[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
19    native_source_cache: Option<SourceBytecodeCache>,
20}
21
22#[derive(Default)]
23struct SessionRegistry {
24    entries: HashMap<String, Session>,
25}
26
27#[derive(Default)]
28struct DevelopmentResourceCatalog {
29    entries: HashMap<String, String>,
30}
31
32#[derive(Default)]
33struct BundleCatalog {
34    entries: HashMap<String, Vec<u8>>,
35}
36
37#[derive(Default)]
38struct MountRegistry {
39    entries: HashMap<u64, FilesystemMount>,
40    session_attachments: HashMap<String, u64>,
41    sandbox_attachments: HashMap<u64, u64>,
42    next_id: u64,
43}
44
45#[derive(Default)]
46struct SandboxProviderRegistry {
47    entries: HashMap<String, Rc<dyn SandboxProvider>>,
48}
49
50#[derive(Default)]
51struct SandboxRegistry {
52    entries: HashMap<u64, Sandbox>,
53    next_id: u64,
54}
55
56/// An isolated, named execution context owned by a [`SessionKernel`].
57pub struct Session {
58    spec: SessionSpec,
59    runtime: Option<Runtime>,
60    state: SessionState,
61    filesystem: Option<AttachedFilesystem>,
62    authority: SessionAuthorityPolicy,
63    last_namespace: String,
64    live_sessions: SessionLiveRegistry,
65}
66
67struct AttachedFilesystem {
68    id: SessionMountId,
69    _provider: Rc<dyn core::FileProvider>,
70}
71
72impl Session {
73    #[cfg(test)]
74    fn new(name: &str, runtime: Runtime) -> Self {
75        let spec = SessionSpec::zero_authority(name)
76            .expect("Session::new requires a validated session name");
77        Self::open(spec, runtime)
78    }
79
80    fn open(spec: SessionSpec, runtime: Runtime) -> Self {
81        let authority = spec.authority;
82        let mut session = Self {
83            spec,
84            runtime: Some(runtime),
85            state: SessionState::New,
86            filesystem: None,
87            authority,
88            last_namespace: "user".into(),
89            live_sessions: SessionLiveRegistry::default(),
90        };
91        session.activate();
92        session
93    }
94
95    pub fn spec(&self) -> &SessionSpec {
96        &self.spec
97    }
98
99    pub fn id(&self) -> &SessionId {
100        &self.spec.id
101    }
102
103    pub fn name(&self) -> &str {
104        self.id().as_str()
105    }
106
107    pub fn state(&self) -> SessionState {
108        self.state
109    }
110
111    pub fn filesystem_mount(&self) -> Option<SessionMountId> {
112        self.filesystem.as_ref().map(|filesystem| filesystem.id)
113    }
114
115    #[cfg(test)]
116    pub(crate) fn module_revision(&self, name: &str) -> Result<u64, String> {
117        Ok(self.runtime()?.namespace_registry.module_revision(name))
118    }
119
120    fn ensure_active(&self) -> Result<(), String> {
121        match self.state {
122            SessionState::Active => Ok(()),
123            SessionState::Closed => Err(format!("SESSION_CLOSED {}", self.name())),
124            SessionState::New => Err(format!("SESSION_NOT_ACTIVE {} new", self.name())),
125        }
126    }
127
128    pub(crate) fn runtime(&self) -> Result<&Runtime, String> {
129        let name = self.spec.id.to_string();
130        self.runtime
131            .as_ref()
132            .ok_or_else(|| format!("SESSION_CLOSED {name}"))
133    }
134
135    pub(crate) fn runtime_mut(&mut self) -> Result<&mut Runtime, String> {
136        let name = self.spec.id.to_string();
137        self.runtime
138            .as_mut()
139            .ok_or_else(|| format!("SESSION_CLOSED {name}"))
140    }
141
142    fn activate(&mut self) {
143        assert_eq!(
144            self.state,
145            SessionState::New,
146            "session must start exactly once"
147        );
148        self.state = SessionState::Active;
149    }
150
151    fn release(&mut self) -> Option<SessionMountId> {
152        if self.state == SessionState::Closed {
153            return None;
154        }
155        self.live_sessions.dispose_all();
156        self.last_namespace = self
157            .runtime
158            .as_ref()
159            .map(Runtime::current_namespace)
160            .unwrap_or_else(|| self.last_namespace.clone());
161        if let Some(runtime) = self.runtime.as_mut() {
162            runtime.providers.set_file(None);
163        }
164        let mount = self.filesystem.take().map(|filesystem| filesystem.id);
165        self.runtime.take();
166        self.authority = SessionAuthorityPolicy::ZERO;
167        self.state = SessionState::Closed;
168        mount
169    }
170
171    pub fn eval(&mut self, source: &str) -> Result<String, String> {
172        self.ensure_active()?;
173        self.runtime_mut()?.eval_transfer_text(source)
174    }
175
176    pub fn current_namespace(&self) -> String {
177        self.runtime
178            .as_ref()
179            .map(Runtime::current_namespace)
180            .unwrap_or_else(|| self.last_namespace.clone())
181    }
182
183    #[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
184    pub fn native_execution_telemetry(
185        &self,
186    ) -> Result<crate::direct_native::NativeExecutionTelemetry, String> {
187        self.ensure_active()?;
188        Ok(self.runtime()?.native_execution_telemetry())
189    }
190
191    pub fn authority(&self) -> SessionAuthorityPolicy {
192        self.authority
193    }
194
195    #[cfg(not(target_arch = "wasm32"))]
196    pub fn install_native_socket_provider(&mut self) {
197        self.runtime_mut()
198            .expect("closed sessions cannot install providers")
199            .install_native_socket_provider();
200        self.authority.host_network = true;
201    }
202
203    #[cfg(not(target_arch = "wasm32"))]
204    pub fn install_native_process_provider(&mut self) {
205        self.runtime_mut()
206            .expect("closed sessions cannot install providers")
207            .install_native_process_provider();
208        self.authority.host_process = true;
209    }
210}
211
212impl crate::lang::protocol::IContext<&str> for Session {
213    type Output = Result<String, String>;
214
215    fn call(&mut self, source: &str) -> Self::Output {
216        self.eval(source)
217    }
218
219}
220
221impl crate::lang::protocol::IComponent for Session {
222    type Metadata = SessionMetadata;
223
224    fn props(&self) -> Self::Metadata {
225        SessionStatus {
226            name: self.id().clone(),
227            namespace: self.current_namespace(),
228            state: self.state,
229            filesystem: self.filesystem_mount(),
230            authority: self.authority,
231        }
232    }
233
234    fn status(&self) -> Self::Metadata {
235        self.props()
236    }
237
238    fn started(&self) -> bool {
239        self.state == SessionState::Active
240    }
241
242    fn stopped(&self) -> bool {
243        self.state == SessionState::Closed
244    }
245
246    fn start(&mut self) {
247        self.activate();
248    }
249
250    fn stop(&mut self) {
251        self.release();
252    }
253}
254
255impl<'a> crate::lang::protocol::IApplicable<Session, &'a str> for Session {
256    type Output = Result<String, String>;
257
258    fn apply_in(&self, runtime: &mut Session, source: &'a str) -> Self::Output {
259        self.ensure_active()?;
260        crate::lang::protocol::IContext::call(runtime, source)
261    }
262
263    fn apply_default(&mut self) -> &mut Session {
264        self
265    }
266
267    fn transform_in(&self, _runtime: &Session, source: &'a str) -> &'a str {
268        source
269    }
270
271    fn transform_out(
272        &self,
273        _runtime: &Session,
274        _source: &'a str,
275        value: Self::Output,
276    ) -> Self::Output {
277        value
278    }
279}
280
281impl<'a> crate::lang::protocol::IInvokeIn<Session, &'a str> for Session {
282    type Output = Result<String, String>;
283
284    fn invoke_in(&self, context: &mut Session, source: &'a str) -> Self::Output {
285        self.ensure_active()?;
286        crate::lang::protocol::IContext::call(context, source)
287    }
288}
289
290struct FilesystemMount {
291    provider: Rc<dyn core::FileProvider>,
292    kind: &'static str,
293    key: String,
294    attachments: usize,
295}
296
297impl Default for SessionKernel {
298    fn default() -> Self {
299        Self::new()
300    }
301}
302
303impl SessionKernel {
304    pub fn new() -> Self {
305        Self::with_runtime_factory(Runtime::new(), Rc::new(Runtime::new))
306    }
307
308    #[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
309    /// Creates an isolated-session kernel with a shared direct-native program
310    /// cache. The cache is useful for short-lived session owners such as the
311    /// Hara test runner; no namespace or mutable value state is shared.
312    pub fn new_with_native_engine(engine: crate::direct_native::NativeEngine) -> Self {
313        let factory_engine = engine.clone();
314        Self::with_runtime_factory(
315            Runtime::with_native_engine(engine),
316            Rc::new(move || Runtime::with_native_engine(factory_engine.clone())),
317        )
318    }
319
320    pub(crate) fn with_runtime_factory(
321        root_runtime: Runtime,
322        runtime_factory: Rc<dyn Fn() -> Runtime>,
323    ) -> Self {
324        let root_id = SessionId::parse("ROOT").expect("ROOT is a valid session identifier");
325        let execution_backend = root_runtime.execution_backend.clone();
326        Self {
327            session_registry: SessionRegistry {
328                entries: HashMap::from([(
329                    root_id.to_string(),
330                    Session::open(
331                        SessionSpec::new(root_id, SessionAuthorityPolicy::ZERO),
332                        root_runtime,
333                    ),
334                )]),
335            },
336            development_resources: DevelopmentResourceCatalog::default(),
337            bundle_catalog: BundleCatalog::default(),
338            mount_registry: MountRegistry {
339                next_id: 1,
340                ..MountRegistry::default()
341            },
342            sandbox_provider_registry: SandboxProviderRegistry::default(),
343            sandbox_registry: SandboxRegistry {
344                next_id: 1,
345                ..SandboxRegistry::default()
346            },
347            runtime_factory,
348            test_runner: "code.test".into(),
349            execution_backend,
350            #[cfg(not(target_arch = "wasm32"))]
351            source_catalog: None,
352            #[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
353            native_source_cache: None,
354        }
355    }
356
357    pub fn set_test_runner(&mut self, runner: &str) -> Result<(), String> {
358        validate_test_runner(runner)?;
359        self.test_runner = runner.into();
360        for session in self.session_registry.entries.values_mut() {
361            session.runtime_mut()?.configure_test_runner(runner)?;
362        }
363        Ok(())
364    }
365
366    /// Selects the ordinary evaluation backend for every existing and future
367    /// session owned by this kernel.
368    pub fn set_execution_backend(&mut self, backend: &str) -> Result<(), String> {
369        validate_execution_backend(backend)?;
370        for session in self.session_registry.entries.values_mut() {
371            session.runtime_mut()?.configure_execution_backend(backend)?;
372        }
373        self.execution_backend = backend.into();
374        Ok(())
375    }
376
377    /// Mounts a lazy native source catalog in every current session and
378    /// carries it into sessions created later by this kernel.
379    #[cfg(not(target_arch = "wasm32"))]
380    pub fn register_source_catalog(&mut self, catalog: &crate::project::SourceCatalog) {
381        self.source_catalog = Some(catalog.clone());
382        for session in self.session_registry.entries.values_mut() {
383            session
384                .runtime_mut()
385                .expect("kernel cannot retain a closed session")
386                .register_source_catalog(catalog);
387        }
388    }
389
390    /// Enables the project-local direct-native source-program cache for every
391    /// current session and for sessions created later by this kernel.
392    #[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
393    pub fn configure_native_source_cache(
394        &mut self,
395        root: &std::path::Path,
396        source_index_fingerprint: [u8; 32],
397    ) {
398        let cache = SourceBytecodeCache::new(root, source_index_fingerprint);
399        self.native_source_cache = Some(cache.clone());
400        for session in self.session_registry.entries.values_mut() {
401            session
402                .runtime_mut()
403                .expect("kernel cannot retain a closed session")
404                .set_direct_native_source_cache(cache.clone());
405        }
406    }
407
408    /// Installs a verified HBX namespace bundle in every current session.
409    /// Bundle loading is transactional per session and remains explicit so a
410    /// kernel cannot accidentally make an application package available to a
411    /// later session without the host opting into it.
412    #[cfg(feature = "bytecode-vm")]
413    pub fn install_bytecode_bundle(&mut self, bytes: &[u8]) -> Result<(), String> {
414        for session in self.session_registry.entries.values_mut() {
415            crate::vm::eval_bytecode_bundle(session.runtime_mut()?, bytes)?;
416        }
417        Ok(())
418    }
419
420    pub fn create_session(&mut self, id: SessionId) -> Result<(), String> {
421        let spec = SessionSpec::new(id, SessionAuthorityPolicy::ZERO);
422        if self.session_registry.entries.contains_key(spec.id.as_str()) {
423            return Err(format!("SESSION_EXISTS {}", spec.id));
424        }
425        let mut runtime = (self.runtime_factory)();
426        runtime.configure_test_runner(&self.test_runner)?;
427        runtime.configure_execution_backend(&self.execution_backend)?;
428        #[cfg(not(target_arch = "wasm32"))]
429        if let Some(source_catalog) = &self.source_catalog {
430            runtime.register_source_catalog(source_catalog);
431        }
432        #[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
433        if let Some(source_cache) = &self.native_source_cache {
434            runtime.set_direct_native_source_cache(source_cache.clone());
435        }
436        for (resource, source) in &self.development_resources.entries {
437            runtime.register_resource(resource, source);
438        }
439        self.session_registry
440            .entries
441            .insert(spec.id.as_str().into(), Session::open(spec, runtime));
442        Ok(())
443    }
444
445    pub fn session_names(&self) -> Vec<SessionId> {
446        let mut names = self
447            .session_registry
448            .entries
449            .values()
450            .map(|session| session.id().clone())
451            .collect::<Vec<_>>();
452        names.sort();
453        names
454    }
455
456    pub fn session(&self, id: &SessionId) -> Result<&Session, String> {
457        self.session_registry
458            .entries
459            .get(id.as_str())
460            .ok_or_else(|| format!("NO_SESSION {id}"))
461    }
462
463    pub fn session_mut(&mut self, id: &SessionId) -> Result<&mut Session, String> {
464        self.session_registry
465            .entries
466            .get_mut(id.as_str())
467            .ok_or_else(|| format!("NO_SESSION {id}"))
468    }
469
470    pub fn session_namespace(&self, id: &SessionId) -> Result<String, String> {
471        self.session_registry
472            .entries
473            .get(id.as_str())
474            .map(Session::current_namespace)
475            .ok_or_else(|| format!("NO_SESSION {id}"))
476    }
477
478    #[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
479    pub fn native_execution_telemetry(
480        &self,
481        id: &SessionId,
482    ) -> Result<crate::direct_native::NativeExecutionTelemetry, String> {
483        self.session_registry
484            .entries
485            .get(id.as_str())
486            .ok_or_else(|| format!("NO_SESSION {id}"))?
487            .native_execution_telemetry()
488    }
489
490    pub fn eval(&mut self, id: &SessionId, source: &str) -> Result<String, String> {
491        self.session_registry
492            .entries
493            .get_mut(id.as_str())
494            .ok_or_else(|| format!("NO_SESSION {id}"))?
495            .eval(source)
496    }
497
498    pub fn register_resource(&mut self, name: &str, source: &str) {
499        self.development_resources
500            .entries
501            .insert(name.into(), source.into());
502        for session in self.session_registry.entries.values_mut() {
503            session
504                .runtime_mut()
505                .expect("kernel cannot retain a closed session")
506                .register_resource(name, source);
507        }
508    }
509
510    pub fn remove_resource(&mut self, name: &str) -> bool {
511        self.development_resources.entries.remove(name).is_some()
512    }
513
514    pub fn resource_names(&self) -> Vec<String> {
515        let mut names = self
516            .development_resources
517            .entries
518            .keys()
519            .cloned()
520            .collect::<Vec<_>>();
521        names.sort();
522        names
523    }
524
525    pub fn register_bundle(&mut self, digest: &str, bytes: &[u8]) -> Result<(), String> {
526        match self.bundle_catalog.entries.get(digest) {
527            Some(current) if current == bytes => Ok(()),
528            Some(_) => Err(format!("BUNDLE_DIGEST_CONFLICT {digest}")),
529            None => {
530                self.bundle_catalog
531                    .entries
532                    .insert(digest.into(), bytes.into());
533                Ok(())
534            }
535        }
536    }
537
538    pub fn bundle(&self, digest: &str) -> Option<&[u8]> {
539        self.bundle_catalog.entries.get(digest).map(Vec::as_slice)
540    }
541
542    pub fn create_memory_filesystem(&mut self, root: &str) -> SessionMountId {
543        self.create_filesystem(Rc::new(core::MemoryFileProvider::new(root)), "memory", root)
544    }
545
546    #[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
547    pub fn create_native_filesystem(&mut self, root: &str) -> SessionMountId {
548        self.create_filesystem(Rc::new(core::NativeFileProvider::new(root)), "native", root)
549    }
550
551    fn create_filesystem(
552        &mut self,
553        provider: Rc<dyn core::FileProvider>,
554        kind: &'static str,
555        key: &str,
556    ) -> SessionMountId {
557        let id = self.mount_registry.next_id;
558        self.mount_registry.next_id = self
559            .mount_registry
560            .next_id
561            .checked_add(1)
562            .expect("filesystem mount identifiers exhausted");
563        self.mount_registry.entries.insert(
564            id,
565            FilesystemMount {
566                provider,
567                kind,
568                key: key.into(),
569                attachments: 0,
570            },
571        );
572        SessionMountId::new(id)
573    }
574
575    pub fn attach_filesystem(
576        &mut self,
577        session: &SessionId,
578        mount_id: SessionMountId,
579    ) -> Result<(), String> {
580        if !self.session_registry.entries.contains_key(session.as_str()) {
581            return Err(format!("NO_SESSION {session}"));
582        }
583        let provider = self
584            .mount_registry
585            .entries
586            .get(&mount_id.get())
587            .ok_or_else(|| format!("NO_FILESYSTEM {mount_id}"))?
588            .provider
589            .clone();
590        if self
591            .mount_registry
592            .session_attachments
593            .get(session.as_str())
594            == Some(&mount_id.get())
595        {
596            return Ok(());
597        }
598        self.detach_filesystem(session)?;
599        self.mount_registry
600            .entries
601            .get_mut(&mount_id.get())
602            .unwrap()
603            .attachments += 1;
604        self.mount_registry
605            .session_attachments
606            .insert(session.to_string(), mount_id.get());
607        let session = self
608            .session_registry
609            .entries
610            .get_mut(session.as_str())
611            .unwrap();
612        session
613            .runtime_mut()?
614            .providers
615            .set_file(Some(provider.clone()));
616        session.filesystem = Some(AttachedFilesystem {
617            id: mount_id,
618            _provider: provider,
619        });
620        Ok(())
621    }
622
623    pub fn detach_filesystem(&mut self, session: &SessionId) -> Result<(), String> {
624        let session = self
625            .session_registry
626            .entries
627            .get_mut(session.as_str())
628            .ok_or_else(|| format!("NO_SESSION {session}"))?;
629        session.runtime_mut()?.providers.set_file(None);
630        session.filesystem.take();
631        if let Some(mount_id) = self
632            .mount_registry
633            .session_attachments
634            .remove(session.id().as_str())
635        {
636            if let Some(mount) = self.mount_registry.entries.get_mut(&mount_id) {
637                mount.attachments = mount.attachments.saturating_sub(1);
638            }
639        }
640        Ok(())
641    }
642
643    pub fn filesystem(&self, session: &SessionId) -> Option<SessionMountId> {
644        self.session_registry
645            .entries
646            .get(session.as_str())
647            .and_then(Session::filesystem_mount)
648    }
649
650    pub fn filesystem_info(&self, mount_id: SessionMountId) -> Result<(&str, &str, usize), String> {
651        self.mount_registry
652            .entries
653            .get(&mount_id.get())
654            .map(|mount| (mount.kind, mount.key.as_str(), mount.attachments))
655            .ok_or_else(|| format!("NO_FILESYSTEM {mount_id}"))
656    }
657
658    pub fn close_filesystem(&mut self, mount_id: SessionMountId) -> Result<(), String> {
659        let mount = self
660            .mount_registry
661            .entries
662            .get(&mount_id.get())
663            .ok_or_else(|| format!("NO_FILESYSTEM {mount_id}"))?;
664        if mount.attachments != 0 {
665            return Err(format!("FILESYSTEM_ATTACHED {mount_id}"));
666        }
667        self.mount_registry.entries.remove(&mount_id.get());
668        Ok(())
669    }
670
671    pub fn close_session(&mut self, id: &SessionId) -> Result<(), String> {
672        if id.as_str() == "ROOT" {
673            return Err("ROOT_CANNOT_CLOSE".into());
674        }
675        if !self.session_registry.entries.contains_key(id.as_str()) {
676            return Err(format!("NO_SESSION {id}"));
677        }
678        self.detach_filesystem(id)?;
679        if let Some(mut session) = self.session_registry.entries.remove(id.as_str()) {
680            crate::lang::protocol::IComponent::stop(&mut session);
681        }
682        Ok(())
683    }
684}
685
686fn validate_test_runner(runner: &str) -> Result<(), String> {
687    if matches!(runner, "code.test" | "native") {
688        Ok(())
689    } else {
690        Err("runtime test runner must be code.test or native".into())
691    }
692}
693
694/// The root Foundation surface deliberately contains only the iterator core.
695/// Native iterator mechanics must enter through the `Iter/*` type alias, so
696/// reject legacy unqualified call heads before namespace rewriting canonicalizes
697/// an alias to its backing method name.
698fn reject_legacy_iterator_calls(form: &Form) -> Result<(), String> {
699    const LEGACY: &[&str] = &[
700        "iter-has?",
701        "iter-finite?",
702        "iter-materialize",
703        "iter-close",
704        "iter-map",
705        "iter-filter",
706        "iter-take-while",
707        "iter-drop-while",
708        "iter-mapcat",
709        "iter-keep",
710        "iter-interpose",
711        "iter-interleave",
712        "iter-every?",
713        "iter-any?",
714        "iter-take",
715        "iter-drop",
716        "iter-zip",
717        "iter-cycle",
718        "iter-partition-pair",
719        "iter-partition-all",
720        "iter-partition",
721        "iter-range",
722        "iter-constantly",
723        "iter-repeatedly",
724        "iter-iterate",
725    ];
726    match form {
727        Form::List(values) => {
728            if let Some(Form::Symbol(name)) = values.first() {
729                if LEGACY.contains(&name.as_str()) {
730                    return Err(format!("unbound symbol: {name}"));
731                }
732                if name == "quote" {
733                    return Ok(());
734                }
735            }
736            for value in values {
737                reject_legacy_iterator_calls(value)?;
738            }
739        }
740        Form::Vector(values) | Form::Set(values) => {
741            for value in values {
742                reject_legacy_iterator_calls(value)?;
743            }
744        }
745        Form::Map(entries) => {
746            for (key, value) in entries {
747                reject_legacy_iterator_calls(key)?;
748                reject_legacy_iterator_calls(value)?;
749            }
750        }
751        Form::Tagged(_, value) | Form::Metadata(_, value) => reject_legacy_iterator_calls(value)?,
752        _ => {}
753    }
754    Ok(())
755}
756
757#[cfg(test)]
758mod authority_tests {
759    use super::*;
760
761    fn session_id(name: &str) -> SessionId {
762        SessionId::parse(name).unwrap()
763    }
764
765    #[test]
766    fn named_sessions_start_with_zero_host_authority() {
767        let mut kernel = SessionKernel::new();
768        let root = session_id("ROOT");
769        #[cfg(not(target_arch = "wasm32"))]
770        {
771            kernel
772                .session_mut(&root)
773                .unwrap()
774                .install_native_socket_provider();
775            kernel
776                .session_mut(&root)
777                .unwrap()
778                .install_native_process_provider();
779            assert_eq!(
780                kernel.session(&root).unwrap().authority().profile(),
781                "explicit"
782            );
783        }
784
785        let child_id = session_id("child");
786        kernel.create_session(child_id.clone()).unwrap();
787        let child = kernel.session(&child_id).unwrap();
788        assert_eq!(child.authority(), SessionAuthorityPolicy::ZERO);
789        assert_eq!(
790            crate::lang::protocol::IComponent::props(child)
791                .authority
792                .profile(),
793            "zero"
794        );
795
796        for capability in ["filesystem", "network/socket", "process"] {
797            let error = kernel
798                .eval(
799                    &child_id,
800                    &format!(
801                        "(std.protocol.ideref.IDeref/deref (std.native.Host/capability? \"{capability}\"))"
802                    ),
803                )
804                .unwrap_err();
805            assert!(error.contains("std.native.Host/capability? requires capability :host-call"));
806            assert!(error.contains(":native/capability-denied"));
807        }
808    }
809
810    #[test]
811    fn session_status_uses_typed_identity_state_and_mount() {
812        use crate::lang::protocol::IComponent;
813
814        let mut kernel = SessionKernel::new();
815        let typed = session_id("typed");
816        kernel.create_session(typed.clone()).unwrap();
817        let initial = kernel.session(&typed).unwrap().props();
818        assert_eq!(initial.name.as_str(), "typed");
819        assert_eq!(initial.state, SessionState::Active);
820        assert_eq!(initial.filesystem, None);
821
822        let mount = kernel.create_memory_filesystem("/");
823        kernel.attach_filesystem(&typed, mount).unwrap();
824        let mounted = kernel.session(&typed).unwrap().props();
825        assert_eq!(mounted.filesystem, Some(mount));
826        assert_eq!(kernel.session(&typed).unwrap().spec().id, mounted.name);
827    }
828
829    #[test]
830    fn scoped_filesystem_mount_does_not_change_host_authority_profile() {
831        let mut kernel = SessionKernel::new();
832        let mounted = session_id("mounted");
833        kernel.create_session(mounted.clone()).unwrap();
834        let mount = kernel.create_memory_filesystem("/");
835        kernel.attach_filesystem(&mounted, mount).unwrap();
836        assert_eq!(
837            kernel.session(&mounted).unwrap().authority(),
838            SessionAuthorityPolicy::ZERO
839        );
840        assert_eq!(kernel.filesystem(&mounted), Some(mount));
841    }
842
843    #[test]
844    fn closing_releases_session_owned_runtime_and_filesystem_once() {
845        use crate::lang::protocol::IComponent;
846
847        let mut kernel = SessionKernel::new();
848        let child = session_id("owned");
849        kernel.create_session(child.clone()).unwrap();
850        let mount = kernel.create_memory_filesystem("/");
851        assert_eq!(
852            Rc::strong_count(&kernel.mount_registry.entries[&mount.get()].provider),
853            1
854        );
855
856        kernel.attach_filesystem(&child, mount).unwrap();
857        assert_eq!(
858            Rc::strong_count(&kernel.mount_registry.entries[&mount.get()].provider),
859            3
860        );
861
862        let mut session = kernel
863            .session_registry
864            .entries
865            .remove(child.as_str())
866            .unwrap();
867        let released_mount = session.release();
868        assert_eq!(released_mount, Some(mount));
869        assert_eq!(session.state(), SessionState::Closed);
870        assert!(session.runtime.is_none());
871        assert!(session.filesystem.is_none());
872        assert_eq!(
873            Rc::strong_count(&kernel.mount_registry.entries[&mount.get()].provider),
874            1
875        );
876
877        assert_eq!(session.release(), None);
878        session.stop();
879        assert_eq!(
880            Rc::strong_count(&kernel.mount_registry.entries[&mount.get()].provider),
881            1
882        );
883    }
884
885    #[test]
886    fn development_resources_and_sealed_bundles_use_distinct_catalogs() {
887        let mut kernel = SessionKernel::new();
888        kernel.register_resource("demo/value.hal", "(ns demo.value) (def value 42)");
889        assert_eq!(kernel.resource_names(), vec!["demo/value.hal"]);
890
891        kernel.register_bundle("sha256:demo", b"sealed").unwrap();
892        kernel.register_bundle("sha256:demo", b"sealed").unwrap();
893        assert_eq!(kernel.bundle("sha256:demo"), Some(b"sealed".as_slice()));
894        assert_eq!(
895            kernel
896                .register_bundle("sha256:demo", b"replacement")
897                .unwrap_err(),
898            "BUNDLE_DIGEST_CONFLICT sha256:demo"
899        );
900
901        assert!(kernel.remove_resource("demo/value.hal"));
902        assert!(kernel.resource_names().is_empty());
903        assert_eq!(kernel.bundle("sha256:demo"), Some(b"sealed".as_slice()));
904    }
905}