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