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
17pub type SyncHandler = Arc<dyn Fn(&[VmValue]) -> Result<VmValue, HostlibError> + Send + Sync>;
21pub type AsyncHandler = Arc<
23 dyn Fn(Vec<VmValue>) -> Pin<Box<dyn Future<Output = Result<VmValue, HostlibError>> + Send>>
24 + Send
25 + Sync,
26>;
27
28#[derive(Clone)]
29pub struct RegisteredAsyncBuiltin {
31 pub name: &'static str,
33 pub module: &'static str,
35 pub method: &'static str,
37 pub handler: AsyncHandler,
39}
40
41#[derive(Clone)]
45pub struct RegisteredBuiltin {
46 pub name: &'static str,
48 pub module: &'static str,
50 pub method: &'static str,
52 pub handler: SyncHandler,
54}
55
56impl std::fmt::Debug for RegisteredBuiltin {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 f.debug_struct("RegisteredBuiltin")
59 .field("name", &self.name)
60 .field("module", &self.module)
61 .field("method", &self.method)
62 .finish()
63 }
64}
65
66#[derive(Default)]
68pub struct BuiltinRegistry {
69 builtins: Vec<RegisteredBuiltin>,
70 async_builtins: Vec<RegisteredAsyncBuiltin>,
71 command_policy_builtins: BTreeSet<&'static str>,
72}
73
74impl BuiltinRegistry {
75 pub fn new() -> Self {
77 Self::default()
78 }
79
80 pub fn register(&mut self, builtin: RegisteredBuiltin) {
82 self.builtins.push(builtin);
83 }
84
85 pub fn register_unimplemented(
88 &mut self,
89 name: &'static str,
90 module: &'static str,
91 method: &'static str,
92 ) {
93 let handler: SyncHandler =
94 Arc::new(move |_args| Err(HostlibError::Unimplemented { builtin: name }));
95 self.register(RegisteredBuiltin {
96 name,
97 module,
98 method,
99 handler,
100 });
101 }
102
103 pub(crate) fn register_fn(
107 &mut self,
108 module: &'static str,
109 name: &'static str,
110 method: &'static str,
111 runner: fn(&[VmValue]) -> Result<VmValue, HostlibError>,
112 ) {
113 let handler: SyncHandler = Arc::new(runner);
114 self.register(RegisteredBuiltin {
115 name,
116 module,
117 method,
118 handler,
119 });
120 }
121
122 pub(crate) fn register_gated_fn(
126 &mut self,
127 module: &'static str,
128 name: &'static str,
129 method: &'static str,
130 runner: fn(&[VmValue]) -> Result<VmValue, HostlibError>,
131 ) {
132 self.register(RegisteredBuiltin {
133 name,
134 module,
135 method,
136 handler: crate::tools::permissions::gated_handler(name, runner),
137 });
138 }
139
140 pub(crate) fn register_gated_command_fn(
143 &mut self,
144 module: &'static str,
145 name: &'static str,
146 method: &'static str,
147 runner: fn(&[VmValue]) -> Result<VmValue, HostlibError>,
148 ) {
149 self.register_gated_fn(module, name, method, runner);
150 self.command_policy_builtins.insert(name);
151 }
152
153 pub(crate) fn register_gated_async_fn<F, Fut>(
154 &mut self,
155 module: &'static str,
156 name: &'static str,
157 method: &'static str,
158 runner: F,
159 ) where
160 F: Fn(Vec<VmValue>) -> Fut + Send + Sync + 'static,
161 Fut: Future<Output = Result<VmValue, HostlibError>> + Send + 'static,
162 {
163 let runner = Arc::new(runner);
164 let handler: AsyncHandler = Arc::new(move |args| {
165 if !crate::tools::permissions::is_enabled(
166 crate::tools::permissions::FEATURE_TOOLS_DETERMINISTIC,
167 ) {
168 return Box::pin(async move {
169 Err(HostlibError::Backend {
170 builtin: name,
171 message: format!(
172 "feature `{}` is not enabled in this session — call \
173 `hostlib_enable(\"{}\")` before invoking this capability",
174 crate::tools::permissions::FEATURE_TOOLS_DETERMINISTIC,
175 crate::tools::permissions::FEATURE_TOOLS_DETERMINISTIC,
176 ),
177 })
178 });
179 }
180 Box::pin(runner(args))
181 });
182 self.async_builtins.push(RegisteredAsyncBuiltin {
183 name,
184 module,
185 method,
186 handler,
187 });
188 }
189
190 fn uses_command_policy(&self, name: &str) -> bool {
191 self.command_policy_builtins.contains(name)
192 }
193
194 pub fn iter(&self) -> impl Iterator<Item = &RegisteredBuiltin> {
196 self.builtins.iter()
197 }
198
199 pub fn iter_async(&self) -> impl Iterator<Item = &RegisteredAsyncBuiltin> {
201 self.async_builtins.iter()
202 }
203
204 pub fn len(&self) -> usize {
206 self.builtins.len() + self.async_builtins.len()
207 }
208
209 pub fn is_empty(&self) -> bool {
211 self.builtins.is_empty() && self.async_builtins.is_empty()
212 }
213
214 pub fn find(&self, name: &str) -> Option<&RegisteredBuiltin> {
216 self.builtins.iter().find(|b| b.name == name)
217 }
218
219 pub fn find_async(&self, name: &str) -> Option<&RegisteredAsyncBuiltin> {
221 self.async_builtins.iter().find(|b| b.name == name)
222 }
223}
224
225pub trait HostlibCapability: 'static {
229 fn module_name(&self) -> &'static str;
231
232 fn register_builtins(&self, registry: &mut BuiltinRegistry);
234}
235
236pub struct HostlibRegistry {
242 builtins: BuiltinRegistry,
243 modules: Vec<&'static str>,
244}
245
246impl Default for HostlibRegistry {
247 fn default() -> Self {
248 Self::new()
249 }
250}
251
252impl HostlibRegistry {
253 pub fn new() -> Self {
256 Self {
257 builtins: BuiltinRegistry::new(),
258 modules: Vec::new(),
259 }
260 }
261
262 #[must_use]
264 pub fn with<C: HostlibCapability>(mut self, capability: C) -> Self {
265 let module = capability.module_name();
266 capability.register_builtins(&mut self.builtins);
267 self.modules.push(module);
268 self
269 }
270
271 pub fn register_into_vm(&mut self, vm: &mut Vm) {
273 for builtin in self.builtins.iter().cloned() {
274 let module = builtin.module;
275 let method = builtin.method;
276 harn_vm::stdlib::host::register_callable_host_operation(
277 module,
278 method,
279 "Hostlib schema-backed operation registered at runtime.",
280 );
281 let handler = builtin.handler.clone();
282 if self.builtins.uses_command_policy(builtin.name) {
283 vm.register_async_builtin(builtin.name, move |ctx, args| {
284 let handler = handler.clone();
285 async move {
286 let request = crate::schemas::validate_request_args(
287 builtin.name,
288 module,
289 method,
290 &args,
291 )
292 .map_err(VmError::from)?;
293 let params = request.as_dict().ok_or_else(|| {
294 VmError::Runtime(format!(
295 "{}: validated request must be a dict",
296 builtin.name
297 ))
298 })?;
299 if let Some(mocked) = harn_vm::stdlib::host::dispatch_mock_hostlib_call(
300 module, method, params,
301 ) {
302 return mocked;
303 }
304 let caller = serde_json::json!({
305 "surface": "hostlib",
306 "builtin": builtin.name,
307 "module": module,
308 "method": method,
309 "session_id": harn_vm::current_agent_session_id(),
310 });
311 match harn_vm::orchestration::run_command_policy_preflight_with_ctx(
312 Some(&ctx),
313 params,
314 caller,
315 )
316 .await?
317 {
318 harn_vm::orchestration::CommandPolicyPreflight::Blocked {
319 status,
320 message,
321 context,
322 decisions,
323 } => {
324 let response = harn_vm::orchestration::blocked_command_response(
325 params, status, &message, context, decisions,
326 );
327 crate::schemas::validate_response(
328 builtin.name,
329 module,
330 method,
331 crate::tools::policy_blocked_run_command_response(response),
332 )
333 .map_err(VmError::from)
334 }
335 harn_vm::orchestration::CommandPolicyPreflight::Proceed {
336 params,
337 context,
338 decisions,
339 } => {
340 let rewritten = VmValue::dict(params.clone());
344 let validated = crate::schemas::validate_request_args(
345 builtin.name,
346 module,
347 method,
348 &[rewritten],
349 )
350 .map_err(VmError::from)?;
351 let result = handler(&[validated]).map_err(VmError::from)?;
352 if crate::tools::run_command_request_is_background(¶ms) {
353 return crate::schemas::validate_response(
354 builtin.name,
355 module,
356 method,
357 result,
358 )
359 .map_err(VmError::from);
360 }
361 let result =
362 harn_vm::orchestration::run_command_policy_postflight_with_ctx(
363 Some(&ctx),
364 ¶ms,
365 result,
366 context,
367 decisions,
368 )
369 .await?;
370 crate::schemas::validate_response(
371 builtin.name,
372 module,
373 method,
374 result,
375 )
376 .map_err(VmError::from)
377 }
378 }
379 }
380 });
381 } else {
382 vm.register_builtin(
383 builtin.name,
384 move |args, _out| -> Result<VmValue, VmError> {
385 let request = crate::schemas::validate_request_args(
386 builtin.name,
387 module,
388 method,
389 args,
390 )
391 .map_err(VmError::from)?;
392 let validated_args = [request.clone()];
393 if let Some(params) = request.as_dict() {
394 if let Some(mocked) = harn_vm::stdlib::host::dispatch_mock_hostlib_call(
395 module, method, params,
396 ) {
397 return mocked;
398 }
399 }
400 handler(&validated_args).map_err(VmError::from)
401 },
402 );
403 }
404 }
405 for builtin in self.builtins.async_builtins.iter().cloned() {
406 let module = builtin.module;
407 let method = builtin.method;
408 harn_vm::stdlib::host::register_callable_host_operation(
409 module,
410 method,
411 "Hostlib schema-backed operation registered at runtime.",
412 );
413 let handler = builtin.handler.clone();
414 vm.register_async_builtin(builtin.name, move |_ctx, args| {
415 let handler = handler.clone();
416 async move {
417 let request =
418 crate::schemas::validate_request_args(builtin.name, module, method, &args)
419 .map_err(VmError::from)?;
420 if let Some(params) = request.as_dict() {
421 if let Some(mocked) = harn_vm::stdlib::host::dispatch_mock_hostlib_call(
422 module, method, params,
423 ) {
424 return mocked;
425 }
426 }
427 let result = handler(vec![request]).await.map_err(VmError::from)?;
428 crate::schemas::validate_response(builtin.name, module, method, result)
429 .map_err(VmError::from)
430 }
431 });
432 }
433 }
434
435 pub fn builtins(&self) -> &BuiltinRegistry {
438 &self.builtins
439 }
440
441 pub fn modules(&self) -> &[&'static str] {
443 &self.modules
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450
451 #[test]
452 fn unimplemented_builtins_route_through_error() {
453 let mut registry = BuiltinRegistry::new();
454 registry.register_unimplemented("hostlib_demo", "demo", "ping");
455 let entry = registry.find("hostlib_demo").expect("registered");
456 let err = (entry.handler)(&[]).expect_err("should be unimplemented");
457 assert!(
458 matches!(err, HostlibError::Unimplemented { builtin } if builtin == "hostlib_demo")
459 );
460 }
461
462 #[test]
463 fn registry_records_modules_in_order() {
464 struct First;
465 impl HostlibCapability for First {
466 fn module_name(&self) -> &'static str {
467 "first"
468 }
469 fn register_builtins(&self, _registry: &mut BuiltinRegistry) {}
470 }
471 struct Second;
472 impl HostlibCapability for Second {
473 fn module_name(&self) -> &'static str {
474 "second"
475 }
476 fn register_builtins(&self, _registry: &mut BuiltinRegistry) {}
477 }
478
479 let registry = HostlibRegistry::new().with(First).with(Second);
480 assert_eq!(registry.modules(), &["first", "second"]);
481 }
482}