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 let ambient_name = builtin.name;
258 let policy_handler = Arc::new({
259 let handler = handler.clone();
260 move |ctx: harn_vm::AsyncBuiltinCtx,
261 args: Vec<VmValue>|
262 -> Pin<
263 Box<dyn Future<Output = Result<VmValue, VmError>> + Send>,
264 > {
265 let handler = handler.clone();
266 Box::pin(async move {
267 let request = crate::schemas::validate_request_args(
268 ambient_name,
269 module,
270 method,
271 &args,
272 )
273 .map_err(VmError::from)?;
274 let params = request.as_dict().ok_or_else(|| {
275 VmError::Runtime(format!(
276 "{ambient_name}: validated request must be a dict"
277 ))
278 })?;
279 let caller = serde_json::json!({
280 "surface": "hostlib",
281 "builtin": ambient_name,
282 "module": module,
283 "method": method,
284 "session_id": harn_vm::current_agent_session_id(),
285 });
286 match harn_vm::orchestration::run_command_policy_preflight_with_ctx(
287 Some(&ctx),
288 params,
289 caller,
290 )
291 .await?
292 {
293 harn_vm::orchestration::CommandPolicyPreflight::Blocked {
294 status,
295 message,
296 context,
297 decisions,
298 } => {
299 let response = harn_vm::orchestration::blocked_command_response(
300 params, status, &message, context, decisions,
301 );
302 crate::schemas::validate_response(
303 ambient_name,
304 module,
305 method,
306 crate::tools::policy_blocked_run_command_response(response),
307 )
308 .map_err(VmError::from)
309 }
310 harn_vm::orchestration::CommandPolicyPreflight::Proceed {
311 params,
312 context,
313 decisions,
314 } => {
315 let rewritten = VmValue::dict(params.clone());
319 let validated = crate::schemas::validate_request_args(
320 ambient_name,
321 module,
322 method,
323 &[rewritten],
324 )
325 .map_err(VmError::from)?;
326 let result = handler(&[validated]).map_err(VmError::from)?;
327 if crate::tools::run_command_request_is_background(¶ms) {
328 return crate::schemas::validate_response(
329 ambient_name,
330 module,
331 method,
332 result,
333 )
334 .map_err(VmError::from);
335 }
336 let result =
337 harn_vm::orchestration::run_command_policy_postflight_with_ctx(
338 Some(&ctx),
339 ¶ms,
340 result,
341 context,
342 decisions,
343 )
344 .await?;
345 crate::schemas::validate_response(
346 ambient_name,
347 module,
348 method,
349 result,
350 )
351 .map_err(VmError::from)
352 }
353 }
354 })
355 }
356 });
357 let capability_dispatch = Arc::clone(&policy_handler);
358 vm.register_async_capability_method(
359 capability,
360 capability_method,
361 move |ctx, args| capability_dispatch(ctx, args),
362 );
363 if harn_parser::legacy_ambient_capabilities_enabled() {
367 let ambient_dispatch = Arc::clone(&policy_handler);
368 vm.register_async_builtin(ambient_name, move |ctx, args| {
369 ambient_dispatch(ctx, args)
370 });
371 }
372 } else {
373 let ambient_name = builtin.name;
374 let sync_handler = Arc::new({
375 let handler = handler.clone();
376 move |args: &[VmValue], _out: &mut String| -> Result<VmValue, VmError> {
377 let request = crate::schemas::validate_request_args(
378 ambient_name,
379 module,
380 method,
381 args,
382 )
383 .map_err(VmError::from)?;
384 let validated_args = [request];
385 handler(&validated_args).map_err(VmError::from)
386 }
387 });
388 let capability_dispatch = Arc::clone(&sync_handler);
389 vm.register_capability_method(capability, capability_method, move |args, out| {
390 capability_dispatch(args, out)
391 });
392 if harn_parser::legacy_ambient_capabilities_enabled() {
393 let ambient_dispatch = Arc::clone(&sync_handler);
394 vm.register_builtin(ambient_name, move |args, out| ambient_dispatch(args, out));
395 }
396 }
397 }
398 for builtin in self.builtins.async_builtins.iter().cloned() {
399 let module = builtin.module;
400 let method = builtin.method;
401 let (capability, capability_method) = capability_binding(module, method);
402 harn_vm::stdlib::host::register_callable_host_operation(
403 module,
404 method,
405 "Hostlib schema-backed operation registered at runtime.",
406 );
407 let ambient_name = builtin.name;
408 let handler = Arc::new({
409 let handler = builtin.handler.clone();
410 move |_ctx: harn_vm::AsyncBuiltinCtx,
411 args: Vec<VmValue>|
412 -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + Send>> {
413 let handler = handler.clone();
414 Box::pin(async move {
415 let request = crate::schemas::validate_request_args(
416 ambient_name,
417 module,
418 method,
419 &args,
420 )
421 .map_err(VmError::from)?;
422 let result = handler(vec![request]).await.map_err(VmError::from)?;
423 crate::schemas::validate_response(ambient_name, module, method, result)
424 .map_err(VmError::from)
425 })
426 }
427 });
428 let capability_dispatch = Arc::clone(&handler);
429 vm.register_async_capability_method(capability, capability_method, move |ctx, args| {
430 capability_dispatch(ctx, args)
431 });
432 if harn_parser::legacy_ambient_capabilities_enabled() {
433 let ambient_dispatch = Arc::clone(&handler);
434 vm.register_async_builtin(ambient_name, move |ctx, args| {
435 ambient_dispatch(ctx, args)
436 });
437 }
438 }
439 }
440
441 pub fn builtins(&self) -> &BuiltinRegistry {
444 &self.builtins
445 }
446
447 pub fn modules(&self) -> &[&'static str] {
449 &self.modules
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 #[test]
458 fn ambient_bridge_projects_legacy_hostlib_wire_names() {
459 let previous = std::env::var_os(harn_parser::HARN_LEGACY_AMBIENT_CAPABILITIES_ENV);
461 unsafe {
463 std::env::remove_var(harn_parser::HARN_LEGACY_AMBIENT_CAPABILITIES_ENV);
464 }
465 let mut strict_vm = Vm::new();
466 crate::install_default(&mut strict_vm);
467 assert!(
468 strict_vm
469 .builtin_metadata_for("hostlib_tools_run_command")
470 .is_none(),
471 "strict install must keep hostlib wire names off the ambient map"
472 );
473
474 unsafe {
475 std::env::set_var(harn_parser::HARN_LEGACY_AMBIENT_CAPABILITIES_ENV, "1");
476 }
477 let mut ambient_vm = Vm::new();
478 crate::install_default(&mut ambient_vm);
479 assert!(
480 ambient_vm
481 .builtin_metadata_for("hostlib_tools_run_command")
482 .is_some(),
483 "ambient bridge must project hostlib wire names as globals"
484 );
485
486 unsafe {
487 match previous {
488 Some(value) => {
489 std::env::set_var(harn_parser::HARN_LEGACY_AMBIENT_CAPABILITIES_ENV, value);
490 }
491 None => std::env::remove_var(harn_parser::HARN_LEGACY_AMBIENT_CAPABILITIES_ENV),
492 }
493 }
494 }
495
496 #[test]
497 fn unimplemented_builtins_route_through_error() {
498 let mut registry = BuiltinRegistry::new();
499 registry.register_unimplemented("hostlib_demo", "demo", "ping");
500 let entry = registry.find("hostlib_demo").expect("registered");
501 let err = (entry.handler)(&[]).expect_err("should be unimplemented");
502 assert!(
503 matches!(err, HostlibError::Unimplemented { builtin } if builtin == "hostlib_demo")
504 );
505 }
506
507 #[test]
508 fn registry_records_modules_in_order() {
509 struct First;
510 impl HostlibCapability for First {
511 fn module_name(&self) -> &'static str {
512 "first"
513 }
514 fn register_builtins(&self, _registry: &mut BuiltinRegistry) {}
515 }
516 struct Second;
517 impl HostlibCapability for Second {
518 fn module_name(&self) -> &'static str {
519 "second"
520 }
521 fn register_builtins(&self, _registry: &mut BuiltinRegistry) {}
522 }
523
524 let registry = HostlibRegistry::new().with(First).with(Second);
525 assert_eq!(registry.modules(), &["first", "second"]);
526 }
527}