1use std::collections::BTreeSet;
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::Arc;
13
14use harn_vm::{Vm, VmError, VmValue};
15
16use crate::error::HostlibError;
17
18fn capability_binding(
19 module: &'static str,
20 method: &'static str,
21) -> (harn_builtin_meta::CapabilityId, &'static str) {
22 harn_builtin_meta::host_capabilities::capability_binding_for_schema(module, method)
23 .unwrap_or_else(|| panic!("hostlib schema `{module}.{method}` has no typed capability"))
24}
25
26pub type SyncHandler = Arc<dyn Fn(&[VmValue]) -> Result<VmValue, HostlibError> + Send + Sync>;
30pub type AsyncHandler = Arc<
32 dyn Fn(Vec<VmValue>) -> Pin<Box<dyn Future<Output = Result<VmValue, HostlibError>> + Send>>
33 + Send
34 + Sync,
35>;
36
37struct CancelOnDrop(Option<Arc<AtomicBool>>);
42
43impl Drop for CancelOnDrop {
44 fn drop(&mut self) {
45 if let Some(token) = self.0.take() {
46 token.store(true, Ordering::SeqCst);
47 }
48 }
49}
50
51#[derive(Clone)]
52pub struct RegisteredAsyncBuiltin {
54 pub name: &'static str,
56 pub module: &'static str,
58 pub method: &'static str,
60 pub handler: AsyncHandler,
62}
63
64#[derive(Clone)]
68pub struct RegisteredBuiltin {
69 pub name: &'static str,
71 pub module: &'static str,
73 pub method: &'static str,
75 pub handler: SyncHandler,
77}
78
79impl std::fmt::Debug for RegisteredBuiltin {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 f.debug_struct("RegisteredBuiltin")
82 .field("name", &self.name)
83 .field("module", &self.module)
84 .field("method", &self.method)
85 .finish()
86 }
87}
88
89#[derive(Default)]
91pub struct BuiltinRegistry {
92 builtins: Vec<RegisteredBuiltin>,
93 async_builtins: Vec<RegisteredAsyncBuiltin>,
94 command_policy_builtins: BTreeSet<&'static str>,
95}
96
97impl BuiltinRegistry {
98 pub fn new() -> Self {
100 Self::default()
101 }
102
103 pub fn register(&mut self, builtin: RegisteredBuiltin) {
105 self.builtins.push(builtin);
106 }
107
108 pub fn register_unimplemented(
111 &mut self,
112 name: &'static str,
113 module: &'static str,
114 method: &'static str,
115 ) {
116 let handler: SyncHandler =
117 Arc::new(move |_args| Err(HostlibError::Unimplemented { builtin: name }));
118 self.register(RegisteredBuiltin {
119 name,
120 module,
121 method,
122 handler,
123 });
124 }
125
126 pub(crate) fn register_fn(
130 &mut self,
131 module: &'static str,
132 name: &'static str,
133 method: &'static str,
134 runner: fn(&[VmValue]) -> Result<VmValue, HostlibError>,
135 ) {
136 let handler: SyncHandler = Arc::new(runner);
137 self.register(RegisteredBuiltin {
138 name,
139 module,
140 method,
141 handler,
142 });
143 }
144
145 pub(crate) fn register_command_fn(
148 &mut self,
149 module: &'static str,
150 name: &'static str,
151 method: &'static str,
152 runner: fn(&[VmValue]) -> Result<VmValue, HostlibError>,
153 ) {
154 self.register_fn(module, name, method, runner);
155 self.command_policy_builtins.insert(name);
156 }
157
158 pub(crate) fn register_async_fn<F, Fut>(
159 &mut self,
160 module: &'static str,
161 name: &'static str,
162 method: &'static str,
163 runner: F,
164 ) where
165 F: Fn(Vec<VmValue>) -> Fut + Send + Sync + 'static,
166 Fut: Future<Output = Result<VmValue, HostlibError>> + Send + 'static,
167 {
168 let runner = Arc::new(runner);
169 let handler: AsyncHandler = Arc::new(move |args| Box::pin(runner(args)));
170 self.async_builtins.push(RegisteredAsyncBuiltin {
171 name,
172 module,
173 method,
174 handler,
175 });
176 }
177
178 fn uses_command_policy(&self, name: &str) -> bool {
179 self.command_policy_builtins.contains(name)
180 }
181
182 pub fn iter(&self) -> impl Iterator<Item = &RegisteredBuiltin> {
184 self.builtins.iter()
185 }
186
187 pub fn iter_async(&self) -> impl Iterator<Item = &RegisteredAsyncBuiltin> {
189 self.async_builtins.iter()
190 }
191
192 pub fn len(&self) -> usize {
194 self.builtins.len() + self.async_builtins.len()
195 }
196
197 pub fn is_empty(&self) -> bool {
199 self.builtins.is_empty() && self.async_builtins.is_empty()
200 }
201
202 pub fn find(&self, name: &str) -> Option<&RegisteredBuiltin> {
204 self.builtins.iter().find(|b| b.name == name)
205 }
206
207 pub fn find_async(&self, name: &str) -> Option<&RegisteredAsyncBuiltin> {
209 self.async_builtins.iter().find(|b| b.name == name)
210 }
211}
212
213pub trait HostlibCapability: 'static {
217 fn module_name(&self) -> &'static str;
219
220 fn register_builtins(&self, registry: &mut BuiltinRegistry);
222}
223
224pub struct HostlibRegistry {
230 builtins: BuiltinRegistry,
231 modules: Vec<&'static str>,
232}
233
234impl Default for HostlibRegistry {
235 fn default() -> Self {
236 Self::new()
237 }
238}
239
240impl HostlibRegistry {
241 pub fn new() -> Self {
244 Self {
245 builtins: BuiltinRegistry::new(),
246 modules: Vec::new(),
247 }
248 }
249
250 #[must_use]
252 pub fn with<C: HostlibCapability>(mut self, capability: C) -> Self {
253 let module = capability.module_name();
254 capability.register_builtins(&mut self.builtins);
255 self.modules.push(module);
256 self
257 }
258
259 pub fn register_into_vm(&mut self, vm: &mut Vm) {
261 for builtin in self.builtins.iter().cloned() {
262 let module = builtin.module;
263 let method = builtin.method;
264 let (capability, capability_method) = capability_binding(module, method);
265 harn_vm::stdlib::host::register_callable_host_operation(
266 module,
267 method,
268 "Hostlib schema-backed operation registered at runtime.",
269 );
270 let handler = builtin.handler.clone();
271 if self.builtins.uses_command_policy(builtin.name) {
272 let ambient_name = builtin.name;
273 let policy_handler = Arc::new({
274 let handler = handler.clone();
275 move |ctx: harn_vm::AsyncBuiltinCtx,
276 args: Vec<VmValue>|
277 -> Pin<
278 Box<dyn Future<Output = Result<VmValue, VmError>> + Send>,
279 > {
280 let handler = handler.clone();
281 Box::pin(async move {
282 let request = crate::schemas::validate_request_args(
283 ambient_name,
284 module,
285 method,
286 &args,
287 )
288 .map_err(VmError::from)?;
289 let params = request.as_dict().ok_or_else(|| {
290 VmError::Runtime(format!(
291 "{ambient_name}: validated request must be a dict"
292 ))
293 })?;
294 let caller = serde_json::json!({
295 "surface": "hostlib",
296 "builtin": ambient_name,
297 "module": module,
298 "method": method,
299 "session_id": harn_vm::current_agent_session_id(),
300 });
301 match harn_vm::orchestration::run_command_policy_preflight_with_ctx(
302 Some(&ctx),
303 params,
304 caller,
305 )
306 .await?
307 {
308 harn_vm::orchestration::CommandPolicyPreflight::Blocked {
309 status,
310 message,
311 context,
312 decisions,
313 } => {
314 let response = harn_vm::orchestration::blocked_command_response(
315 params, status, &message, context, decisions,
316 );
317 crate::schemas::validate_response(
318 ambient_name,
319 module,
320 method,
321 crate::tools::policy_blocked_run_command_response(response),
322 )
323 .map_err(VmError::from)
324 }
325 harn_vm::orchestration::CommandPolicyPreflight::Proceed {
326 params,
327 context,
328 decisions,
329 } => {
330 let rewritten = VmValue::dict(params.clone());
334 let validated = crate::schemas::validate_request_args(
335 ambient_name,
336 module,
337 method,
338 &[rewritten],
339 )
340 .map_err(VmError::from)?;
341 let (parent_cancel, deadline) = ctx.interrupt_sources();
342 let cancel = parent_cancel.unwrap_or_else(|| {
343 Arc::new(AtomicBool::new(false))
344 });
345 let mut cancel_on_drop = CancelOnDrop(Some(Arc::clone(&cancel)));
346 let handler_for_blocking = handler.clone();
347 let completed = harn_vm::orchestration::run_blocking_with_ambient(
348 move || {
349 let _interrupt = harn_vm::op_interrupt::install(
350 Some(cancel),
351 deadline,
352 );
353 handler_for_blocking(&[validated]).map_err(VmError::from)
354 },
355 )
356 .await;
357 cancel_on_drop.0 = None;
363 let result = completed
364 .map_err(|error| {
365 VmError::Runtime(format!(
366 "{ambient_name} blocking host operation failed: {error}"
367 ))
368 })??;
369 if crate::tools::run_command_request_is_background(¶ms) {
370 return crate::schemas::validate_response(
371 ambient_name,
372 module,
373 method,
374 result,
375 )
376 .map_err(VmError::from);
377 }
378 let result =
379 harn_vm::orchestration::run_command_policy_postflight_with_ctx(
380 Some(&ctx),
381 ¶ms,
382 result,
383 context,
384 decisions,
385 )
386 .await?;
387 crate::schemas::validate_response(
388 ambient_name,
389 module,
390 method,
391 result,
392 )
393 .map_err(VmError::from)
394 }
395 }
396 })
397 }
398 });
399 let capability_dispatch = Arc::clone(&policy_handler);
400 vm.register_async_capability_method(
401 capability,
402 capability_method,
403 move |ctx, args| capability_dispatch(ctx, args),
404 );
405 if harn_parser::legacy_ambient_capabilities_enabled() {
409 let ambient_dispatch = Arc::clone(&policy_handler);
410 vm.register_async_builtin(ambient_name, move |ctx, args| {
411 ambient_dispatch(ctx, args)
412 });
413 }
414 } else {
415 let ambient_name = builtin.name;
416 let sync_handler = Arc::new({
417 let handler = handler.clone();
418 move |args: &[VmValue], _out: &mut String| -> Result<VmValue, VmError> {
419 let request = crate::schemas::validate_request_args(
420 ambient_name,
421 module,
422 method,
423 args,
424 )
425 .map_err(VmError::from)?;
426 let validated_args = [request];
427 handler(&validated_args).map_err(VmError::from)
428 }
429 });
430 let capability_dispatch = Arc::clone(&sync_handler);
431 vm.register_capability_method(capability, capability_method, move |args, out| {
432 capability_dispatch(args, out)
433 });
434 if harn_parser::legacy_ambient_capabilities_enabled() {
435 let ambient_dispatch = Arc::clone(&sync_handler);
436 vm.register_builtin(ambient_name, move |args, out| ambient_dispatch(args, out));
437 }
438 }
439 }
440 for builtin in self.builtins.async_builtins.iter().cloned() {
441 let module = builtin.module;
442 let method = builtin.method;
443 let (capability, capability_method) = capability_binding(module, method);
444 harn_vm::stdlib::host::register_callable_host_operation(
445 module,
446 method,
447 "Hostlib schema-backed operation registered at runtime.",
448 );
449 let ambient_name = builtin.name;
450 let handler = Arc::new({
451 let handler = builtin.handler.clone();
452 move |_ctx: harn_vm::AsyncBuiltinCtx,
453 args: Vec<VmValue>|
454 -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + Send>> {
455 let handler = handler.clone();
456 Box::pin(async move {
457 let request = crate::schemas::validate_request_args(
458 ambient_name,
459 module,
460 method,
461 &args,
462 )
463 .map_err(VmError::from)?;
464 let result = handler(vec![request]).await.map_err(VmError::from)?;
465 crate::schemas::validate_response(ambient_name, module, method, result)
466 .map_err(VmError::from)
467 })
468 }
469 });
470 let capability_dispatch = Arc::clone(&handler);
471 vm.register_async_capability_method(capability, capability_method, move |ctx, args| {
472 capability_dispatch(ctx, args)
473 });
474 if harn_parser::legacy_ambient_capabilities_enabled() {
475 let ambient_dispatch = Arc::clone(&handler);
476 vm.register_async_builtin(ambient_name, move |ctx, args| {
477 ambient_dispatch(ctx, args)
478 });
479 }
480 }
481 }
482
483 pub fn builtins(&self) -> &BuiltinRegistry {
486 &self.builtins
487 }
488
489 pub fn modules(&self) -> &[&'static str] {
491 &self.modules
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498
499 #[test]
500 fn ambient_bridge_projects_legacy_hostlib_wire_names() {
501 let previous = std::env::var_os(harn_parser::HARN_LEGACY_AMBIENT_CAPABILITIES_ENV);
503 unsafe {
505 std::env::remove_var(harn_parser::HARN_LEGACY_AMBIENT_CAPABILITIES_ENV);
506 }
507 let mut strict_vm = Vm::new();
508 crate::install_default(&mut strict_vm);
509 assert!(
510 strict_vm
511 .builtin_metadata_for("hostlib_tools_run_command")
512 .is_none(),
513 "strict install must keep hostlib wire names off the ambient map"
514 );
515
516 unsafe {
517 std::env::set_var(harn_parser::HARN_LEGACY_AMBIENT_CAPABILITIES_ENV, "1");
518 }
519 let mut ambient_vm = Vm::new();
520 crate::install_default(&mut ambient_vm);
521 assert!(
522 ambient_vm
523 .builtin_metadata_for("hostlib_tools_run_command")
524 .is_some(),
525 "ambient bridge must project hostlib wire names as globals"
526 );
527
528 unsafe {
529 match previous {
530 Some(value) => {
531 std::env::set_var(harn_parser::HARN_LEGACY_AMBIENT_CAPABILITIES_ENV, value);
532 }
533 None => std::env::remove_var(harn_parser::HARN_LEGACY_AMBIENT_CAPABILITIES_ENV),
534 }
535 }
536 }
537
538 #[test]
539 fn unimplemented_builtins_route_through_error() {
540 let mut registry = BuiltinRegistry::new();
541 registry.register_unimplemented("hostlib_demo", "demo", "ping");
542 let entry = registry.find("hostlib_demo").expect("registered");
543 let err = (entry.handler)(&[]).expect_err("should be unimplemented");
544 assert!(
545 matches!(err, HostlibError::Unimplemented { builtin } if builtin == "hostlib_demo")
546 );
547 }
548
549 #[test]
550 fn registry_records_modules_in_order() {
551 struct First;
552 impl HostlibCapability for First {
553 fn module_name(&self) -> &'static str {
554 "first"
555 }
556 fn register_builtins(&self, _registry: &mut BuiltinRegistry) {}
557 }
558 struct Second;
559 impl HostlibCapability for Second {
560 fn module_name(&self) -> &'static str {
561 "second"
562 }
563 fn register_builtins(&self, _registry: &mut BuiltinRegistry) {}
564 }
565
566 let registry = HostlibRegistry::new().with(First).with(Second);
567 assert_eq!(registry.modules(), &["first", "second"]);
568 }
569}