Skip to main content

hara_native/
native_cli.rs

1#![cfg(not(target_arch = "wasm32"))]
2
3use std::cell::RefCell;
4use std::path::{Path, PathBuf};
5use std::rc::Rc;
6use std::sync::{mpsc, Arc};
7
8use crate::core::{map_entries, ExceptionInfo, Promise, TraceFrame, Value};
9use crate::invoke_hta::InvokeHtaError;
10use crate::lang::data::Symbol;
11use crate::lang::protocol::INamespaced;
12use crate::{
13    EvaluationId, InProcessSandboxProvider, Runtime, SandboxId, SandboxSpec, SandboxStatus,
14    SessionId, SessionKernel,
15};
16
17mod arguments;
18mod documentation;
19mod kernel;
20use arguments::{
21    keyword, optional_string as optional_string_argument, string as string_argument,
22    strings as strings_argument, strings_value, tap_value,
23};
24pub use documentation::{Documentation, DocumentationValue};
25use kernel::kernel_call;
26
27// Optimized brokers stay within the production 8 MiB ceiling. Debug evaluator
28// frames are much larger and need the same development allowance as the CLI
29// and portable test runner while loading the full language library.
30const RUNTIME_BROKER_STACK_SIZE: usize = if cfg!(debug_assertions) {
31    64 * 1024 * 1024
32} else {
33    8 * 1024 * 1024
34};
35const MAX_DIAGNOSTIC_DATA_BYTES: usize = 16 * 1024;
36
37#[derive(Clone, Copy)]
38enum RuntimeBootstrap {
39    Full,
40    Core,
41    Source,
42}
43
44enum Request {
45    Eval {
46        session: String,
47        source: String,
48        reply: mpsc::Sender<Result<String, String>>,
49    },
50    EvalDiagnostic {
51        session: String,
52        source: String,
53        reply: mpsc::Sender<Result<String, RuntimeDiagnostic>>,
54    },
55    Namespace {
56        session: String,
57        reply: mpsc::Sender<Result<String, String>>,
58    },
59    Complete {
60        session: String,
61        prefix: String,
62        reply: mpsc::Sender<Result<Vec<String>, String>>,
63    },
64    Doc {
65        session: String,
66        symbol: String,
67        reply: mpsc::Sender<Result<Documentation, String>>,
68    },
69    Create {
70        session: String,
71        reply: mpsc::Sender<Result<String, String>>,
72    },
73    Close {
74        session: String,
75        reply: mpsc::Sender<Result<String, String>>,
76    },
77    List {
78        reply: mpsc::Sender<Result<Vec<String>, String>>,
79    },
80    Info {
81        session: String,
82        reply: mpsc::Sender<Result<String, String>>,
83    },
84    RegisterResource {
85        name: String,
86        source: String,
87        reply: mpsc::Sender<Result<(), String>>,
88    },
89    RemoveResource {
90        name: String,
91        reply: mpsc::Sender<Result<(), String>>,
92    },
93    ListResources {
94        reply: mpsc::Sender<Result<Vec<String>, String>>,
95    },
96    InstallModule {
97        session: String,
98        manifest: String,
99        module: crate::wasmtime_provider::CompiledWasmModule,
100        reply: mpsc::Sender<Result<String, String>>,
101    },
102    InvokeModule {
103        session: String,
104        namespace: String,
105        export: String,
106        arguments: Vec<u8>,
107        reply: mpsc::Sender<Result<Vec<u8>, String>>,
108    },
109    InvokeHta {
110        session: String,
111        qualified_var: String,
112        arguments: Vec<u8>,
113        reply: mpsc::Sender<Result<Vec<u8>, InvokeHtaError>>,
114    },
115    SandboxOpen {
116        spec: SandboxSpec,
117        reply: mpsc::Sender<Result<SandboxId, String>>,
118    },
119    SandboxEval {
120        sandbox: SandboxId,
121        source: String,
122        started: mpsc::Sender<Result<EvaluationId, String>>,
123        reply: mpsc::Sender<Result<String, String>>,
124    },
125    SandboxCall {
126        sandbox: SandboxId,
127        callable: String,
128        arguments: Vec<u8>,
129        started: mpsc::Sender<Result<EvaluationId, String>>,
130        reply: mpsc::Sender<Result<Vec<u8>, String>>,
131    },
132    SandboxCancel {
133        sandbox: SandboxId,
134        evaluation: Option<EvaluationId>,
135        reply: mpsc::Sender<Result<bool, String>>,
136    },
137    SandboxStatus {
138        sandbox: SandboxId,
139        reply: mpsc::Sender<Result<SandboxStatus, String>>,
140    },
141    SandboxClose {
142        sandbox: SandboxId,
143        reply: mpsc::Sender<Result<(), String>>,
144    },
145    Shutdown,
146}
147
148struct BrokerHandle {
149    sender: mpsc::Sender<Request>,
150}
151
152impl Drop for BrokerHandle {
153    fn drop(&mut self) {
154        let _ = self.sender.send(Request::Shutdown);
155    }
156}
157
158#[derive(Clone)]
159pub struct RuntimeBroker {
160    handle: Arc<BrokerHandle>,
161    root: Option<PathBuf>,
162}
163
164/// Structured state retained at an embedding boundary when an evaluation
165/// fails.  It deliberately complements `RuntimeBroker::eval` rather than
166/// changing that established string-error API.
167#[derive(Clone, Debug)]
168pub(crate) struct RuntimeDiagnostic {
169    pub message: String,
170    pub exception: Option<RuntimeException>,
171    pub frames: Vec<TraceFrame>,
172}
173
174/// Send-safe exception information retained for diagnostics. Runtime Values
175/// use single-threaded reference types, so this snapshot is made on the broker
176/// thread before the reply crosses its channel boundary.
177#[derive(Clone, Debug)]
178pub(crate) struct RuntimeException {
179    pub message: String,
180    pub class: Option<String>,
181    pub code: Option<String>,
182    pub data: String,
183    pub cause: Option<Box<RuntimeException>>,
184    pub throws: Vec<crate::core::ExceptionSite>,
185}
186
187impl RuntimeDiagnostic {
188    fn message(message: String) -> Self {
189        Self {
190            message,
191            exception: None,
192            frames: Vec::new(),
193        }
194    }
195}
196
197fn exception_attribute(exception: &ExceptionInfo, name: &str) -> Option<Value> {
198    let key = Value::Keyword(name.into());
199    map_entries(exception.data.as_ref())?
200        .into_iter()
201        .find_map(|(candidate, value)| (candidate == key).then_some(value))
202}
203
204fn bounded_diagnostic_data(value: String) -> String {
205    if value.len() <= MAX_DIAGNOSTIC_DATA_BYTES {
206        return value;
207    }
208    let mut end = MAX_DIAGNOSTIC_DATA_BYTES.saturating_sub(3);
209    while end > 0 && !value.is_char_boundary(end) {
210        end -= 1;
211    }
212    format!("{}...", &value[..end])
213}
214
215fn exception_snapshot(exception: &ExceptionInfo) -> RuntimeException {
216    let provenance = exception.provenance.borrow();
217    RuntimeException {
218        message: exception.message.clone(),
219        class: exception_attribute(exception, "ex/class").map(|value| value.display()),
220        code: exception_attribute(exception, "ex/code").map(|value| value.display()),
221        data: bounded_diagnostic_data(exception.data.display()),
222        cause: exception.cause.as_deref().and_then(|value| match value {
223            Value::ExceptionInfo(cause) => Some(Box::new(exception_snapshot(cause))),
224            _ => None,
225        }),
226        throws: provenance.throws.clone(),
227    }
228}
229
230fn captured_exception(value: Option<Value>) -> Option<RuntimeException> {
231    match value {
232        Some(Value::ExceptionInfo(exception)) => Some(exception_snapshot(&exception)),
233        _ => None,
234    }
235}
236
237impl RuntimeBroker {
238    pub fn start() -> Result<Self, String> {
239        Self::start_with_bootstrap(None, false, false, false, RuntimeBootstrap::Full)
240    }
241
242    /// Starts an isolated broker with the portable core-language runtime.
243    ///
244    /// This is intended for small embedding surfaces and focused tests
245    /// that do not require the language-level Foundation bundle.
246    pub fn start_core() -> Result<Self, String> {
247        Self::start_with_bootstrap(None, false, false, false, RuntimeBootstrap::Core)
248    }
249
250    pub fn start_with(
251        root: Option<PathBuf>,
252        native_sockets: bool,
253        allow_process: bool,
254        allow_postgres: bool,
255    ) -> Result<Self, String> {
256        Self::start_with_bootstrap(
257            root,
258            native_sockets,
259            allow_process,
260            allow_postgres,
261            RuntimeBootstrap::Full,
262        )
263    }
264
265    /// Starts a full broker with the requested ordinary evaluation backend.
266    /// Library callers retain the interpreter-default `start_with` entrypoint;
267    /// command-line frontends use this method to make native execution explicit
268    /// while preserving an interpreter escape hatch.
269    pub fn start_with_backend(
270        root: Option<PathBuf>,
271        native_sockets: bool,
272        allow_process: bool,
273        allow_postgres: bool,
274        execution_backend: &str,
275    ) -> Result<Self, String> {
276        Self::start_with_bootstrap_and_backend(
277            root,
278            native_sockets,
279            allow_process,
280            allow_postgres,
281            RuntimeBootstrap::Full,
282            execution_backend,
283        )
284    }
285
286    /// Starts a full Foundation-backed broker with project namespaces loaded
287    /// lazily from a native source catalog.
288    pub fn start_with_backend_and_source_catalog(
289        root: Option<PathBuf>,
290        native_sockets: bool,
291        allow_process: bool,
292        allow_postgres: bool,
293        execution_backend: &str,
294        source_catalog: crate::project::SourceCatalog,
295    ) -> Result<Self, String> {
296        Self::start_with_bootstrap_and_backend_and_catalog(
297            root,
298            native_sockets,
299            allow_process,
300            allow_postgres,
301            RuntimeBootstrap::Full,
302            execution_backend,
303            Some(source_catalog),
304        )
305    }
306
307    /// Starts a broker whose language libraries are resolved from a native
308    /// project source catalog. Foundation is bootstrapped from source before
309    /// the requested ordinary backend is enabled.
310    pub fn start_with_source_catalog(
311        root: Option<PathBuf>,
312        native_sockets: bool,
313        allow_process: bool,
314        allow_postgres: bool,
315        execution_backend: &str,
316        source_catalog: crate::project::SourceCatalog,
317    ) -> Result<Self, String> {
318        Self::start_with_bootstrap_and_backend_and_catalog(
319            root,
320            native_sockets,
321            allow_process,
322            allow_postgres,
323            RuntimeBootstrap::Source,
324            execution_backend,
325            Some(source_catalog),
326        )
327    }
328
329    fn start_with_bootstrap(
330        root: Option<PathBuf>,
331        native_sockets: bool,
332        allow_process: bool,
333        allow_postgres: bool,
334        bootstrap: RuntimeBootstrap,
335    ) -> Result<Self, String> {
336        Self::start_with_bootstrap_and_backend(
337            root,
338            native_sockets,
339            allow_process,
340            allow_postgres,
341            bootstrap,
342            "interpreter",
343        )
344    }
345
346    fn start_with_bootstrap_and_backend(
347        root: Option<PathBuf>,
348        native_sockets: bool,
349        allow_process: bool,
350        allow_postgres: bool,
351        bootstrap: RuntimeBootstrap,
352        execution_backend: &str,
353    ) -> Result<Self, String> {
354        Self::start_with_bootstrap_and_backend_and_catalog(
355            root,
356            native_sockets,
357            allow_process,
358            allow_postgres,
359            bootstrap,
360            execution_backend,
361            None,
362        )
363    }
364
365    fn start_with_bootstrap_and_backend_and_catalog(
366        root: Option<PathBuf>,
367        native_sockets: bool,
368        allow_process: bool,
369        allow_postgres: bool,
370        bootstrap: RuntimeBootstrap,
371        execution_backend: &str,
372        source_catalog: Option<crate::project::SourceCatalog>,
373    ) -> Result<Self, String> {
374        crate::validate_execution_backend(execution_backend)?;
375        if allow_postgres {
376            return Err(
377                "PostgreSQL support is not included in the core hara-native crate".to_owned(),
378            );
379        }
380        let execution_backend = execution_backend.to_owned();
381        let (sender, receiver) = mpsc::channel();
382        let runtime_root = root.clone();
383        std::thread::Builder::new()
384            .name("hara-runtime-broker".into())
385            .stack_size(RUNTIME_BROKER_STACK_SIZE)
386            .spawn(move || {
387                run(
388                    receiver,
389                    runtime_root,
390                    native_sockets,
391                    allow_process,
392                    allow_postgres,
393                    bootstrap,
394                    execution_backend,
395                    source_catalog,
396                )
397            })
398            .map_err(|error| format!("runtime broker failed: {error}"))?;
399        Ok(Self {
400            handle: Arc::new(BrokerHandle { sender }),
401            root,
402        })
403    }
404
405    pub(super) fn root(&self) -> Option<&Path> {
406        self.root.as_deref()
407    }
408
409    pub fn eval(&self, session: &str, source: &str) -> Result<String, String> {
410        self.call(|reply| Request::Eval {
411            session: session.into(),
412            source: source.into(),
413            reply,
414        })
415    }
416
417    pub(crate) fn eval_diagnostic(
418        &self,
419        session: &str,
420        source: &str,
421    ) -> Result<String, RuntimeDiagnostic> {
422        let (reply, response) = mpsc::channel();
423        self.handle
424            .sender
425            .send(Request::EvalDiagnostic {
426                session: session.into(),
427                source: source.into(),
428                reply,
429            })
430            .map_err(|_| RuntimeDiagnostic::message("runtime broker is closed".into()))?;
431        response.recv().map_err(|_| {
432            RuntimeDiagnostic::message("runtime broker stopped without a response".into())
433        })?
434    }
435
436    pub fn namespace(&self, session: &str) -> Result<String, String> {
437        self.call(|reply| Request::Namespace {
438            session: session.into(),
439            reply,
440        })
441    }
442
443    pub fn complete(&self, session: &str, prefix: &str) -> Result<Vec<String>, String> {
444        self.call(|reply| Request::Complete {
445            session: session.into(),
446            prefix: prefix.into(),
447            reply,
448        })
449    }
450
451    pub fn documentation(&self, session: &str, symbol: &str) -> Result<Documentation, String> {
452        self.call(|reply| Request::Doc {
453            session: session.into(),
454            symbol: symbol.into(),
455            reply,
456        })
457    }
458
459    pub fn create(&self, session: &str) -> Result<String, String> {
460        self.call(|reply| Request::Create {
461            session: session.into(),
462            reply,
463        })
464    }
465
466    pub fn close(&self, session: &str) -> Result<String, String> {
467        self.call(|reply| Request::Close {
468            session: session.into(),
469            reply,
470        })
471    }
472
473    pub fn list(&self) -> Result<Vec<String>, String> {
474        self.call(|reply| Request::List { reply })
475    }
476
477    pub fn info(&self, session: &str) -> Result<String, String> {
478        self.call(|reply| Request::Info {
479            session: session.into(),
480            reply,
481        })
482    }
483
484    pub fn register_resource(&self, name: &str, source: &str) -> Result<(), String> {
485        self.call(|reply| Request::RegisterResource {
486            name: name.into(),
487            source: source.into(),
488            reply,
489        })
490    }
491
492    pub fn remove_resource(&self, name: &str) -> Result<(), String> {
493        self.call(|reply| Request::RemoveResource {
494            name: name.into(),
495            reply,
496        })
497    }
498
499    pub fn resources(&self) -> Result<Vec<String>, String> {
500        self.call(|reply| Request::ListResources { reply })
501    }
502
503    pub fn install_module(
504        &self,
505        session: &str,
506        manifest: &str,
507        module: &crate::wasmtime_provider::CompiledWasmModule,
508    ) -> Result<String, String> {
509        self.call(|reply| Request::InstallModule {
510            session: session.into(),
511            manifest: manifest.into(),
512            module: module.clone(),
513            reply,
514        })
515    }
516
517    pub fn invoke_hta(
518        &self,
519        session: &str,
520        qualified_var: &str,
521        arguments: &[u8],
522    ) -> Result<Vec<u8>, InvokeHtaError> {
523        let (reply, response) = mpsc::channel();
524        self.handle
525            .sender
526            .send(Request::InvokeHta {
527                session: session.into(),
528                qualified_var: qualified_var.into(),
529                arguments: arguments.into(),
530                reply,
531            })
532            .map_err(|_| InvokeHtaError::BrokerClosed)?;
533        response.recv().map_err(|_| InvokeHtaError::BrokerStopped)?
534    }
535
536    pub fn invoke_module(
537        &self,
538        session: &str,
539        namespace: &str,
540        export: &str,
541        arguments: &[u8],
542    ) -> Result<Vec<u8>, String> {
543        self.call(|reply| Request::InvokeModule {
544            session: session.into(),
545            namespace: namespace.into(),
546            export: export.into(),
547            arguments: arguments.into(),
548            reply,
549        })
550    }
551
552    fn sandbox_open(&self, spec: SandboxSpec) -> Result<SandboxId, String> {
553        self.call(|reply| Request::SandboxOpen { spec, reply })
554    }
555
556    fn sandbox_eval_receiver(
557        &self,
558        sandbox: SandboxId,
559        source: &str,
560    ) -> Result<(EvaluationId, mpsc::Receiver<Result<String, String>>), String> {
561        let (reply, response) = mpsc::channel();
562        let (started_reply, started_response) = mpsc::channel();
563        self.handle
564            .sender
565            .send(Request::SandboxEval {
566                sandbox,
567                source: source.into(),
568                started: started_reply,
569                reply,
570            })
571            .map_err(|_| "runtime broker is closed".to_owned())?;
572        let evaluation = started_response.recv().map_err(|_| {
573            "runtime broker stopped before starting sandbox evaluation".to_owned()
574        })??;
575        Ok((evaluation, response))
576    }
577
578    fn sandbox_call_receiver(
579        &self,
580        sandbox: SandboxId,
581        callable: &str,
582        arguments: &[u8],
583    ) -> Result<(EvaluationId, mpsc::Receiver<Result<Vec<u8>, String>>), String> {
584        let (reply, response) = mpsc::channel();
585        let (started_reply, started_response) = mpsc::channel();
586        self.handle
587            .sender
588            .send(Request::SandboxCall {
589                sandbox,
590                callable: callable.into(),
591                arguments: arguments.into(),
592                started: started_reply,
593                reply,
594            })
595            .map_err(|_| "runtime broker is closed".to_owned())?;
596        let evaluation = started_response
597            .recv()
598            .map_err(|_| "runtime broker stopped before starting sandbox call".to_owned())??;
599        Ok((evaluation, response))
600    }
601
602    fn sandbox_cancel(&self, sandbox: SandboxId) -> Result<bool, String> {
603        self.call(|reply| Request::SandboxCancel {
604            sandbox,
605            evaluation: None,
606            reply,
607        })
608    }
609
610    fn sandbox_cancel_evaluation(
611        &self,
612        sandbox: SandboxId,
613        evaluation: EvaluationId,
614    ) -> Result<bool, String> {
615        self.call(|reply| Request::SandboxCancel {
616            sandbox,
617            evaluation: Some(evaluation),
618            reply,
619        })
620    }
621
622    fn sandbox_status(&self, sandbox: SandboxId) -> Result<SandboxStatus, String> {
623        self.call(|reply| Request::SandboxStatus { sandbox, reply })
624    }
625
626    fn sandbox_close(&self, sandbox: SandboxId) -> Result<(), String> {
627        self.call(|reply| Request::SandboxClose { sandbox, reply })
628    }
629
630    fn call<T>(
631        &self,
632        request: impl FnOnce(mpsc::Sender<Result<T, String>>) -> Request,
633    ) -> Result<T, String> {
634        let (reply, response) = mpsc::channel();
635        self.handle
636            .sender
637            .send(request(reply))
638            .map_err(|_| "runtime broker is closed".to_owned())?;
639        response
640            .recv()
641            .map_err(|_| "runtime broker stopped without a response".to_owned())?
642    }
643}
644
645fn runtime(
646    root: Option<&PathBuf>,
647    native_sockets: bool,
648    allow_process: bool,
649    allow_postgres: bool,
650    bootstrap: RuntimeBootstrap,
651    execution_backend: &str,
652    source_catalog: Option<&crate::project::SourceCatalog>,
653) -> Runtime {
654    let mut runtime = match bootstrap {
655        RuntimeBootstrap::Full => Runtime::new(),
656        RuntimeBootstrap::Core | RuntimeBootstrap::Source => Runtime::core(),
657    };
658    if let Some(source_catalog) = source_catalog {
659        runtime.register_source_catalog(source_catalog);
660    }
661    if matches!(bootstrap, RuntimeBootstrap::Source) {
662        runtime
663            .bootstrap_source_foundation()
664            .expect("source Foundation bootstrap must be valid");
665    }
666    if let Some(root) = root {
667        runtime.install_native_file_provider(root.to_string_lossy().as_ref());
668    }
669    if native_sockets {
670        runtime.install_native_socket_provider();
671    }
672    if allow_process {
673        runtime.install_native_process_provider();
674    }
675    // Native bootstrap recompiles evaluator-created protocol closures. Install
676    // the host providers first so those closures capture the runtime's actual
677    // authority instead of the zero-provider bootstrap context.
678    runtime
679        .configure_execution_backend(execution_backend)
680        .expect("validated execution backend must configure");
681    let _ = allow_postgres;
682    runtime
683}
684
685fn run(
686    receiver: mpsc::Receiver<Request>,
687    root: Option<PathBuf>,
688    native_sockets: bool,
689    allow_process: bool,
690    allow_postgres: bool,
691    bootstrap: RuntimeBootstrap,
692    execution_backend: String,
693    source_catalog: Option<crate::project::SourceCatalog>,
694) {
695    let runtime_root = root.clone();
696    let runtime_backend = execution_backend.clone();
697    let runtime_catalog = source_catalog;
698    let runtime_factory: Rc<dyn Fn() -> Runtime> = Rc::new(move || {
699        runtime(
700            runtime_root.as_ref(),
701            native_sockets,
702            allow_process,
703            allow_postgres,
704            bootstrap,
705            &runtime_backend,
706            runtime_catalog.as_ref(),
707        )
708    });
709    let root_runtime = runtime_factory();
710    let mut kernel = SessionKernel::with_runtime_factory(root_runtime, runtime_factory);
711    kernel.register_sandbox_provider(Rc::new(InProcessSandboxProvider));
712    while let Ok(request) = receiver.recv() {
713        match request {
714            Request::Eval {
715                session,
716                source,
717                reply,
718            } => {
719                let result = broker_session_id(&session).and_then(|id| {
720                    let runtime = kernel.session_mut(&id)?.runtime_mut()?;
721                    if execution_backend == "direct-native" {
722                        runtime.eval_native(&source)
723                    } else {
724                        runtime.eval_native_traced(&source)
725                    }
726                });
727                let _ = reply.send(result);
728            }
729            Request::EvalDiagnostic {
730                session,
731                source,
732                reply,
733            } => {
734                let result = broker_session_id(&session)
735                    .map_err(RuntimeDiagnostic::message)
736                    .and_then(|id| {
737                        let runtime = kernel
738                            .session_mut(&id)
739                            .and_then(|session| session.runtime_mut())
740                            .map_err(RuntimeDiagnostic::message)?;
741                        let ((result, frames), exception) = runtime.eval_native_diagnostic(&source);
742                        result.map_err(|message| RuntimeDiagnostic {
743                            message,
744                            exception: captured_exception(exception),
745                            frames,
746                        })
747                    });
748                let _ = reply.send(result);
749            }
750            Request::Namespace { session, reply } => {
751                let result =
752                    broker_session_id(&session).and_then(|id| kernel.session_namespace(&id));
753                let _ = reply.send(result);
754            }
755            Request::Complete {
756                session,
757                prefix,
758                reply,
759            } => {
760                let result = broker_session_id(&session).and_then(|id| {
761                    kernel.session(&id)?.runtime().map(|runtime| {
762                        let mut symbols = runtime
763                            .visible_symbols()
764                            .into_iter()
765                            .filter(|symbol| symbol.starts_with(&prefix))
766                            .collect::<Vec<_>>();
767                        symbols.dedup();
768                        symbols
769                    })
770                });
771                let _ = reply.send(result);
772            }
773            Request::Doc {
774                session,
775                symbol,
776                reply,
777            } => {
778                let result = broker_session_id(&session)
779                    .and_then(|id| documentation(kernel.session(&id)?.runtime()?, &symbol));
780                let _ = reply.send(result);
781            }
782            Request::Create { session, reply } => {
783                let result = SessionId::parse(&session)
784                    .map_err(|_| format!("Session already exists or is invalid: {session}"))
785                    .and_then(|id| {
786                        kernel
787                            .create_session(id)
788                            .map_err(|_| format!("Session already exists or is invalid: {session}"))
789                    })
790                    .map(|_| session);
791                let _ = reply.send(result);
792            }
793            Request::Close { session, reply } => {
794                let result = broker_session_id(&session)
795                    .and_then(|id| kernel.close_session(&id))
796                    .map(|_| session)
797                    .map_err(|error| match error.as_str() {
798                        "ROOT_CANNOT_CLOSE" => "ROOT cannot be closed".into(),
799                        _ if error.starts_with("NO_SESSION ") => {
800                            format!("No session: {}", error.trim_start_matches("NO_SESSION "))
801                        }
802                        _ => error,
803                    });
804                let _ = reply.send(result);
805            }
806            Request::List { reply } => {
807                let names = kernel
808                    .session_names()
809                    .into_iter()
810                    .map(|id| id.to_string())
811                    .collect();
812                let _ = reply.send(Ok(names));
813            }
814            Request::Info { session, reply } => {
815                let result = broker_session_id(&session)
816                    .and_then(|id| kernel.session_namespace(&id))
817                    .map(|namespace| format!("{session} {namespace}"));
818                let _ = reply.send(result);
819            }
820            Request::RegisterResource {
821                name,
822                source,
823                reply,
824            } => {
825                kernel.register_resource(&name, &source);
826                let _ = reply.send(Ok(()));
827            }
828            Request::RemoveResource { name, reply } => {
829                kernel.remove_resource(&name);
830                let _ = reply.send(Ok(()));
831            }
832            Request::ListResources { reply } => {
833                let _ = reply.send(Ok(kernel.resource_names()));
834            }
835            Request::InstallModule {
836                session,
837                manifest,
838                module,
839                reply,
840            } => {
841                let result = broker_session_id(&session).and_then(|id| {
842                    let runtime = kernel.session_mut(&id)?.runtime_mut()?;
843                    let provider = module.provider();
844                    let parsed =
845                        crate::extension::ExtensionManifest::parse(&manifest, "MODULE PUT")?;
846                    let namespace = parsed.namespace.clone();
847                    runtime.install_wasm_extension(&manifest, "MODULE PUT", provider)?;
848                    Ok(namespace)
849                });
850                let _ = reply.send(result);
851            }
852            Request::InvokeModule {
853                session,
854                namespace,
855                export,
856                arguments,
857                reply,
858            } => {
859                let result = broker_session_id(&session).and_then(|id| {
860                    let runtime = kernel.session_mut(&id)?.runtime_mut()?;
861                    let arguments = crate::hta::decode(&arguments)?;
862                    let arguments: Vec<crate::extension::Value> = match arguments {
863                        crate::extension::Value::Vector(values) => values.iter().cloned().collect(),
864                        crate::extension::Value::Tuple(values) => values.iter().cloned().collect(),
865                        other => {
866                            return Err(format!(
867                                "hta/arguments: expected vector, got {}",
868                                other.display()
869                            ))
870                        }
871                    };
872                    let result = runtime.invoke_wasm_extension(&namespace, &export, &arguments)?;
873                    crate::hta::encode(&result)
874                });
875                let _ = reply.send(result);
876            }
877            Request::InvokeHta {
878                session,
879                qualified_var,
880                arguments,
881                reply,
882            } => {
883                let result = SessionId::parse(&session)
884                    .map_err(|_| InvokeHtaError::SessionMissing(session.clone()))
885                    .and_then(|id| {
886                        kernel
887                            .session_mut(&id)
888                            .map_err(|_| InvokeHtaError::SessionMissing(session.clone()))?
889                            .runtime_mut()
890                            .map_err(InvokeHtaError::Execution)?
891                            .invoke_hta(&qualified_var, &arguments)
892                    });
893                let _ = reply.send(result);
894            }
895            Request::SandboxOpen { spec, reply } => {
896                let _ = reply.send(kernel.open_sandbox(spec).map_err(|error| error.to_string()));
897            }
898            Request::SandboxEval {
899                sandbox,
900                source,
901                started,
902                reply,
903            } => match kernel.sandbox_eval(sandbox, &source) {
904                Ok(pending) => {
905                    let _ = started.send(Ok(pending.evaluation()));
906                    std::thread::spawn(move || {
907                        let _ = reply.send(pending.wait().map_err(|error| error.to_string()));
908                    });
909                }
910                Err(error) => {
911                    let error = error.to_string();
912                    let _ = started.send(Err(error.clone()));
913                    let _ = reply.send(Err(error));
914                }
915            },
916            Request::SandboxCall {
917                sandbox,
918                callable,
919                arguments,
920                started,
921                reply,
922            } => match kernel.sandbox_call(sandbox, &callable, &arguments) {
923                Ok(pending) => {
924                    let _ = started.send(Ok(pending.evaluation()));
925                    std::thread::spawn(move || {
926                        let _ = reply.send(pending.wait().map_err(|error| error.to_string()));
927                    });
928                }
929                Err(error) => {
930                    let error = error.to_string();
931                    let _ = started.send(Err(error.clone()));
932                    let _ = reply.send(Err(error));
933                }
934            },
935            Request::SandboxCancel {
936                sandbox,
937                evaluation,
938                reply,
939            } => {
940                let result = match evaluation {
941                    Some(evaluation) => kernel.cancel_sandbox_evaluation(sandbox, evaluation),
942                    None => kernel.cancel_sandbox(sandbox),
943                };
944                let _ = reply.send(result.map_err(|error| error.to_string()));
945            }
946            Request::SandboxStatus { sandbox, reply } => {
947                let _ = reply.send(
948                    kernel
949                        .sandbox_status(sandbox)
950                        .map_err(|error| error.to_string()),
951                );
952            }
953            Request::SandboxClose { sandbox, reply } => {
954                let _ = reply.send(
955                    kernel
956                        .close_sandbox(sandbox)
957                        .map_err(|error| error.to_string()),
958                );
959            }
960            Request::Shutdown => break,
961        }
962    }
963}
964
965fn broker_session_id(session: &str) -> Result<SessionId, String> {
966    SessionId::parse(session).map_err(|_| format!("No session: {session}"))
967}
968
969fn documentation(runtime: &Runtime, symbol: &str) -> Result<Documentation, String> {
970    documentation::lookup(runtime, symbol)
971}
972
973/// Installs the generic native driver behind `std.native.Kernel/*`.
974/// Command policy remains in Hara; this adapter only multiplexes isolated
975/// evaluator sessions and transfers portable values across the boundary.
976pub fn install_native_kernel(runtime: &mut Runtime, broker: RuntimeBroker) {
977    runtime.install_native_kernel_provider(Rc::new(move |operation, arguments| {
978        kernel_call(&broker, &operation, &arguments)
979    }));
980}
981
982#[cfg(test)]
983mod tests;