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