Skip to main content

harn_vm/stdlib/host/
bridge.rs

1//! Embedder `HostCallBridge` seam for canonical `host_call` dispatch.
2
3use std::cell::RefCell;
4use std::future::Future;
5use std::pin::Pin;
6use std::sync::Arc;
7
8use crate::value::{VmError, VmValue};
9
10use super::turn_cache;
11
12/// Boxed future returned by [`HostCallBridge::dispatch`].
13///
14/// Prefer this over `async_trait`: production call sites are already async,
15/// and a hand-rolled boxed future is the smallest seam that lets
16/// `dispatch_host_operation_with_ctx` await a bridge. The future is `Send`
17/// because the stdlib `host_call` builtin is registered through the async
18/// builtin path, which requires `Send` futures even when embedders pin work
19/// to a current-thread `LocalSet`.
20pub type HostCallDispatchFuture<'a> =
21    Pin<Box<dyn Future<Output = Result<Option<VmValue>, VmError>> + Send + 'a>>;
22
23/// Box a ready host-call result for sync bridge implementations.
24pub fn host_call_ready(
25    result: Result<Option<VmValue>, VmError>,
26) -> HostCallDispatchFuture<'static> {
27    Box::pin(async move { result })
28}
29
30/// Embedder-supplied bridge for `host_call` ops.
31///
32/// Embedders (debug adapters, CLIs, IDE hosts) implement this trait to
33/// satisfy capability/operation pairs that harn-vm itself doesn't know how
34/// to handle. Returning `Ok(None)` means "I don't handle this op — fall
35/// through to the built-in fallbacks (env-derived defaults, then the
36/// `unsupported operation` error)". `Ok(Some(value))` is the result;
37/// `Err(VmError::Thrown(_))` surfaces as a Harn exception.
38///
39/// `dispatch` returns a `Send` boxed future so network protocol bridges
40/// (ACP `host/call`, DAP reverse requests) can await without blocking the
41/// caller. Sync bridges can return [`host_call_ready`].
42pub trait HostCallBridge: Send + Sync {
43    fn dispatch<'a>(
44        &'a self,
45        capability: &'a str,
46        operation: &'a str,
47        params: &'a crate::value::DictMap,
48    ) -> HostCallDispatchFuture<'a>;
49
50    fn list_tools(&self) -> Result<Option<VmValue>, VmError> {
51        Ok(None)
52    }
53
54    fn call_tool(&self, _name: &str, _args: &VmValue) -> Result<Option<VmValue>, VmError> {
55        Ok(None)
56    }
57}
58
59thread_local! {
60    pub(super) static HOST_CALL_BRIDGE: RefCell<Option<Arc<dyn HostCallBridge>>> =
61        const { RefCell::new(None) };
62}
63
64/// Install a bridge for the current thread. The bridge is consulted on
65/// every `host_call` *after* mock matching but *before* the built-in
66/// match arms, so embedders can override anything they like (and equally
67/// punt on anything they don't, by returning `Ok(None)`).
68pub fn set_host_call_bridge(bridge: Arc<dyn HostCallBridge>) {
69    turn_cache::reset();
70    HOST_CALL_BRIDGE.with(|b| *b.borrow_mut() = Some(bridge));
71}
72
73/// Remove the current thread's bridge. Idempotent.
74pub fn clear_host_call_bridge() {
75    turn_cache::reset();
76    HOST_CALL_BRIDGE.with(|b| *b.borrow_mut() = None);
77}
78
79/// Dispatch `(capability, operation, params)` to the currently-installed
80/// `HostCallBridge`, if any. `Some(Ok(_))` means the bridge handled the
81/// call; `Some(Err(_))` means it tried but raised; `None` means there is
82/// no bridge or the bridge declined this op (returned `Ok(None)`).
83///
84/// Mirrors the inner block of `dispatch_host_operation` but without the
85/// mock-call check or the built-in fallbacks — useful for callers that
86/// want to treat the bridge as one of several sinks (e.g. inbound MCP
87/// `elicitation/create` requests).
88pub async fn dispatch_host_call_bridge(
89    capability: &str,
90    operation: &str,
91    params: &crate::value::DictMap,
92) -> Option<Result<VmValue, VmError>> {
93    let bridge = HOST_CALL_BRIDGE.with(|b| b.borrow().clone())?;
94    match bridge.dispatch(capability, operation, params).await {
95        Ok(Some(value)) => Some(Ok(value)),
96        Ok(None) => None,
97        Err(error) => Some(Err(error)),
98    }
99}