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