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::{DictMap, 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/// Apply the canonical turn cache without cloning params on ordinary live host
33/// calls. Metadata alone may replace its request with an owned bulk snapshot
34/// request; every other operation keeps the bridge's borrowed fast path.
35pub(super) async fn dispatch_cached(
36    bridge: Arc<dyn HostCallBridge>,
37    capability: &str,
38    operation: &str,
39    params: &DictMap,
40) -> Result<Option<VmValue>, VmError> {
41    if capability == "project" && operation == "metadata_get" {
42        return turn_cache::cached_metadata_or(params, |params: DictMap| async move {
43            bridge.dispatch(capability, operation, &params).await
44        })
45        .await;
46    }
47    turn_cache::cached_or(capability, operation, params, || {
48        bridge.dispatch(capability, operation, params)
49    })
50    .await
51}
52
53/// Embedder-supplied bridge for `host_call` ops.
54///
55/// Embedders (debug adapters, CLIs, IDE hosts) implement this trait to
56/// satisfy capability/operation pairs that harn-vm itself doesn't know how
57/// to handle. Returning `Ok(None)` means "I don't handle this op — fall
58/// through to the built-in fallbacks (env-derived defaults, then the
59/// `unsupported operation` error)". `Ok(Some(value))` is the result;
60/// `Err(VmError::Thrown(_))` surfaces as a Harn exception.
61///
62/// `dispatch` returns a `Send` boxed future so network protocol bridges
63/// (ACP `host/call`, DAP reverse requests) can await without blocking the
64/// caller. Sync bridges can return [`host_call_ready`].
65pub trait HostCallBridge: Send + Sync {
66    fn dispatch<'a>(
67        &'a self,
68        capability: &'a str,
69        operation: &'a str,
70        params: &'a crate::value::DictMap,
71    ) -> HostCallDispatchFuture<'a>;
72
73    fn list_tools(&self) -> Result<Option<VmValue>, VmError> {
74        Ok(None)
75    }
76
77    fn call_tool(&self, _name: &str, _args: &VmValue) -> Result<Option<VmValue>, VmError> {
78        Ok(None)
79    }
80}
81
82thread_local! {
83    pub(super) static HOST_CALL_BRIDGE: RefCell<Option<Arc<dyn HostCallBridge>>> =
84        const { RefCell::new(None) };
85}
86
87/// Install a bridge for the current thread. The bridge is consulted on
88/// every `host_call` *after* mock matching but *before* the built-in
89/// match arms, so embedders can override anything they like (and equally
90/// punt on anything they don't, by returning `Ok(None)`).
91pub fn set_host_call_bridge(bridge: Arc<dyn HostCallBridge>) {
92    turn_cache::reset();
93    HOST_CALL_BRIDGE.with(|b| *b.borrow_mut() = Some(bridge));
94}
95
96/// Install a bridge for one lexical host scope and restore the previous bridge on drop.
97pub fn install_host_call_bridge(bridge: Arc<dyn HostCallBridge>) -> HostCallBridgeGuard {
98    turn_cache::reset();
99    let previous = HOST_CALL_BRIDGE.with(|slot| slot.borrow_mut().replace(bridge));
100    HostCallBridgeGuard {
101        previous,
102        _not_send: PhantomData,
103    }
104}
105
106#[must_use = "dropping the guard restores the previous host-call bridge"]
107pub struct HostCallBridgeGuard {
108    previous: Option<Arc<dyn HostCallBridge>>,
109    // The installed bridge lives in thread-local storage and must be restored
110    // on the same thread.
111    _not_send: PhantomData<Rc<()>>,
112}
113
114impl Drop for HostCallBridgeGuard {
115    fn drop(&mut self) {
116        turn_cache::reset();
117        let previous = self.previous.take();
118        HOST_CALL_BRIDGE.with(|slot| *slot.borrow_mut() = previous);
119    }
120}
121
122/// Remove the current thread's bridge. Idempotent.
123pub fn clear_host_call_bridge() {
124    turn_cache::reset();
125    HOST_CALL_BRIDGE.with(|b| *b.borrow_mut() = None);
126}
127
128/// Dispatch `(capability, operation, params)` to the currently-installed
129/// `HostCallBridge`, if any. `Some(Ok(_))` means the bridge handled the
130/// call; `Some(Err(_))` means it tried but raised; `None` means there is
131/// no bridge or the bridge declined this op (returned `Ok(None)`).
132///
133/// Mirrors the inner block of `dispatch_host_operation` but without the
134/// mock-call check or the built-in fallbacks — useful for callers that
135/// want to treat the bridge as one of several sinks (e.g. inbound MCP
136/// `elicitation/create` requests).
137pub async fn dispatch_host_call_bridge(
138    capability: &str,
139    operation: &str,
140    params: &crate::value::DictMap,
141) -> Option<Result<VmValue, VmError>> {
142    let bridge = HOST_CALL_BRIDGE.with(|b| b.borrow().clone())?;
143    match bridge.dispatch(capability, operation, params).await {
144        Ok(Some(value)) => Some(Ok(value)),
145        Ok(None) => None,
146        Err(error) => Some(Err(error)),
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use crate::value::DictMap;
154
155    struct NamedBridge(&'static str);
156
157    impl HostCallBridge for NamedBridge {
158        fn dispatch<'a>(
159            &'a self,
160            _capability: &'a str,
161            _operation: &'a str,
162            _params: &'a DictMap,
163        ) -> HostCallDispatchFuture<'a> {
164            host_call_ready(Ok(Some(VmValue::String(self.0.into()))))
165        }
166    }
167
168    async fn active_name() -> Option<String> {
169        dispatch_host_call_bridge("test", "name", &DictMap::new())
170            .await?
171            .ok()
172            .and_then(|value| match value {
173                VmValue::String(value) => Some(value.to_string()),
174                _ => None,
175            })
176    }
177
178    #[tokio::test(flavor = "current_thread")]
179    async fn lexical_bridge_install_restores_nested_state() {
180        clear_host_call_bridge();
181        let outer = install_host_call_bridge(Arc::new(NamedBridge("outer")));
182        assert_eq!(active_name().await.as_deref(), Some("outer"));
183        {
184            let _inner = install_host_call_bridge(Arc::new(NamedBridge("inner")));
185            assert_eq!(active_name().await.as_deref(), Some("inner"));
186        }
187        assert_eq!(active_name().await.as_deref(), Some("outer"));
188        drop(outer);
189        assert_eq!(active_name().await, None);
190    }
191}