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