Skip to main content

oino/
loader.rs

1//! Extension sources (`-e/--extension`, installed packages and the distribution share dir,
2//! all resolved by [`oino_core::extensions::sources`] — dist.md §5) plus the `builtin:<id>`
3//! test components (feature `faux`) and the [`WasmRuntime`] construction.
4//!
5//! Load order: share-dir packages, then `~/.oino/extensions`, then trust-gated
6//! `<cwd>/.oino/extensions`, then `-e`; a package with the id of an earlier one replaces it
7//! in place, so a user or `-e` copy shadows the distribution. `--exclude-extension <id>`
8//! drops composed packages by id before instantiation (WP-X2). `OINO_DIST_EXTENSIONS`
9//! overrides the share dir (empty: disabled).
10//!
11//! Every `CliLoader` builds its own [`WasmRuntime`] (the cwd is per instance), but the
12//! engine, epoch-ticker thread, compile caches and local-services hub are process-shared
13//! (`oino_wasm_host::EngineRegistry`, E-ENGINE): the startup path, every ACP session and a
14//! multi-cwd host share one engine and compile each distinct component once per process.
15
16use std::sync::{Arc, PoisonError};
17
18use futures::future::BoxFuture;
19use oino_core::extensions::api::ExtensionInstance;
20use oino_core::extensions::host::HostApi;
21use oino_core::extensions::sources::{SourcesContext, resolve};
22use oino_core::services::ExtensionLoader;
23use oino_wasm_host::core_bridge::{CoreLoader, SourcedPackage};
24use oino_wasm_host::{HostConfig, WasmRuntime, WasmRuntimeOptions};
25
26use crate::cli::args::Diagnostic;
27
28/// The extension load failure hint (D13).
29pub const EXTENSION_LOAD_FAILURE_HINT: &str = "Hint: Start without extensions using \"oino -ne\".";
30
31/// Components instantiated by the binary itself (feature `faux`).
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33enum Builtin {
34    /// Scripted provider (feature `faux`).
35    #[cfg(feature = "faux")]
36    Faux,
37    /// UI probe extension (feature `faux`).
38    #[cfg(feature = "faux")]
39    UiProbe,
40}
41
42impl Builtin {
43    fn parse(id: &str) -> Option<Self> {
44        match id {
45            #[cfg(feature = "faux")]
46            "faux" => Some(Self::Faux),
47            #[cfg(feature = "faux")]
48            "ui-probe" => Some(Self::UiProbe),
49            _ => None,
50        }
51    }
52
53    #[cfg_attr(
54        not(feature = "faux"),
55        allow(unused_variables, reason = "no built-ins without features")
56    )]
57    fn instantiate(self, host: &Arc<dyn HostApi>) -> Arc<dyn ExtensionInstance> {
58        match self {
59            #[cfg(feature = "faux")]
60            Self::Faux => Arc::new(crate::faux::FauxComponent(
61                crate::faux::FauxProvider::from_env().with_host(host),
62            )),
63            #[cfg(feature = "faux")]
64            Self::UiProbe => Arc::new(crate::faux::UiProbe::new(host)),
65        }
66    }
67}
68
69/// Where packages are looked up.
70#[derive(Clone, Debug)]
71pub struct LoaderContext {
72    /// Agent dir (`~/.oino`).
73    pub agent_dir: std::path::PathBuf,
74    /// Project cwd.
75    pub cwd: std::path::PathBuf,
76    /// `-ne`: skip `kind: "extension"` packages of the scanned sources (providers stay).
77    pub no_extensions: bool,
78    /// `--exclude-extension`: package ids dropped after composition, before instantiation
79    /// (WP-X2); `builtin:` test components are not subject.
80    pub exclude_extensions: Vec<String>,
81}
82
83impl LoaderContext {
84    fn sources_context(&self) -> SourcesContext {
85        SourcesContext {
86            agent_dir: self.agent_dir.clone(),
87            cwd: self.cwd.clone(),
88            dist_dirs: Vec::new(),
89            no_extensions: self.no_extensions,
90            excluded: self.exclude_extensions.clone(),
91        }
92    }
93}
94
95/// Loader for share-dir, installed and `-e` extensions (`builtin:` test components).
96pub struct CliLoader {
97    builtins: Vec<Builtin>,
98    wasm: Option<CoreLoader>,
99    diagnostics: Vec<Diagnostic>,
100}
101
102impl std::fmt::Debug for CliLoader {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        f.debug_struct("CliLoader")
105            .field("builtins", &self.builtins)
106            .field("wasm", &self.wasm)
107            .finish_non_exhaustive()
108    }
109}
110
111fn diagnostic(d: oino_core::extensions::sources::SourceDiagnostic) -> Diagnostic {
112    use oino_core::extensions::sources::SourceSeverity;
113    match d.severity {
114        SourceSeverity::Warning => Diagnostic::warning(d.message),
115        SourceSeverity::Error => Diagnostic::error(d.message),
116    }
117}
118
119/// An unrecognised `builtin:` id keeps the `-e` error shape (and names the id).
120fn unknown_builtin(source: &str, id: &str) -> Diagnostic {
121    Diagnostic::error(format!(
122        "Failed to load extension \"{source}\": unknown built-in extension \"{id}\""
123    ))
124}
125
126impl CliLoader {
127    /// Resolves the package set; unknown `-e` sources become `Failed to load extension` errors.
128    #[must_use]
129    pub fn new(sources: &[String], context: &LoaderContext) -> Self {
130        let mut builtins = Vec::new();
131        let mut explicit = Vec::new();
132        let mut diagnostics = Vec::new();
133        for source in sources {
134            if let Some(id) = source.strip_prefix("builtin:") {
135                match Builtin::parse(id) {
136                    Some(builtin) => builtins.push(builtin),
137                    None => diagnostics.push(unknown_builtin(source, id)),
138                }
139            } else {
140                explicit.push(source.clone());
141            }
142        }
143        let resolved = resolve(&explicit, &context.sources_context());
144        diagnostics.extend(resolved.diagnostics.into_iter().map(diagnostic));
145        let packages = resolved
146            .packages
147            .into_iter()
148            .map(|p| SourcedPackage {
149                package: p.package,
150                project: p.project,
151            })
152            .collect::<Vec<_>>();
153        let wasm = if packages.is_empty() {
154            None
155        } else {
156            let config = HostConfig::new(context.agent_dir.clone(), context.cwd.clone());
157            // Resolves the process-shared engine (E-ENGINE): repeated loaders (ACP sessions,
158            // a multi-cwd host) reuse one engine, one ticker thread and the compile caches.
159            match WasmRuntime::new(WasmRuntimeOptions::new(config)) {
160                Ok(runtime) => Some(CoreLoader::new(Arc::new(runtime), packages)),
161                Err(error) => {
162                    diagnostics.push(Diagnostic::error(format!(
163                        "Failed to start the extension host: {error}"
164                    )));
165                    None
166                }
167            }
168        };
169        Self {
170            builtins,
171            wasm,
172            diagnostics,
173        }
174    }
175
176    /// Problems found while resolving sources.
177    #[must_use]
178    pub fn diagnostics(&self) -> &[Diagnostic] {
179        &self.diagnostics
180    }
181
182    /// Component load failures from the last [`ExtensionLoader::load`] (warnings).
183    #[must_use]
184    pub fn load_errors(&self) -> Vec<Diagnostic> {
185        self.wasm.as_ref().map_or_else(Vec::new, |wasm| {
186            wasm.errors
187                .lock()
188                .unwrap_or_else(PoisonError::into_inner)
189                .iter()
190                .map(|e| {
191                    Diagnostic::warning(format!(
192                        "Failed to load extension \"{}\": {}",
193                        e.extension, e.message
194                    ))
195                })
196                .collect()
197        })
198    }
199}
200
201impl ExtensionLoader for CliLoader {
202    fn load(
203        &self,
204        host: Arc<dyn HostApi>,
205        project_trusted: bool,
206    ) -> BoxFuture<'_, Vec<Arc<dyn ExtensionInstance>>> {
207        self.load_enabled(host, project_trusted, &[])
208    }
209
210    fn load_enabled<'a>(
211        &'a self,
212        host: Arc<dyn HostApi>,
213        project_trusted: bool,
214        disabled: &'a [String],
215    ) -> BoxFuture<'a, Vec<Arc<dyn ExtensionInstance>>> {
216        Box::pin(async move {
217            let mut instances: Vec<Arc<dyn ExtensionInstance>> = self
218                .builtins
219                .iter()
220                .map(|builtin| builtin.instantiate(&host))
221                .collect();
222            if let Some(wasm) = &self.wasm {
223                instances.extend(wasm.load_enabled(host, project_trusted, disabled).await);
224            }
225            instances
226        })
227    }
228}