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