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::marker::PhantomData;
6use std::pin::Pin;
7use std::rc::Rc;
8use std::sync::Arc;
9
10use crate::value::{VmError, VmValue};
11
12use super::turn_cache;
13
14/// Boxed future returned by [`HostCallBridge::dispatch`].
15///
16/// Prefer this over `async_trait`: production call sites are already async,
17/// and a hand-rolled boxed future is the smallest seam that lets
18/// `dispatch_host_operation_with_ctx` await a bridge. The future is `Send`
19/// because the stdlib `host_call` builtin is registered through the async
20/// builtin path, which requires `Send` futures even when embedders pin work
21/// to a current-thread `LocalSet`.
22pub type HostCallDispatchFuture<'a> =
23    Pin<Box<dyn Future<Output = Result<Option<VmValue>, VmError>> + Send + 'a>>;
24
25/// Box a ready host-call result for sync bridge implementations.
26pub fn host_call_ready(
27    result: Result<Option<VmValue>, VmError>,
28) -> HostCallDispatchFuture<'static> {
29    Box::pin(async move { result })
30}
31
32/// Embedder-supplied bridge for `host_call` ops.
33///
34/// Embedders (debug adapters, CLIs, IDE hosts) implement this trait to
35/// satisfy capability/operation pairs that harn-vm itself doesn't know how
36/// to handle. Returning `Ok(None)` means "I don't handle this op — fall
37/// through to the built-in fallbacks (env-derived defaults, then the
38/// `unsupported operation` error)". `Ok(Some(value))` is the result;
39/// `Err(VmError::Thrown(_))` surfaces as a Harn exception.
40///
41/// `dispatch` returns a `Send` boxed future so network protocol bridges
42/// (ACP `host/call`, DAP reverse requests) can await without blocking the
43/// caller. Sync bridges can return [`host_call_ready`].
44pub trait HostCallBridge: Send + Sync {
45    fn dispatch<'a>(
46        &'a self,
47        capability: &'a str,
48        operation: &'a str,
49        params: &'a crate::value::DictMap,
50    ) -> HostCallDispatchFuture<'a>;
51
52    fn list_tools(&self) -> Result<Option<VmValue>, VmError> {
53        Ok(None)
54    }
55
56    fn call_tool(&self, _name: &str, _args: &VmValue) -> Result<Option<VmValue>, VmError> {
57        Ok(None)
58    }
59}
60
61thread_local! {
62    pub(super) static HOST_CALL_BRIDGE: RefCell<Option<Arc<dyn HostCallBridge>>> =
63        const { RefCell::new(None) };
64}
65
66/// Install a bridge for the current thread. The bridge is consulted on
67/// every `host_call` *after* mock matching but *before* the built-in
68/// match arms, so embedders can override anything they like (and equally
69/// punt on anything they don't, by returning `Ok(None)`).
70pub fn set_host_call_bridge(bridge: Arc<dyn HostCallBridge>) {
71    turn_cache::reset();
72    HOST_CALL_BRIDGE.with(|b| *b.borrow_mut() = Some(bridge));
73}
74
75/// Install a bridge for one lexical host scope and restore the previous bridge on drop.
76pub fn install_host_call_bridge(bridge: Arc<dyn HostCallBridge>) -> HostCallBridgeGuard {
77    turn_cache::reset();
78    let previous = HOST_CALL_BRIDGE.with(|slot| slot.borrow_mut().replace(bridge));
79    HostCallBridgeGuard {
80        previous,
81        _not_send: PhantomData,
82    }
83}
84
85#[must_use = "dropping the guard restores the previous host-call bridge"]
86pub struct HostCallBridgeGuard {
87    previous: Option<Arc<dyn HostCallBridge>>,
88    // The installed bridge lives in thread-local storage and must be restored
89    // on the same thread.
90    _not_send: PhantomData<Rc<()>>,
91}
92
93impl Drop for HostCallBridgeGuard {
94    fn drop(&mut self) {
95        turn_cache::reset();
96        let previous = self.previous.take();
97        HOST_CALL_BRIDGE.with(|slot| *slot.borrow_mut() = previous);
98    }
99}
100
101/// Remove the current thread's bridge. Idempotent.
102pub fn clear_host_call_bridge() {
103    turn_cache::reset();
104    HOST_CALL_BRIDGE.with(|b| *b.borrow_mut() = None);
105}
106
107/// Dispatch `(capability, operation, params)` to the currently-installed
108/// `HostCallBridge`, if any. `Some(Ok(_))` means the bridge handled the
109/// call; `Some(Err(_))` means it tried but raised; `None` means there is
110/// no bridge or the bridge declined this op (returned `Ok(None)`).
111///
112/// Mirrors the inner block of `dispatch_host_operation` but without the
113/// mock-call check or the built-in fallbacks — useful for callers that
114/// want to treat the bridge as one of several sinks (e.g. inbound MCP
115/// `elicitation/create` requests).
116pub async fn dispatch_host_call_bridge(
117    capability: &str,
118    operation: &str,
119    params: &crate::value::DictMap,
120) -> Option<Result<VmValue, VmError>> {
121    let bridge = HOST_CALL_BRIDGE.with(|b| b.borrow().clone())?;
122    match bridge.dispatch(capability, operation, params).await {
123        Ok(Some(value)) => Some(Ok(value)),
124        Ok(None) => None,
125        Err(error) => Some(Err(error)),
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::value::DictMap;
133
134    struct NamedBridge(&'static str);
135
136    impl HostCallBridge for NamedBridge {
137        fn dispatch<'a>(
138            &'a self,
139            _capability: &'a str,
140            _operation: &'a str,
141            _params: &'a DictMap,
142        ) -> HostCallDispatchFuture<'a> {
143            host_call_ready(Ok(Some(VmValue::String(self.0.into()))))
144        }
145    }
146
147    async fn active_name() -> Option<String> {
148        dispatch_host_call_bridge("test", "name", &DictMap::new())
149            .await?
150            .ok()
151            .and_then(|value| match value {
152                VmValue::String(value) => Some(value.to_string()),
153                _ => None,
154            })
155    }
156
157    #[tokio::test(flavor = "current_thread")]
158    async fn lexical_bridge_install_restores_nested_state() {
159        clear_host_call_bridge();
160        let outer = install_host_call_bridge(Arc::new(NamedBridge("outer")));
161        assert_eq!(active_name().await.as_deref(), Some("outer"));
162        {
163            let _inner = install_host_call_bridge(Arc::new(NamedBridge("inner")));
164            assert_eq!(active_name().await.as_deref(), Some("inner"));
165        }
166        assert_eq!(active_name().await.as_deref(), Some("outer"));
167        drop(outer);
168        assert_eq!(active_name().await, None);
169    }
170}