Skip to main content

harn_hostlib/
registry.rs

1//! Registration plumbing.
2//!
3//! Each module exposes a [`HostlibCapability`] implementation that pushes
4//! its builtins into a [`BuiltinRegistry`]. The registry can then either
5//! be wired into a real [`harn_vm::Vm`] (production path) or introspected
6//! by tests to assert the exposed surface without touching the VM.
7
8use std::sync::Arc;
9
10use harn_vm::{Vm, VmError, VmValue};
11
12use crate::error::HostlibError;
13
14/// Sync builtin handler signature. Mirrors the closure type accepted by
15/// [`harn_vm::Vm::register_builtin`]; we keep it `Send + Sync` so capability
16/// instances can be shared across threads if an embedder ever wants that.
17pub type SyncHandler = Arc<dyn Fn(&[VmValue]) -> Result<VmValue, HostlibError> + Send + Sync>;
18
19/// One registered builtin. The name is what Harn scripts call (e.g.
20/// `hostlib_ast_parse_file`); `module` and `method` are the canonical
21/// schema-directory coordinates (`schemas/<module>/<method>.request.json`).
22#[derive(Clone)]
23pub struct RegisteredBuiltin {
24    /// Builtin name as Harn scripts see it.
25    pub name: &'static str,
26    /// Module bucket (e.g. `"ast"`, `"tools"`).
27    pub module: &'static str,
28    /// Method name within the module (e.g. `"parse_file"`, `"search"`).
29    pub method: &'static str,
30    /// Handler invoked when Harn calls the builtin.
31    pub handler: SyncHandler,
32}
33
34impl std::fmt::Debug for RegisteredBuiltin {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        f.debug_struct("RegisteredBuiltin")
37            .field("name", &self.name)
38            .field("module", &self.module)
39            .field("method", &self.method)
40            .finish()
41    }
42}
43
44/// Mutable collector each capability writes into during `register`.
45#[derive(Default)]
46pub struct BuiltinRegistry {
47    builtins: Vec<RegisteredBuiltin>,
48}
49
50impl BuiltinRegistry {
51    /// Construct an empty registry.
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Push one builtin. Capabilities call this from `register_builtins`.
57    pub fn register(&mut self, builtin: RegisteredBuiltin) {
58        self.builtins.push(builtin);
59    }
60
61    /// Convenience: register a builtin whose body is the `unimplemented`
62    /// scaffold error.
63    pub fn register_unimplemented(
64        &mut self,
65        name: &'static str,
66        module: &'static str,
67        method: &'static str,
68    ) {
69        let handler: SyncHandler =
70            Arc::new(move |_args| Err(HostlibError::Unimplemented { builtin: name }));
71        self.register(RegisteredBuiltin {
72            name,
73            module,
74            method,
75            handler,
76        });
77    }
78
79    /// Convenience: register a stateless builtin backed by a plain fn
80    /// pointer. This is the shape almost every capability module uses;
81    /// keeping it here avoids each module hand-rolling its own copy.
82    pub(crate) fn register_fn(
83        &mut self,
84        module: &'static str,
85        name: &'static str,
86        method: &'static str,
87        runner: fn(&[VmValue]) -> Result<VmValue, HostlibError>,
88    ) {
89        let handler: SyncHandler = Arc::new(runner);
90        self.register(RegisteredBuiltin {
91            name,
92            module,
93            method,
94            handler,
95        });
96    }
97
98    /// Like [`Self::register_fn`], but wraps the handler in the shared
99    /// deterministic-tools permission gate
100    /// ([`crate::tools::permissions::gated_handler`]).
101    pub(crate) fn register_gated_fn(
102        &mut self,
103        module: &'static str,
104        name: &'static str,
105        method: &'static str,
106        runner: fn(&[VmValue]) -> Result<VmValue, HostlibError>,
107    ) {
108        self.register(RegisteredBuiltin {
109            name,
110            module,
111            method,
112            handler: crate::tools::permissions::gated_handler(name, runner),
113        });
114    }
115
116    /// Iterate over every registered builtin.
117    pub fn iter(&self) -> impl Iterator<Item = &RegisteredBuiltin> {
118        self.builtins.iter()
119    }
120
121    /// Total count.
122    pub fn len(&self) -> usize {
123        self.builtins.len()
124    }
125
126    /// True when nothing has been registered yet.
127    pub fn is_empty(&self) -> bool {
128        self.builtins.is_empty()
129    }
130
131    /// Look up a builtin by its Harn-visible name.
132    pub fn find(&self, name: &str) -> Option<&RegisteredBuiltin> {
133        self.builtins.iter().find(|b| b.name == name)
134    }
135}
136
137/// One module's worth of builtins. Kept tiny on purpose: capabilities exist
138/// purely so tests can reason about the surface without booting a VM, and
139/// so embedders can opt into individual modules.
140pub trait HostlibCapability: 'static {
141    /// Module name (matches the `schemas/<module>/` directory).
142    fn module_name(&self) -> &'static str;
143
144    /// Push every builtin this module exposes into `registry`.
145    fn register_builtins(&self, registry: &mut BuiltinRegistry);
146}
147
148/// Composes capabilities and emits VM registrations.
149///
150/// `HostlibRegistry` is the type embedders interact with. It owns the
151/// capability instances and the populated [`BuiltinRegistry`] together so
152/// the same surface can be inspected by tests *and* wired into a VM.
153pub struct HostlibRegistry {
154    builtins: BuiltinRegistry,
155    modules: Vec<&'static str>,
156}
157
158impl Default for HostlibRegistry {
159    fn default() -> Self {
160        Self::new()
161    }
162}
163
164impl HostlibRegistry {
165    /// Construct an empty registry. Most callers want [`crate::install_default`]
166    /// instead, which pre-populates every shipped capability.
167    pub fn new() -> Self {
168        Self {
169            builtins: BuiltinRegistry::new(),
170            modules: Vec::new(),
171        }
172    }
173
174    /// Add one capability to the registry. Returns `self` for chaining.
175    #[must_use]
176    pub fn with<C: HostlibCapability>(mut self, capability: C) -> Self {
177        let module = capability.module_name();
178        capability.register_builtins(&mut self.builtins);
179        self.modules.push(module);
180        self
181    }
182
183    /// Wire every registered builtin into the supplied VM.
184    pub fn register_into_vm(&mut self, vm: &mut Vm) {
185        for builtin in self.builtins.iter().cloned() {
186            let module = builtin.module;
187            let method = builtin.method;
188            let handler = builtin.handler.clone();
189            vm.register_builtin(
190                builtin.name,
191                move |args, _out| -> Result<VmValue, VmError> {
192                    if let Some(params) = args.first().and_then(VmValue::as_dict) {
193                        if let Some(mocked) = harn_vm::stdlib::host::dispatch_mock_hostlib_call(
194                            module, method, params,
195                        ) {
196                            return mocked;
197                        }
198                    }
199                    handler(args).map_err(VmError::from)
200                },
201            );
202        }
203    }
204
205    /// Borrow the underlying [`BuiltinRegistry`] for introspection (e.g.
206    /// schema-drift tests).
207    pub fn builtins(&self) -> &BuiltinRegistry {
208        &self.builtins
209    }
210
211    /// List the module names that have been registered, in insertion order.
212    pub fn modules(&self) -> &[&'static str] {
213        &self.modules
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn unimplemented_builtins_route_through_error() {
223        let mut registry = BuiltinRegistry::new();
224        registry.register_unimplemented("hostlib_demo", "demo", "ping");
225        let entry = registry.find("hostlib_demo").expect("registered");
226        let err = (entry.handler)(&[]).expect_err("should be unimplemented");
227        assert!(
228            matches!(err, HostlibError::Unimplemented { builtin } if builtin == "hostlib_demo")
229        );
230    }
231
232    #[test]
233    fn registry_records_modules_in_order() {
234        struct First;
235        impl HostlibCapability for First {
236            fn module_name(&self) -> &'static str {
237                "first"
238            }
239            fn register_builtins(&self, _registry: &mut BuiltinRegistry) {}
240        }
241        struct Second;
242        impl HostlibCapability for Second {
243            fn module_name(&self) -> &'static str {
244                "second"
245            }
246            fn register_builtins(&self, _registry: &mut BuiltinRegistry) {}
247        }
248
249        let registry = HostlibRegistry::new().with(First).with(Second);
250        assert_eq!(registry.modules(), &["first", "second"]);
251    }
252}