Skip to main content

harn_vm/vm/
dispatch.rs

1use std::future::Future;
2use std::sync::Arc;
3
4use crate::value::{ErrorCategory, VmBuiltinFn, VmClosure, VmError, VmValue};
5use crate::BuiltinId;
6
7use super::{
8    CallArgs, ScopeSpan, Vm, VmBuiltinArity, VmBuiltinDispatch, VmBuiltinEntry, VmBuiltinKind,
9    VmBuiltinMetadata,
10};
11
12/// Everything that watches one builtin call, held open for its duration.
13///
14/// A builtin reaches its handler through one of three paths (two sync fast
15/// paths that differ only in where the arguments live, and the async/bridge
16/// path). Each used to open the auto-trace span itself, so an observer added to
17/// one path silently missed the other two — per-builtin cost recording was
18/// added to the async path first and reported nothing, because a `let` binding
19/// in the arm nobody takes looks exactly like a working one.
20///
21/// Opening this is the one thing a dispatch path must do before invoking a
22/// handler. New observers belong here, not at a call site.
23struct BuiltinObservation<'a> {
24    _span: Option<ScopeSpan>,
25    _timer: Option<crate::builtin_profile::BuiltinTimer<'a>>,
26}
27
28impl Vm {
29    fn builtin_span_kind(name: &str) -> Option<crate::tracing::SpanKind> {
30        match name {
31            "llm_call" | "llm_stream" | "llm_stream_call" | "agent_loop" | "agent_turn" => {
32                Some(crate::tracing::SpanKind::LlmCall)
33            }
34            "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
35            _ => None,
36        }
37    }
38
39    /// Open the observation scope for one builtin call. Both members are inert
40    /// unless the operator asked for the corresponding output.
41    ///
42    /// The guard is returned BOXED, and only when something is actually being
43    /// observed. A builtin call reaches the VM recursively (a builtin invokes a
44    /// pipeline that dispatches more builtins), so this guard is a live local on
45    /// every frame of that recursion. Held by value, the ~48-byte aggregate did
46    /// not just add its own size per frame — as a recursive-frame local it
47    /// shifted the compiler's spill/inline decisions, so the real growth
48    /// exceeded `size_of::<BuiltinObservation>()` and overflowed the stack on
49    /// deep dispatch even with profiling OFF (both members `None`). Returning
50    /// `Option<Box<_>>` keeps the frame local pointer-sized (an 8-byte niche
51    /// `None` on the inert hot path, with no allocation) and only touches the
52    /// heap when an observer is genuinely active. See harn#4928.
53    fn observe_builtin_call(name: &str) -> Option<Box<BuiltinObservation<'_>>> {
54        let span = Self::builtin_span_kind(name).map(|kind| ScopeSpan::new(kind, name.to_string()));
55        let timer = crate::builtin_profile::BuiltinTimer::start(name);
56        if span.is_none() && timer.is_none() {
57            // Inert: nothing to observe. No allocation, an 8-byte `None` local.
58            return None;
59        }
60        Some(Box::new(BuiltinObservation {
61            _span: span,
62            _timer: timer,
63        }))
64    }
65
66    fn is_runtime_context_builtin(name: &str) -> bool {
67        matches!(
68            name,
69            "runtime_context"
70                | "task_current"
71                | "runtime_context_values"
72                | "runtime_context_get"
73                | "runtime_context_set"
74                | "runtime_context_clear"
75        )
76    }
77
78    fn resolve_sync_builtin_id_or_name(
79        &self,
80        direct_id: Option<BuiltinId>,
81        name: &str,
82    ) -> Option<Result<VmBuiltinFn, VmError>> {
83        if crate::autonomy::needs_async_side_effect_enforcement(name)
84            || Self::is_runtime_context_builtin(name)
85        {
86            return None;
87        }
88
89        let dispatch = if let Some(id) = direct_id {
90            self.builtins_by_id
91                .get(&id)
92                .filter(|entry| entry.name.as_ref() == name)
93                .map(|entry| entry.dispatch.clone())
94        } else {
95            None
96        }
97        .or_else(|| {
98            self.builtins
99                .get(name)
100                .cloned()
101                .map(VmBuiltinDispatch::Sync)
102        });
103
104        let Some(dispatch) = dispatch else {
105            if self.async_builtins.contains_key(name) || self.bridge.is_some() {
106                return None;
107            }
108            let all_builtins = self
109                .builtins
110                .keys()
111                .chain(self.async_builtins.keys())
112                .map(|s| s.as_str());
113            return Some(
114                if let Some(suggestion) = crate::value::closest_match(name, all_builtins) {
115                    Err(VmError::Runtime(format!(
116                        "Undefined builtin: {name} (did you mean `{suggestion}`?)"
117                    )))
118                } else {
119                    Err(VmError::UndefinedBuiltin(name.to_string()))
120                },
121            );
122        };
123
124        match dispatch {
125            VmBuiltinDispatch::Sync(builtin) => Some(Ok(builtin)),
126            VmBuiltinDispatch::Async(_) => None,
127        }
128    }
129
130    fn validate_sync_builtin_args(&self, name: &str, args: &[VmValue]) -> Result<(), VmError> {
131        if self.denied_builtins.contains(name) {
132            return Err(VmError::CategorizedError {
133                message: format!("Tool '{name}' is not permitted."),
134                category: ErrorCategory::ToolRejected,
135            });
136        }
137        crate::orchestration::enforce_current_policy_for_builtin(name, args)?;
138        self.record_builtin_contract_effects(name, args);
139        crate::typecheck::validate_builtin_call(name, args, None)
140    }
141
142    fn index_builtin_id(&mut self, name: &str, dispatch: VmBuiltinDispatch) {
143        let id = BuiltinId::from_name(name);
144        if self.builtin_id_collisions.contains(&id) {
145            return;
146        }
147        if let Some(existing) = self.builtins_by_id.get(&id) {
148            if existing.name.as_ref() != name {
149                Arc::make_mut(&mut self.builtins_by_id).remove(&id);
150                Arc::make_mut(&mut self.builtin_id_collisions).insert(id);
151                return;
152            }
153        }
154        Arc::make_mut(&mut self.builtins_by_id).insert(
155            id,
156            VmBuiltinEntry {
157                name: std::sync::Arc::from(name),
158                dispatch,
159            },
160        );
161    }
162
163    fn refresh_builtin_id(&mut self, name: &str) {
164        if let Some(builtin) = self.builtins.get(name).cloned() {
165            self.index_builtin_id(name, VmBuiltinDispatch::Sync(builtin));
166        } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
167            self.index_builtin_id(name, VmBuiltinDispatch::Async(async_builtin));
168        } else {
169            let id = BuiltinId::from_name(name);
170            if self
171                .builtins_by_id
172                .get(&id)
173                .is_some_and(|entry| entry.name.as_ref() == name)
174            {
175                Arc::make_mut(&mut self.builtins_by_id).remove(&id);
176            }
177        }
178    }
179
180    /// Register a sync builtin function.
181    pub fn register_builtin<F>(&mut self, name: &str, f: F)
182    where
183        F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + Send + Sync + 'static,
184    {
185        Arc::make_mut(&mut self.builtins).insert(name.to_string(), Arc::new(f));
186        Arc::make_mut(&mut self.builtin_metadata)
187            .insert(name.to_string(), VmBuiltinMetadata::sync(name.to_string()));
188        self.refresh_builtin_id(name);
189    }
190
191    /// Register a dynamically supplied sync builtin with a complete typed
192    /// source exposure/effect contract.
193    pub fn register_builtin_with_contract<F>(
194        &mut self,
195        name: &str,
196        contract: harn_builtin_meta::BuiltinContract,
197        f: F,
198    ) where
199        F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + Send + Sync + 'static,
200    {
201        self.register_builtin_with_metadata(
202            VmBuiltinMetadata::sync(name.to_string()).with_contract(contract),
203            f,
204        );
205    }
206
207    /// Register a sync builtin function with discoverable metadata.
208    pub fn register_builtin_with_metadata<F>(&mut self, metadata: VmBuiltinMetadata, f: F)
209    where
210        F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + Send + Sync + 'static,
211    {
212        let name = metadata.name().to_string();
213        Arc::make_mut(&mut self.builtins).insert(name.clone(), Arc::new(f));
214        Arc::make_mut(&mut self.builtin_metadata)
215            .insert(name.clone(), metadata.with_kind(VmBuiltinKind::Sync));
216        self.refresh_builtin_id(&name);
217    }
218
219    /// Register a `VmBuiltinDef` (the shape emitted by `#[harn_builtin]`).
220    /// Registers the primary name plus each declared alias, sharing the
221    /// same handler. `runtime_only` defs skip the parser-side publish (the
222    /// vm-side registration still happens). `parser_only` defs skip the
223    /// vm-side registration entirely (handler is `None`).
224    pub fn register_builtin_def(&mut self, def: &'static crate::stdlib::macros::VmBuiltinDef) {
225        use crate::stdlib::macros::VmBuiltinHandler;
226        if def.parser_only {
227            return;
228        }
229        // Derive arity from the parsed `BuiltinSignature` so the discoverable
230        // metadata layer (harn explain, alignment-test metadata check) keeps
231        // parity with the pre-macro DSL builder.
232        let arity = arity_from_sig(&def.sig);
233        let names = std::iter::once(def.sig.name).chain(def.aliases.iter().copied());
234        for name in names {
235            match def.handler {
236                VmBuiltinHandler::Sync(f) => {
237                    let meta = builtin_def_metadata(def, name, arity, VmBuiltinKind::Sync);
238                    self.register_builtin_with_metadata(meta, f);
239                }
240                VmBuiltinHandler::Async(f) => {
241                    let meta = builtin_def_metadata(def, name, arity, VmBuiltinKind::Async);
242                    // Wrap the function pointer that already returns an
243                    // AsyncBuiltinFuture so register_async_builtin_with_metadata's
244                    // generic handler/future bounds are met.
245                    self.register_async_builtin_with_metadata(meta, f);
246                }
247                VmBuiltinHandler::None => {
248                    // Parser-only, but reached here despite parser_only=false.
249                    // This is a configuration bug.
250                    panic!(
251                        "VmBuiltinHandler::None for {name:?} without parser_only=true \
252                         on its BuiltinDef"
253                    );
254                }
255            }
256        }
257    }
258
259    /// Project macro-declared Harness methods onto the runtime dispatch table.
260    ///
261    /// Modules may still install a specialized adapter explicitly. For the
262    /// ordinary case, however, the builtin contract is the single source of
263    /// truth: declaring `exposure = "harness.<capability>.<method>"` is enough
264    /// to make that handler callable through the typed capability and never
265    /// creates a second hand-maintained registration list.
266    pub(crate) fn project_declared_capability_methods(&mut self) {
267        use harn_builtin_meta::BuiltinExposure;
268
269        let projections = self
270            .builtin_metadata
271            .iter()
272            .filter_map(|(name, metadata)| {
273                let BuiltinExposure::HarnessMethod { capability, method } =
274                    metadata.contract().exposure
275                else {
276                    return None;
277                };
278                Some((capability, method, name.clone(), metadata.kind()))
279            })
280            .collect::<Vec<_>>();
281
282        for (capability, method, name, kind) in projections {
283            let key = (capability, method.to_string());
284            if self.capability_methods.contains_key(&key) {
285                continue;
286            }
287            let dispatch = match kind {
288                VmBuiltinKind::Sync => self
289                    .builtins
290                    .get(name.as_str())
291                    .cloned()
292                    .map(VmBuiltinDispatch::Sync),
293                VmBuiltinKind::Async => self
294                    .async_builtins
295                    .get(name.as_str())
296                    .cloned()
297                    .map(VmBuiltinDispatch::Async),
298            }
299            .unwrap_or_else(|| {
300                panic!(
301                    "declared capability method harness.{}.{method} has no runtime handler `{name}`",
302                    capability.field_name()
303                )
304            });
305            Arc::make_mut(&mut self.capability_methods).insert(key, dispatch);
306        }
307    }
308
309    /// Remove a sync builtin (so an async version can take precedence).
310    pub fn unregister_builtin(&mut self, name: &str) {
311        Arc::make_mut(&mut self.builtins).remove(name);
312        if self.async_builtins.contains_key(name) {
313            Arc::make_mut(&mut self.builtin_metadata).insert(
314                name.to_string(),
315                VmBuiltinMetadata::async_builtin(name.to_string()),
316            );
317        } else {
318            Arc::make_mut(&mut self.builtin_metadata).remove(name);
319        }
320        self.refresh_builtin_id(name);
321    }
322
323    /// Register an async builtin function. The handler receives the explicit
324    /// [`crate::vm::AsyncBuiltinCtx`] threaded by the dispatch loop.
325    pub fn register_async_builtin<F, Fut>(&mut self, name: &str, f: F)
326    where
327        F: Fn(crate::vm::AsyncBuiltinCtx, Vec<VmValue>) -> Fut + Send + Sync + 'static,
328        Fut: Future<Output = Result<VmValue, VmError>> + Send + 'static,
329    {
330        Arc::make_mut(&mut self.async_builtins).insert(
331            name.to_string(),
332            Arc::new(move |ctx, args| Box::pin(f(ctx, args))),
333        );
334        Arc::make_mut(&mut self.builtin_metadata).insert(
335            name.to_string(),
336            VmBuiltinMetadata::async_builtin(name.to_string()),
337        );
338        self.refresh_builtin_id(name);
339    }
340
341    /// Register a dynamically supplied async builtin with a complete typed
342    /// source exposure/effect contract.
343    pub fn register_async_builtin_with_contract<F, Fut>(
344        &mut self,
345        name: &str,
346        contract: harn_builtin_meta::BuiltinContract,
347        f: F,
348    ) where
349        F: Fn(crate::vm::AsyncBuiltinCtx, Vec<VmValue>) -> Fut + Send + Sync + 'static,
350        Fut: Future<Output = Result<VmValue, VmError>> + Send + 'static,
351    {
352        self.register_async_builtin_with_metadata(
353            VmBuiltinMetadata::async_builtin(name.to_string()).with_contract(contract),
354            f,
355        );
356    }
357
358    /// Register an async builtin function with discoverable metadata. The
359    /// handler receives the explicit [`crate::vm::AsyncBuiltinCtx`].
360    pub fn register_async_builtin_with_metadata<F, Fut>(
361        &mut self,
362        metadata: VmBuiltinMetadata,
363        f: F,
364    ) where
365        F: Fn(crate::vm::AsyncBuiltinCtx, Vec<VmValue>) -> Fut + Send + Sync + 'static,
366        Fut: Future<Output = Result<VmValue, VmError>> + Send + 'static,
367    {
368        let name = metadata.name().to_string();
369        Arc::make_mut(&mut self.async_builtins).insert(
370            name.clone(),
371            Arc::new(move |ctx, args| Box::pin(f(ctx, args))),
372        );
373        Arc::make_mut(&mut self.builtin_metadata)
374            .insert(name.clone(), metadata.with_kind(VmBuiltinKind::Async));
375        self.refresh_builtin_id(&name);
376    }
377
378    /// Install a host implementation behind a typed capability method.
379    ///
380    /// The implementation is deliberately absent from the ordinary builtin
381    /// maps, so registering it cannot create an ambient source-level name.
382    /// The parser-visible signature/effect contract must independently name
383    /// the same `(capability, method)` pair in the builtin manifest.
384    pub fn register_capability_method<F>(
385        &mut self,
386        capability: harn_builtin_meta::CapabilityId,
387        method: &str,
388        f: F,
389    ) where
390        F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + Send + Sync + 'static,
391    {
392        self.insert_capability_method(capability, method, VmBuiltinDispatch::Sync(Arc::new(f)));
393    }
394
395    /// Async counterpart of [`Self::register_capability_method`].
396    pub fn register_async_capability_method<F, Fut>(
397        &mut self,
398        capability: harn_builtin_meta::CapabilityId,
399        method: &str,
400        f: F,
401    ) where
402        F: Fn(crate::vm::AsyncBuiltinCtx, Vec<VmValue>) -> Fut + Send + Sync + 'static,
403        Fut: Future<Output = Result<VmValue, VmError>> + Send + 'static,
404    {
405        self.insert_capability_method(
406            capability,
407            method,
408            VmBuiltinDispatch::Async(Arc::new(move |ctx, args| Box::pin(f(ctx, args)))),
409        );
410    }
411
412    fn insert_capability_method(
413        &mut self,
414        capability: harn_builtin_meta::CapabilityId,
415        method: &str,
416        dispatch: VmBuiltinDispatch,
417    ) {
418        let key = (capability, method.to_string());
419        let replaced = Arc::make_mut(&mut self.capability_methods).insert(key, dispatch);
420        assert!(
421            replaced.is_none(),
422            "capability method harness.{}.{} registered twice",
423            capability.field_name(),
424            method
425        );
426    }
427
428    pub(crate) fn registered_builtin_id(&self, name: &str) -> Option<BuiltinId> {
429        let id = BuiltinId::from_name(name);
430        if self
431            .builtins_by_id
432            .get(&id)
433            .is_some_and(|entry| entry.name.as_ref() == name)
434        {
435            Some(id)
436        } else {
437            None
438        }
439    }
440
441    /// Invoke a closure inline against the existing VM frame stack.
442    ///
443    /// Dispatch path for every callback-taking method on lists/dicts/sets
444    /// (`.map`, `.filter`, `.reduce`, `.each`, `.sort_by`, …) via
445    /// [`call_callable_value`]. The closure's frame is pushed onto
446    /// `self.frames` using the same machinery as `Op::Call`, and the
447    /// shared dispatch loop ([`Vm::drive_until_frame_depth`]) drains the
448    /// sub-execution back to the caller's depth.
449    ///
450    /// This avoids the per-invocation `Pin<Box<dyn Future>>` heap
451    /// allocation a recursive `async fn` would require — the recursion
452    /// cycle (closure → `.map` → callback → closure) is broken instead at
453    /// [`Vm::call_method`], which keeps a single boxed future per
454    /// method-call site rather than per callback element.
455    ///
456    /// Exception handlers are saved and cleared before the sub-execution
457    /// so an unhandled throw inside the body propagates as a Rust
458    /// `Result::Err` to the caller's dispatch loop. Iterators, deadlines,
459    /// and frames are scoped by `CallFrame::saved_iterator_depth` and the
460    /// per-frame deadline tags.
461    pub(crate) async fn call_closure(
462        &mut self,
463        closure: &VmClosure,
464        args: &[VmValue],
465    ) -> Result<VmValue, VmError> {
466        self.call_closure_args(closure, CallArgs::Slice(args)).await
467    }
468
469    pub(crate) async fn call_closure_args(
470        &mut self,
471        closure: &VmClosure,
472        args: CallArgs<'_>,
473    ) -> Result<VmValue, VmError> {
474        let saved_handlers = std::mem::take(&mut self.exception_handlers);
475        let active_context = (!crate::step_runtime::is_tracked_function(&closure.func.name))
476            .then(crate::step_runtime::suspend_active_context);
477
478        let target_frame_depth = self.frames.len();
479        let frame_result = self.push_closure_frame_args(closure, &args);
480        drop(args);
481        let result = match frame_result {
482            Ok(()) => self.drive_until_frame_depth(target_frame_depth).await,
483            Err(e) => Err(e),
484        };
485
486        self.exception_handlers = saved_handlers;
487        drop(active_context);
488
489        result
490    }
491
492    /// Invoke a value as a callable. Supports `VmValue::Closure` and
493    /// `VmValue::BuiltinRef`, so builtin names passed by reference (e.g.
494    /// `dict.rekeyed(snake_to_camel)`) dispatch through the same code path as
495    /// user-defined closures.
496    pub(crate) async fn call_callable_value(
497        &mut self,
498        callable: &VmValue,
499        args: &[VmValue],
500    ) -> Result<VmValue, VmError> {
501        self.call_callable_args(callable, CallArgs::Slice(args))
502            .await
503    }
504
505    pub(crate) async fn call_callable_owned(
506        &mut self,
507        callable: &VmValue,
508        args: Vec<VmValue>,
509    ) -> Result<VmValue, VmError> {
510        self.call_callable_args(callable, CallArgs::Owned(args))
511            .await
512    }
513
514    pub(crate) async fn call_callable_zero(
515        &mut self,
516        callable: &VmValue,
517    ) -> Result<VmValue, VmError> {
518        self.call_callable_args(callable, CallArgs::Empty).await
519    }
520
521    pub(crate) async fn call_callable_one(
522        &mut self,
523        callable: &VmValue,
524        arg: &VmValue,
525    ) -> Result<VmValue, VmError> {
526        self.call_callable_args(callable, CallArgs::One(arg)).await
527    }
528
529    pub(crate) async fn call_callable_two(
530        &mut self,
531        callable: &VmValue,
532        first: &VmValue,
533        second: &VmValue,
534    ) -> Result<VmValue, VmError> {
535        self.call_callable_args(callable, CallArgs::Two(first, second))
536            .await
537    }
538
539    pub(crate) async fn call_callable_args(
540        &mut self,
541        callable: &VmValue,
542        args: CallArgs<'_>,
543    ) -> Result<VmValue, VmError> {
544        match callable {
545            VmValue::Closure(closure) => self.call_closure_args(closure, args).await,
546            VmValue::Dict(registry) => {
547                let handler =
548                    crate::vm::tool_callable::require_single_harn_tool_handler(registry, || {
549                        "expected callable, got dict".to_string()
550                    })?;
551                self.call_closure_args(&handler, args).await
552            }
553            VmValue::BuiltinRef(name) => {
554                if !crate::autonomy::needs_async_side_effect_enforcement(name) {
555                    if let Some(result) = self.call_sync_builtin_by_ref_args(name, &args) {
556                        return result;
557                    }
558                }
559                self.call_named_builtin(name, args.into_vec()).await
560            }
561            VmValue::BuiltinRefId(r) => {
562                if let Some(result) =
563                    self.try_call_sync_builtin_id_or_name_args(Some(r.id), &r.name, &args)
564                {
565                    return result;
566                }
567                self.call_builtin_id_or_name(r.id, &r.name, args.into_vec())
568                    .await
569            }
570            other => Err(VmError::TypeError(format!(
571                "expected callable, got {}",
572                other.type_name()
573            ))),
574        }
575    }
576
577    fn call_sync_builtin_by_ref_args(
578        &mut self,
579        name: &str,
580        args: &CallArgs<'_>,
581    ) -> Option<Result<VmValue, VmError>> {
582        self.try_call_sync_builtin_id_or_name_args(None, name, args)
583    }
584
585    /// Returns true if `v` is callable via `call_callable_value`.
586    pub(crate) fn is_callable_value(v: &VmValue) -> bool {
587        matches!(
588            v,
589            VmValue::Closure(_) | VmValue::BuiltinRef(_) | VmValue::BuiltinRefId(_)
590        ) || crate::vm::tool_callable::is_single_harn_tool_registry_value(v)
591    }
592
593    /// Public wrapper for `call_closure`, used by the MCP server to invoke
594    /// tool handler closures from outside the VM execution loop.
595    pub async fn call_closure_pub(
596        &mut self,
597        closure: &VmClosure,
598        args: &[VmValue],
599    ) -> Result<VmValue, VmError> {
600        self.ensure_execution_available()?;
601        self.cancel_grace_instructions_remaining = None;
602        self.call_closure(closure, args).await
603    }
604
605    /// Resolve a named builtin: sync builtins → async builtins → bridge → error.
606    /// Used by Call, TailCall, and Pipe handlers to avoid duplicating this lookup.
607    pub(crate) async fn call_named_builtin(
608        &mut self,
609        name: &str,
610        args: Vec<VmValue>,
611    ) -> Result<VmValue, VmError> {
612        self.call_builtin_impl(name, args, None, true).await
613    }
614
615    /// Invoke a hidden builtin that implements an already-authorized Harness
616    /// method. The nominal capability contract is the policy/effect owner;
617    /// applying the legacy builtin contract again would duplicate receipts
618    /// and, for approval-gated effects, request human approval twice.
619    pub(in crate::vm) async fn call_capability_builtin(
620        &mut self,
621        name: &str,
622        args: Vec<VmValue>,
623    ) -> Result<VmValue, VmError> {
624        self.call_builtin_impl(name, args, None, false).await
625    }
626
627    /// Invoke a synchronous hidden builtin behind an already-authorized
628    /// Harness method without constructing the recursive async dispatcher.
629    ///
630    /// Capability dispatch has already applied autonomy, policy, and receipt
631    /// handling before reaching this seam. Calling the sync handler directly
632    /// therefore preserves one contract owner and keeps deeply nested
633    /// agent/tool execution from accumulating the much larger async builtin
634    /// frame for ordinary filesystem operations.
635    pub(in crate::vm) fn call_capability_sync_builtin(
636        &mut self,
637        name: &str,
638        args: &[VmValue],
639    ) -> Result<VmValue, VmError> {
640        if self.denied_builtins.contains(name) {
641            return Err(VmError::CategorizedError {
642                message: format!("Tool '{name}' is not permitted."),
643                category: ErrorCategory::ToolRejected,
644            });
645        }
646        let builtin = self
647            .builtins
648            .get(name)
649            .cloned()
650            .ok_or_else(|| VmError::UndefinedBuiltin(name.to_string()))?;
651        let _observe = Self::observe_builtin_call(name);
652        // The Harness method contract has already applied policy and recorded
653        // its typed effects. Only validate the hidden implementation's runtime
654        // argument shape here; `validate_sync_builtin_args` would reapply the
655        // obsolete ambient-builtin policy and can reject an already-approved
656        // capability call.
657        crate::typecheck::validate_builtin_call(name, args, None)?;
658        let _interrupt = self.sync_builtin_interrupt_guard();
659        builtin(args, &mut self.output)
660    }
661
662    pub(crate) async fn call_builtin_id_or_name(
663        &mut self,
664        id: BuiltinId,
665        name: &str,
666        args: Vec<VmValue>,
667    ) -> Result<VmValue, VmError> {
668        self.call_builtin_impl(name, args, Some(id), true).await
669    }
670
671    /// Install the thread-local [`crate::op_interrupt`] context for the
672    /// duration of a sync builtin call, so blocking builtins (subprocess
673    /// waits in particular) can observe scope cancellation and `deadline`
674    /// expiry that the async `tokio::select!` wrapper cannot deliver while
675    /// the op future is stuck inside a synchronous handler. Returns `None`
676    /// (no thread-local traffic) when nothing is armed.
677    pub(in crate::vm) fn sync_builtin_interrupt_guard(
678        &self,
679    ) -> Option<crate::op_interrupt::OpInterruptGuard> {
680        // Mirror `execution.rs::next_deadline`: innermost scope deadline,
681        // tightened by the interrupt-handler deadline when that is sooner.
682        let scope_deadline = self.deadlines.last().map(|(deadline, _)| *deadline);
683        let deadline = match (scope_deadline, self.interrupt_handler_deadline) {
684            (Some(scope), Some(interrupt)) => Some(scope.min(interrupt)),
685            (scope, interrupt) => scope.or(interrupt),
686        };
687        if self.cancel_token.is_none() && deadline.is_none() {
688            return None;
689        }
690        Some(crate::op_interrupt::install(
691            self.cancel_token.clone(),
692            deadline,
693        ))
694    }
695
696    pub(crate) fn try_call_sync_builtin_id_or_name_args(
697        &mut self,
698        direct_id: Option<BuiltinId>,
699        name: &str,
700        args: &CallArgs<'_>,
701    ) -> Option<Result<VmValue, VmError>> {
702        if self.denied_builtins.contains(name) {
703            return Some(Err(VmError::CategorizedError {
704                message: format!("Tool '{name}' is not permitted."),
705                category: ErrorCategory::ToolRejected,
706            }));
707        }
708        let builtin = match self.resolve_sync_builtin_id_or_name(direct_id, name)? {
709            Ok(builtin) => builtin,
710            Err(error) => return Some(Err(error)),
711        };
712        let _observe = Self::observe_builtin_call(name);
713        if let Err(error) = args.with_slice(|slice| self.validate_sync_builtin_args(name, slice)) {
714            return Some(Err(error));
715        }
716
717        let _interrupt = self.sync_builtin_interrupt_guard();
718        Some(args.with_slice(|slice| builtin(slice, &mut self.output)))
719    }
720
721    pub(crate) fn try_call_sync_builtin_id_or_name_from_stack_args(
722        &mut self,
723        direct_id: Option<BuiltinId>,
724        name: &str,
725        args_start: usize,
726    ) -> Option<Result<VmValue, VmError>> {
727        if self.denied_builtins.contains(name) {
728            return Some(Err(VmError::CategorizedError {
729                message: format!("Tool '{name}' is not permitted."),
730                category: ErrorCategory::ToolRejected,
731            }));
732        }
733        let builtin = match self.resolve_sync_builtin_id_or_name(direct_id, name)? {
734            Ok(builtin) => builtin,
735            Err(error) => return Some(Err(error)),
736        };
737        if args_start > self.stack.len() {
738            return Some(Err(VmError::Runtime(
739                "call argument stack underflow".to_string(),
740            )));
741        }
742
743        let _observe = Self::observe_builtin_call(name);
744        if let Err(error) = self.validate_sync_builtin_args(name, &self.stack[args_start..]) {
745            return Some(Err(error));
746        }
747
748        let _interrupt = self.sync_builtin_interrupt_guard();
749        Some(builtin(&self.stack[args_start..], &mut self.output))
750    }
751
752    async fn call_builtin_impl(
753        &mut self,
754        name: &str,
755        args: Vec<VmValue>,
756        direct_id: Option<BuiltinId>,
757        enforce_contract: bool,
758    ) -> Result<VmValue, VmError> {
759        let _observe = Self::observe_builtin_call(name);
760
761        // Sandbox check: deny builtins blocked by --deny/--allow flags.
762        if self.denied_builtins.contains(name) {
763            return Err(VmError::CategorizedError {
764                message: format!("Tool '{name}' is not permitted."),
765                category: ErrorCategory::ToolRejected,
766            });
767        }
768        let autonomy =
769            if enforce_contract && crate::autonomy::needs_async_side_effect_enforcement(name) {
770                crate::autonomy::enforce_builtin_side_effect_boxed(name, &args).await?
771            } else {
772                None
773            };
774        if let Some(crate::autonomy::AutonomyDecision::Skip(value)) = autonomy {
775            return Ok(value);
776        }
777        if enforce_contract {
778            if !matches!(
779                autonomy,
780                Some(crate::autonomy::AutonomyDecision::AllowApproved)
781            ) {
782                crate::orchestration::enforce_current_policy_for_builtin(name, &args)?;
783            }
784            self.record_builtin_contract_effects(name, &args);
785        }
786        crate::typecheck::validate_builtin_call(name, &args, None)?;
787
788        if let Some(id) = direct_id {
789            if let Some(entry) = self.builtins_by_id.get(&id).cloned() {
790                if entry.name.as_ref() == name {
791                    return self.call_builtin_entry(name, entry.dispatch, args).await;
792                }
793            }
794        }
795
796        if let Some(builtin) = self.builtins.get(name).cloned() {
797            self.call_builtin_entry(name, VmBuiltinDispatch::Sync(builtin), args)
798                .await
799        } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
800            self.call_builtin_entry(name, VmBuiltinDispatch::Async(async_builtin), args)
801                .await
802        } else if let Some(bridge) = &self.bridge {
803            if enforce_contract {
804                crate::orchestration::enforce_current_policy_for_bridge_builtin(name)?;
805            }
806            let args_json: Vec<serde_json::Value> =
807                args.iter().map(crate::llm::vm_value_to_json).collect();
808            let result = bridge
809                .call(
810                    "builtin_call",
811                    serde_json::json!({"name": name, "args": args_json}),
812                )
813                .await?;
814            Ok(crate::bridge::json_result_to_vm_value(&result))
815        } else {
816            let all_builtins = self
817                .builtins
818                .keys()
819                .chain(self.async_builtins.keys())
820                .map(|s| s.as_str());
821            if let Some(suggestion) = crate::value::closest_match(name, all_builtins) {
822                return Err(VmError::Runtime(format!(
823                    "Undefined builtin: {name} (did you mean `{suggestion}`?)"
824                )));
825            }
826            Err(VmError::UndefinedBuiltin(name.to_string()))
827        }
828    }
829
830    pub(in crate::vm) async fn call_builtin_entry(
831        &mut self,
832        name: &str,
833        dispatch: VmBuiltinDispatch,
834        args: Vec<VmValue>,
835    ) -> Result<VmValue, VmError> {
836        let result = match dispatch {
837            VmBuiltinDispatch::Sync(builtin) => {
838                let _interrupt = self.sync_builtin_interrupt_guard();
839                builtin(&args, &mut self.output)
840            }
841            VmBuiltinDispatch::Async(async_builtin) => {
842                // Bind a fresh child VM as the async-builtin context for the
843                // duration of this future, threading the explicit ctx handle
844                // into the handler. Drain any output VM-side closures
845                // forwarded into the ctx back to the parent.
846                let (result, captured) =
847                    crate::vm::run_async_builtin_with(self.child_vm_inline(), |ctx| {
848                        async_builtin(ctx, args)
849                    })
850                    .await;
851                if !captured.is_empty() {
852                    self.output.push_str(&captured);
853                }
854                result
855            }
856        }?;
857        if matches!(
858            name,
859            "sync_mutex_acquire"
860                | "sync_semaphore_acquire"
861                | "sync_gate_acquire"
862                | "sync_rwlock_acquire"
863        ) {
864            if let VmValue::SyncPermit(permit) = &result {
865                self.adopt_sync_permit_for_current_scope(permit.as_ref().clone());
866            }
867        }
868        Ok(result)
869    }
870}
871
872/// Build the discoverable [`VmBuiltinMetadata`] for one entry of a
873/// `#[harn_builtin]`-emitted `VmBuiltinDef`, threading the optional
874/// category / doc / signature_text fields without duplicating the chain
875/// across the Sync / Async dispatch arms in `register_builtin_def`.
876fn builtin_def_metadata(
877    def: &'static crate::stdlib::macros::VmBuiltinDef,
878    name: &'static str,
879    arity: VmBuiltinArity,
880    kind: VmBuiltinKind,
881) -> VmBuiltinMetadata {
882    let mut meta = match kind {
883        VmBuiltinKind::Sync => VmBuiltinMetadata::sync_static(name),
884        VmBuiltinKind::Async => VmBuiltinMetadata::async_static(name),
885    }
886    .arity(arity);
887    if let Some(category) = def.category {
888        meta = meta.category_static(category);
889    }
890    if let Some(doc) = def.doc {
891        meta = meta.doc_static(doc);
892    }
893    if let Some(sig_text) = def.signature_text {
894        meta = meta.signature_static(sig_text);
895    } else {
896        // Builtins declared via `sig_expr = …` (a canonical
897        // `harn_builtin_meta::signatures` const) carry no human-typed `sig`
898        // string, so render the parsed signature back through its `Display`
899        // impl. `Display` round-trips through the macro sig grammar (enforced
900        // by the signature-text drift test), so `harn explain` / LSP hover
901        // still surface an accurate, canonical signature.
902        meta = meta.signature_owned(format!("{}", def.sig));
903    }
904    meta.with_contract(def.contract)
905}
906
907/// Derive a [`VmBuiltinArity`] from a parsed [`BuiltinSignature`]. Required
908/// params count toward the floor; optional params and `has_rest` widen the
909/// ceiling. Returns `Variadic` for `(...args: any)`-shaped sigs that have
910/// no required params.
911fn arity_from_sig(sig: &harn_builtin_meta::BuiltinSignature) -> VmBuiltinArity {
912    let required = sig.params.iter().filter(|p| !p.optional).count();
913    let total = sig.params.len();
914    if sig.has_rest {
915        if required == 0 {
916            VmBuiltinArity::Variadic
917        } else {
918            VmBuiltinArity::Min(required)
919        }
920    } else if required == total {
921        VmBuiltinArity::Exact(total)
922    } else {
923        VmBuiltinArity::Range {
924            min: required,
925            max: total,
926        }
927    }
928}