1use std::collections::BTreeSet;
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::Arc;
12
13use harn_vm::{Vm, VmError, VmValue};
14
15use crate::error::HostlibError;
16
17fn capability_binding(
18 module: &'static str,
19 method: &'static str,
20) -> (harn_builtin_meta::CapabilityId, &'static str) {
21 harn_builtin_meta::host_capabilities::capability_binding_for_schema(module, method)
22 .unwrap_or_else(|| panic!("hostlib schema `{module}.{method}` has no typed capability"))
23}
24
25pub type SyncHandler = Arc<dyn Fn(&[VmValue]) -> Result<VmValue, HostlibError> + Send + Sync>;
29pub type AsyncHandler = Arc<
31 dyn Fn(Vec<VmValue>) -> Pin<Box<dyn Future<Output = Result<VmValue, HostlibError>> + Send>>
32 + Send
33 + Sync,
34>;
35
36#[derive(Clone)]
37pub struct RegisteredAsyncBuiltin {
39 pub name: &'static str,
41 pub module: &'static str,
43 pub method: &'static str,
45 pub handler: AsyncHandler,
47}
48
49#[derive(Clone)]
53pub struct RegisteredBuiltin {
54 pub name: &'static str,
56 pub module: &'static str,
58 pub method: &'static str,
60 pub handler: SyncHandler,
62}
63
64impl std::fmt::Debug for RegisteredBuiltin {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 f.debug_struct("RegisteredBuiltin")
67 .field("name", &self.name)
68 .field("module", &self.module)
69 .field("method", &self.method)
70 .finish()
71 }
72}
73
74#[derive(Default)]
76pub struct BuiltinRegistry {
77 builtins: Vec<RegisteredBuiltin>,
78 async_builtins: Vec<RegisteredAsyncBuiltin>,
79 command_policy_builtins: BTreeSet<&'static str>,
80}
81
82impl BuiltinRegistry {
83 pub fn new() -> Self {
85 Self::default()
86 }
87
88 pub fn register(&mut self, builtin: RegisteredBuiltin) {
90 self.builtins.push(builtin);
91 }
92
93 pub fn register_unimplemented(
96 &mut self,
97 name: &'static str,
98 module: &'static str,
99 method: &'static str,
100 ) {
101 let handler: SyncHandler =
102 Arc::new(move |_args| Err(HostlibError::Unimplemented { builtin: name }));
103 self.register(RegisteredBuiltin {
104 name,
105 module,
106 method,
107 handler,
108 });
109 }
110
111 pub(crate) fn register_fn(
115 &mut self,
116 module: &'static str,
117 name: &'static str,
118 method: &'static str,
119 runner: fn(&[VmValue]) -> Result<VmValue, HostlibError>,
120 ) {
121 let handler: SyncHandler = Arc::new(runner);
122 self.register(RegisteredBuiltin {
123 name,
124 module,
125 method,
126 handler,
127 });
128 }
129
130 pub(crate) fn register_command_fn(
133 &mut self,
134 module: &'static str,
135 name: &'static str,
136 method: &'static str,
137 runner: fn(&[VmValue]) -> Result<VmValue, HostlibError>,
138 ) {
139 self.register_fn(module, name, method, runner);
140 self.command_policy_builtins.insert(name);
141 }
142
143 pub(crate) fn register_async_fn<F, Fut>(
144 &mut self,
145 module: &'static str,
146 name: &'static str,
147 method: &'static str,
148 runner: F,
149 ) where
150 F: Fn(Vec<VmValue>) -> Fut + Send + Sync + 'static,
151 Fut: Future<Output = Result<VmValue, HostlibError>> + Send + 'static,
152 {
153 let runner = Arc::new(runner);
154 let handler: AsyncHandler = Arc::new(move |args| Box::pin(runner(args)));
155 self.async_builtins.push(RegisteredAsyncBuiltin {
156 name,
157 module,
158 method,
159 handler,
160 });
161 }
162
163 fn uses_command_policy(&self, name: &str) -> bool {
164 self.command_policy_builtins.contains(name)
165 }
166
167 pub fn iter(&self) -> impl Iterator<Item = &RegisteredBuiltin> {
169 self.builtins.iter()
170 }
171
172 pub fn iter_async(&self) -> impl Iterator<Item = &RegisteredAsyncBuiltin> {
174 self.async_builtins.iter()
175 }
176
177 pub fn len(&self) -> usize {
179 self.builtins.len() + self.async_builtins.len()
180 }
181
182 pub fn is_empty(&self) -> bool {
184 self.builtins.is_empty() && self.async_builtins.is_empty()
185 }
186
187 pub fn find(&self, name: &str) -> Option<&RegisteredBuiltin> {
189 self.builtins.iter().find(|b| b.name == name)
190 }
191
192 pub fn find_async(&self, name: &str) -> Option<&RegisteredAsyncBuiltin> {
194 self.async_builtins.iter().find(|b| b.name == name)
195 }
196}
197
198pub trait HostlibCapability: 'static {
202 fn module_name(&self) -> &'static str;
204
205 fn register_builtins(&self, registry: &mut BuiltinRegistry);
207}
208
209pub struct HostlibRegistry {
215 builtins: BuiltinRegistry,
216 modules: Vec<&'static str>,
217}
218
219impl Default for HostlibRegistry {
220 fn default() -> Self {
221 Self::new()
222 }
223}
224
225impl HostlibRegistry {
226 pub fn new() -> Self {
229 Self {
230 builtins: BuiltinRegistry::new(),
231 modules: Vec::new(),
232 }
233 }
234
235 #[must_use]
237 pub fn with<C: HostlibCapability>(mut self, capability: C) -> Self {
238 let module = capability.module_name();
239 capability.register_builtins(&mut self.builtins);
240 self.modules.push(module);
241 self
242 }
243
244 pub fn register_into_vm(&mut self, vm: &mut Vm) {
246 for builtin in self.builtins.iter().cloned() {
247 let module = builtin.module;
248 let method = builtin.method;
249 let (capability, capability_method) = capability_binding(module, method);
250 harn_vm::stdlib::host::register_callable_host_operation(
251 module,
252 method,
253 "Hostlib schema-backed operation registered at runtime.",
254 );
255 let handler = builtin.handler.clone();
256 if self.builtins.uses_command_policy(builtin.name) {
257 vm.register_async_capability_method(
258 capability,
259 capability_method,
260 move |ctx, args| {
261 let handler = handler.clone();
262 async move {
263 let request = crate::schemas::validate_request_args(
264 builtin.name,
265 module,
266 method,
267 &args,
268 )
269 .map_err(VmError::from)?;
270 let params = request.as_dict().ok_or_else(|| {
271 VmError::Runtime(format!(
272 "{}: validated request must be a dict",
273 builtin.name
274 ))
275 })?;
276 let caller = serde_json::json!({
277 "surface": "hostlib",
278 "builtin": builtin.name,
279 "module": module,
280 "method": method,
281 "session_id": harn_vm::current_agent_session_id(),
282 });
283 match harn_vm::orchestration::run_command_policy_preflight_with_ctx(
284 Some(&ctx),
285 params,
286 caller,
287 )
288 .await?
289 {
290 harn_vm::orchestration::CommandPolicyPreflight::Blocked {
291 status,
292 message,
293 context,
294 decisions,
295 } => {
296 let response = harn_vm::orchestration::blocked_command_response(
297 params, status, &message, context, decisions,
298 );
299 crate::schemas::validate_response(
300 builtin.name,
301 module,
302 method,
303 crate::tools::policy_blocked_run_command_response(response),
304 )
305 .map_err(VmError::from)
306 }
307 harn_vm::orchestration::CommandPolicyPreflight::Proceed {
308 params,
309 context,
310 decisions,
311 } => {
312 let rewritten = VmValue::dict(params.clone());
316 let validated = crate::schemas::validate_request_args(
317 builtin.name,
318 module,
319 method,
320 &[rewritten],
321 )
322 .map_err(VmError::from)?;
323 let result = handler(&[validated]).map_err(VmError::from)?;
324 if crate::tools::run_command_request_is_background(¶ms) {
325 return crate::schemas::validate_response(
326 builtin.name,
327 module,
328 method,
329 result,
330 )
331 .map_err(VmError::from);
332 }
333 let result =
334 harn_vm::orchestration::run_command_policy_postflight_with_ctx(
335 Some(&ctx),
336 ¶ms,
337 result,
338 context,
339 decisions,
340 )
341 .await?;
342 crate::schemas::validate_response(
343 builtin.name,
344 module,
345 method,
346 result,
347 )
348 .map_err(VmError::from)
349 }
350 }
351 }
352 },
353 );
354 } else {
355 vm.register_capability_method(
356 capability,
357 capability_method,
358 move |args, _out| -> Result<VmValue, VmError> {
359 let request = crate::schemas::validate_request_args(
360 builtin.name,
361 module,
362 method,
363 args,
364 )
365 .map_err(VmError::from)?;
366 let validated_args = [request];
367 handler(&validated_args).map_err(VmError::from)
368 },
369 );
370 }
371 }
372 for builtin in self.builtins.async_builtins.iter().cloned() {
373 let module = builtin.module;
374 let method = builtin.method;
375 let (capability, capability_method) = capability_binding(module, method);
376 harn_vm::stdlib::host::register_callable_host_operation(
377 module,
378 method,
379 "Hostlib schema-backed operation registered at runtime.",
380 );
381 let handler = builtin.handler.clone();
382 vm.register_async_capability_method(
383 capability,
384 capability_method,
385 move |_ctx, args| {
386 let handler = handler.clone();
387 async move {
388 let request = crate::schemas::validate_request_args(
389 builtin.name,
390 module,
391 method,
392 &args,
393 )
394 .map_err(VmError::from)?;
395 let result = handler(vec![request]).await.map_err(VmError::from)?;
396 crate::schemas::validate_response(builtin.name, module, method, result)
397 .map_err(VmError::from)
398 }
399 },
400 );
401 }
402 }
403
404 pub fn builtins(&self) -> &BuiltinRegistry {
407 &self.builtins
408 }
409
410 pub fn modules(&self) -> &[&'static str] {
412 &self.modules
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419
420 #[test]
421 fn unimplemented_builtins_route_through_error() {
422 let mut registry = BuiltinRegistry::new();
423 registry.register_unimplemented("hostlib_demo", "demo", "ping");
424 let entry = registry.find("hostlib_demo").expect("registered");
425 let err = (entry.handler)(&[]).expect_err("should be unimplemented");
426 assert!(
427 matches!(err, HostlibError::Unimplemented { builtin } if builtin == "hostlib_demo")
428 );
429 }
430
431 #[test]
432 fn registry_records_modules_in_order() {
433 struct First;
434 impl HostlibCapability for First {
435 fn module_name(&self) -> &'static str {
436 "first"
437 }
438 fn register_builtins(&self, _registry: &mut BuiltinRegistry) {}
439 }
440 struct Second;
441 impl HostlibCapability for Second {
442 fn module_name(&self) -> &'static str {
443 "second"
444 }
445 fn register_builtins(&self, _registry: &mut BuiltinRegistry) {}
446 }
447
448 let registry = HostlibRegistry::new().with(First).with(Second);
449 assert_eq!(registry.modules(), &["first", "second"]);
450 }
451}