Skip to main content

protocol_droid/application/bridge/
upstream.rs

1//! One upstream per downstream client.
2//!
3//! ## Why a factory rather than a connection
4//!
5//! A bridge proxies. Every client that connects needs a server to be proxied
6//! *to*, and that server has to be its own: a session upstream is bound to one
7//! client, so two clients sharing one connection are two clients inside a
8//! session the upstream believes belongs to a single peer.
9//!
10//! That is invisible on `stdio`, where one process is one client, and it is why
11//! the first version of this bridge opened its upstream once at startup and got
12//! away with it. It is not a property of bridges, it is a property of `stdio`.
13//! The moment a client arrives on a network transport the shape stops working:
14//!
15//! - A frame the server volunteers — `notifications/tools/list_changed` and the
16//!   rest — names no client. Shared, it has nowhere to go that is honest.
17//! - A request the server originates (sampling, elicitation) arrives on the same
18//!   stream with an `id` and a `method`, and has to be answered by the client
19//!   it was addressed to. Shared, there is no way to know which.
20//!
21//! Both are the same bug seen from two sides: one connection serving many
22//! clients has no way to attribute a message to one of them. So
23//! [`Upstream`] is called once per client, and [`run_client`] is the only
24//! supported way to run a loop, which makes the multiplicity impossible to get
25//! wrong by accident.
26//!
27//! ## Why the leg and the peer travel together
28//!
29//! [`OpenedUpstream`] exists to stop a caller opening one connection,
30//! negotiating against another, and forwarding over a third. The result would
31//! be an [`Agreement`](Agreement) describing a server
32//! the bridge is not talking to — and that type exists to be the truth about
33//! two real peers. The pairing is the claim; a `(Leg, Peer)` tuple is two
34//! values that can be mixed, and this is not one of those.
35//!
36//! # Examples
37//!
38//! An upstream that reaches one fixed endpoint, opened once per client:
39//!
40//! ```
41//! use std::future::Future;
42//!
43//! use protocol_droid::application::bridge::{OpenedUpstream, Upstream, UpstreamError};
44//!
45//! /// An upstream backed by whatever "dial and negotiate" means for a caller.
46//! struct FromDial<F> {
47//!     dial: F,
48//! }
49//!
50//! impl<F, Fut> Upstream for FromDial<F>
51//! where
52//!     F: Fn() -> Fut + Send + Sync,
53//!     Fut: Future<Output = Result<OpenedUpstream, UpstreamError>> + Send,
54//! {
55//!     fn open(&self) -> impl Future<Output = Result<OpenedUpstream, UpstreamError>> + Send {
56//!         (self.dial)()
57//!     }
58//! }
59//!
60//! # async fn dial() -> Result<OpenedUpstream, UpstreamError> {
61//! #     Err(UpstreamError::Unreachable("not connected".to_owned()))
62//! # }
63//! # async fn example() -> Result<(), UpstreamError> {
64//! // Called once per client, and every call is expected to be a new connection.
65//! let upstream = FromDial { dial };
66//! let _opened = upstream.open().await?;
67//! # Ok(())
68//! # }
69//! ```
70//!
71//! The closure is where the transport goes, and the one thing to keep straight
72//! inside it: dial, then negotiate against the connection just dialled. Opening
73//! one connection and negotiating over another produces a `Leg` describing a
74//! server the bridge is not talking to, which `OpenedUpstream` cannot express
75//! even if you wanted it to.
76
77use std::sync::Arc;
78
79use crate::application::bridge::{
80    BridgeError, BridgeLoop, BridgePeer, DownstreamConfig, LoopError, LoopReport, ToolResultCodec,
81    Wiring,
82};
83use crate::application::jsonrpc::DynFrameSource;
84use crate::application::session::DynFrameSink;
85use crate::domain::{DegradePolicy, Leg};
86
87/// One upstream connection, and the leg describing it.
88///
89/// Constructed by [`Upstream::open`] rather than by a caller, so the pairing
90/// survives into the wiring instead of being re-established at the call site.
91#[derive(Clone)]
92pub struct OpenedUpstream {
93    leg: Leg,
94    peer: Arc<dyn BridgePeer>,
95}
96
97impl core::fmt::Debug for OpenedUpstream {
98    /// By hand, because a peer is a live connection and not something with a
99    /// readable state. The leg is the claim worth seeing; the peer is the thing
100    /// that makes it true and has no `Debug` to print.
101    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
102        f.debug_struct("OpenedUpstream")
103            .field("leg", &self.leg)
104            .field("peer", &"<a live connection>")
105            .finish()
106    }
107}
108
109impl OpenedUpstream {
110    /// Pairs a negotiated leg with the peer it was negotiated against.
111    #[must_use]
112    pub fn new(leg: Leg, peer: Arc<dyn BridgePeer>) -> Self {
113        Self { leg, peer }
114    }
115
116    /// What the upstream agreed to.
117    #[must_use]
118    pub const fn leg(&self) -> Leg {
119        self.leg
120    }
121
122    /// The connection the leg was negotiated against.
123    #[must_use]
124    pub fn peer(&self) -> &Arc<dyn BridgePeer> {
125        &self.peer
126    }
127
128    /// Splits into the two halves [`Wiring::new`] takes.
129    #[must_use]
130    pub fn into_parts(self) -> (Leg, Arc<dyn BridgePeer>) {
131        (self.leg, self.peer)
132    }
133}
134
135/// Opens upstreams, one per downstream client.
136///
137/// Called once for each client, and each call is expected to produce a fresh
138/// connection. An implementation that hands back the same connection twice
139/// reintroduces exactly the sharing this exists to prevent, and a bridge that
140/// does it will lose a `tools/list_changed` or fail to answer a sampling
141/// request with no error anywhere to show for it.
142pub trait Upstream: Send + Sync {
143    /// Opens and negotiates one upstream.
144    ///
145    /// # Errors
146    ///
147    /// [`UpstreamError`] when the server could not be reached or refused the
148    /// handshake. This is the only failure channel on purpose: a client whose
149    /// server is unreachable has no bridge to be proxied through, so the caller
150    /// answers the client rather than starting a loop that would fail on its
151    /// first frame.
152    fn open(&self) -> impl Future<Output = Result<OpenedUpstream, UpstreamError>> + Send;
153}
154
155/// Why an upstream could not be opened.
156#[derive(Debug, thiserror::Error)]
157pub enum UpstreamError {
158    /// The server could not be reached, or declined the handshake.
159    ///
160    /// A `String` because the transport names its own failures and the
161    /// application layer has no business enumerating them — the same reason
162    /// [`TransportError`](crate::application::client::TransportError) is a
163    /// string.
164    #[error("{0}")]
165    Unreachable(String),
166}
167
168impl From<BridgeError> for UpstreamError {
169    fn from(error: BridgeError) -> Self {
170        Self::Unreachable(error.to_string())
171    }
172}
173
174/// Why one client's bridge stopped.
175#[derive(Debug, thiserror::Error)]
176pub enum BridgeClientError {
177    /// This client's upstream could not be opened.
178    #[error("the upstream could not be opened: {0}")]
179    Upstream(#[from] UpstreamError),
180
181    /// The client's own channel failed.
182    ///
183    /// A refusal the upstream returned is **not** here. Those are answered to
184    /// the client and counted in [`LoopReport::failed`]; only a broken pipe
185    /// ends the loop.
186    #[error("{0}")]
187    Loop(#[from] LoopError),
188}
189
190/// Runs one client from end to end against its own upstream.
191///
192/// The only supported way to run a [`BridgeLoop`] against a caller-chosen
193/// upstream, because it is the only one that guarantees the two halves were
194/// opened together. A caller assembling a `Wiring` by hand can still do it, and
195/// can still get it wrong; this is the shape that cannot be wrong by omission.
196///
197/// # Errors
198///
199/// [`BridgeClientError::Upstream`] when this client's server could not be
200/// reached, and [`BridgeClientError::Loop`] when the client's own channel
201/// fails. The two are kept apart because they mean opposite things to whoever
202/// is serving: the first is a server the bridge cannot reach, the second is a
203/// client that went away mid-conversation.
204pub async fn run_client<U, C, S, K>(
205    upstream: &U,
206    policy: DegradePolicy,
207    config: DownstreamConfig,
208    codec: C,
209    source: S,
210    sink: K,
211) -> Result<LoopReport, BridgeClientError>
212where
213    U: Upstream,
214    C: ToolResultCodec,
215    S: DynFrameSource,
216    K: DynFrameSink,
217{
218    let (leg, peer) = upstream.open().await?.into_parts();
219    let mut bridge_loop =
220        BridgeLoop::new(Wiring::new(leg, policy, peer, codec), config, source, sink);
221    Ok(bridge_loop.run().await?)
222}
223
224#[cfg(test)]
225mod tests {
226    use std::collections::VecDeque;
227    use std::sync::atomic::{AtomicUsize, Ordering};
228    use std::sync::{Arc, Mutex};
229
230    use serde_json::{Map, Value, json};
231
232    use super::{BridgeClientError, OpenedUpstream, Upstream, UpstreamError, run_client};
233    use crate::application::bridge::{
234        BridgeError, BridgePeer, DownstreamConfig, LoopError, LoopReport, SendFuture,
235        ToolResultCodec,
236    };
237    use crate::application::jsonrpc::{DynFrameSource, FrameSource, SourceError};
238    use crate::application::session::{DynFrameSink, FrameSink, SinkError};
239    use crate::domain::feature::{Feature, FeatureProfile};
240    use crate::domain::{Agreement, DegradePolicy, Leg, ProtocolVersion, TransportKind};
241
242    /// Every failure a test here can hit, so `?` works throughout.
243    #[derive(Debug, thiserror::Error)]
244    enum Failure {
245        /// A test double's mutex was poisoned by an earlier panic.
246        #[error("a test double's mutex was poisoned")]
247        Poisoned,
248        /// The bridge could not reach its upstream.
249        #[error(transparent)]
250        Bridge(#[from] BridgeError),
251        /// A client's own loop failed.
252        #[error(transparent)]
253        Client(#[from] BridgeClientError),
254        /// A frame did not parse.
255        #[error(transparent)]
256        Json(#[from] serde_json::Error),
257        /// A downstream leg could not be settled.
258        #[error(transparent)]
259        Downstream(#[from] crate::application::bridge::DownstreamError),
260        /// A test's own complaint.
261        #[error("{0}")]
262        Claim(String),
263    }
264
265    impl From<LoopError> for Failure {
266        fn from(error: LoopError) -> Self {
267            Self::Client(BridgeClientError::Loop(error))
268        }
269    }
270
271    /// Hands out one peer per call, each recording into its own list.
272    ///
273    /// The recording is per peer and shared behind an `Arc`, so a test can read
274    /// "which upstream saw this" after the loop has dropped the peer into the
275    /// live [`Bridge`](crate::application::bridge::Bridge) — which it always
276    /// does, and which is why a double that could only be inspected while
277    /// owned would answer for nobody.
278    #[derive(Default)]
279    struct Factory {
280        opened: AtomicUsize,
281        seen: Arc<Mutex<Vec<Vec<String>>>>,
282        leg: Option<Leg>,
283    }
284
285    impl Factory {
286        fn offering(features: &[Feature]) -> Self {
287            Self {
288                opened: AtomicUsize::new(0),
289                seen: Arc::new(Mutex::new(Vec::new())),
290                leg: Some(*agreeing(features).upstream()),
291            }
292        }
293
294        /// How many clients have been given an upstream.
295        fn opened(&self) -> usize {
296            self.opened.load(Ordering::SeqCst)
297        }
298
299        /// The methods the `n`th upstream was sent.
300        fn methods(&self, nth: usize) -> Result<Vec<String>, Failure> {
301            // Cloned so the guard is gone before the message is built — the
302            // recording is a test's whole, small, and holding a lock across a
303            // `format!` is the kind of thing that deadlocks the next run.
304            let seen = self.seen.lock().map_err(|_| Failure::Poisoned)?.clone();
305            let opened = seen.len();
306            seen.into_iter().nth(nth).ok_or_else(|| {
307                Failure::Claim(format!(
308                    "no upstream was opened at index {nth}; {opened} were opened"
309                ))
310            })
311        }
312    }
313
314    impl Upstream for Factory {
315        /// Not `async`, because nothing here waits: the double hands back a
316        /// peer rather than dialling one, and an `async fn` with no `.await`
317        /// is a future that can never yield.
318        fn open(&self) -> impl Future<Output = Result<OpenedUpstream, UpstreamError>> + Send {
319            // The index is taken before the recording grows, so the two cannot
320            // disagree about which peer is which when a client runs
321            // concurrently.
322            let nth = self.opened.fetch_add(1, Ordering::SeqCst);
323            let Ok(mut seen) = self.seen.lock() else {
324                return std::future::ready(Err(UpstreamError::Unreachable(
325                    "the recording is poisoned".to_owned(),
326                )));
327            };
328            seen.push(Vec::new());
329            drop(seen);
330            let Some(leg) = self.leg else {
331                return std::future::ready(Err(UpstreamError::Unreachable(
332                    "this factory was never given a leg".to_owned(),
333                )));
334            };
335            std::future::ready(Ok(OpenedUpstream::new(
336                leg,
337                Arc::new(Numbered {
338                    nth,
339                    seen: Arc::clone(&self.seen),
340                }),
341            )))
342        }
343    }
344
345    /// One peer, knowing which one it is.
346    struct Numbered {
347        nth: usize,
348        seen: Arc<Mutex<Vec<Vec<String>>>>,
349    }
350
351    impl Numbered {
352        /// Records what arrived, if this peer is still the one at `nth`.
353        fn note(&self, method: &str) -> Result<(), BridgeError> {
354            let Ok(mut seen) = self.seen.lock() else {
355                return Err(BridgeError::Close("the recording is poisoned".to_owned()));
356            };
357            let Some(one) = seen.get_mut(self.nth) else {
358                return Err(BridgeError::Close("this peer has no slot".to_owned()));
359            };
360            one.push(method.to_owned());
361            Ok(())
362        }
363    }
364
365    impl BridgePeer for Numbered {
366        fn send(
367            &self,
368            envelope: Map<String, Value>,
369        ) -> SendFuture<'_, Result<Map<String, Value>, BridgeError>> {
370            Box::pin(async move {
371                if let Some(method) = envelope.get("method").and_then(Value::as_str) {
372                    self.note(method)?;
373                }
374                let id = envelope.get("id").cloned().unwrap_or(Value::Null);
375                Ok(json!({"jsonrpc": "2.0", "id": id, "result": {"ok": true}})
376                    .as_object()
377                    .cloned()
378                    .unwrap_or_default())
379            })
380        }
381
382        fn notify(&self, envelope: Map<String, Value>) -> SendFuture<'_, Result<(), BridgeError>> {
383            Box::pin(async move {
384                if let Some(method) = envelope.get("method").and_then(Value::as_str) {
385                    self.note(method)?;
386                }
387                Ok(())
388            })
389        }
390
391        fn close(&self) -> SendFuture<'_, Result<(), BridgeError>> {
392            Box::pin(async { Ok(()) })
393        }
394    }
395
396    /// Hands over a fixed list of frames, then reports end-of-stream.
397    #[derive(Default)]
398    struct Scripted {
399        frames: Mutex<VecDeque<String>>,
400    }
401
402    impl Scripted {
403        fn of<S: AsRef<str>>(frames: &[S]) -> Self {
404            let queue = frames
405                .iter()
406                .map(|frame| frame.as_ref().to_owned())
407                .collect();
408            Self {
409                frames: Mutex::new(queue),
410            }
411        }
412    }
413
414    impl FrameSource for Scripted {
415        fn next_frame(&self) -> impl Future<Output = Result<Option<String>, SourceError>> + Send {
416            // Not `async`, and deliberately: nothing is awaited, and an `async`
417            // body would hold the guard across a suspension point.
418            let next = self
419                .frames
420                .lock()
421                .map(|mut frames| frames.pop_front())
422                .map_err(|_| SourceError::Transport);
423            std::future::ready(next)
424        }
425    }
426
427    /// Keeps every frame written, so a test can read the wire back.
428    #[derive(Clone, Default)]
429    struct Written {
430        frames: Arc<Mutex<Vec<String>>>,
431    }
432
433    impl FrameSink for Written {
434        fn send(&self, frame: &str) -> impl Future<Output = Result<(), SinkError>> + Send {
435            let pushed = self
436                .frames
437                .lock()
438                .map(|mut frames| frames.push(frame.to_owned()))
439                .map_err(|_| SinkError);
440            std::future::ready(pushed)
441        }
442    }
443
444    fn serving(features: &[Feature]) -> FeatureProfile {
445        features
446            .iter()
447            .fold(FeatureProfile::empty(), |profile, f| profile.with(*f))
448    }
449
450    /// An agreement where both peers offer exactly `features`.
451    ///
452    /// At the newest revision that still has an `initialize` handshake. The
453    /// build's newest revision has none — SEP-2575 removed it — so an agreement
454    /// there could never be settled with a client, and every test built on one
455    /// would be exercising a path no client can take.
456    fn agreeing(features: &[Feature]) -> Agreement {
457        let version = ProtocolVersion::V2025_11_25;
458        Agreement::between(
459            Leg::new(version, serving(features)),
460            Leg::new(version, serving(features)),
461        )
462    }
463
464    /// A client that handshakes and then calls `tools/list`.
465    fn client_frames() -> Vec<String> {
466        vec![
467            format!(
468                r#"{{"jsonrpc":"2.0","id":1,"method":"initialize","params":{{"protocolVersion":"{}","capabilities":{{}},"clientInfo":{{"name":"scripted"}}}}}}"#,
469                ProtocolVersion::V2025_11_25
470            ),
471            r#"{"jsonrpc":"2.0","id":2,"method":"tools/list"}"#.to_owned(),
472        ]
473    }
474
475    async fn run_one(upstream: &Factory) -> Result<(Arc<Mutex<Vec<String>>>, LoopReport), Failure> {
476        let recording: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
477        let report = run_client(
478            upstream,
479            DegradePolicy::default(),
480            DownstreamConfig::new("bridge", "0.1.0", TransportKind::Stdio)?,
481            crate::application::bridge::content::doubles::Counting::reading_nothing(),
482            Scripted::of(&client_frames()),
483            Written {
484                frames: Arc::clone(&recording),
485            },
486        )
487        .await?;
488        Ok((recording, report))
489    }
490
491    /// Two clients, two upstreams, neither sharing.
492    ///
493    /// The property the whole module exists for, and the one `stdio` could never
494    /// test because one process is one client. Run sequentially so the two
495    /// loops cannot interleave and muddy which peer saw what — the assertion is
496    /// about attribution, not about concurrency.
497    #[tokio::test]
498    async fn two_clients_get_two_independent_upstreams() -> Result<(), Failure> {
499        let upstream = Factory::offering(&[Feature::Tools]);
500
501        let (_first_wire, first) = run_one(&upstream).await?;
502        let (second_wire, second) = run_one(&upstream).await?;
503
504        assert_eq!(
505            upstream.opened(),
506            2,
507            "the upstream was not opened per client"
508        );
509        assert!(first.handshaken, "the first client never connected");
510        assert!(second.handshaken, "the second client never connected");
511        assert_eq!(
512            first.forwarded, 1,
513            "the first client's request did not reach its upstream"
514        );
515        assert_eq!(
516            second.forwarded, 1,
517            "the second client's request did not reach its upstream"
518        );
519
520        // The claim: each client reached a *different* upstream, and each saw
521        // only its own request. A shared connection would put both clients'
522        // calls in one peer's recording.
523        assert_eq!(
524            upstream.methods(0)?,
525            vec!["tools/list".to_owned()],
526            "the first upstream saw something other than its own client's call"
527        );
528        assert_eq!(
529            upstream.methods(1)?,
530            vec!["tools/list".to_owned()],
531            "the second upstream saw something other than its own client's call"
532        );
533
534        // And each client got an answer, which a shared connection would also
535        // have let through — so the recording above is what separates them.
536        let wire = second_wire.lock().map_err(|_| Failure::Poisoned)?.clone();
537        assert!(
538            wire.len() >= 2,
539            "the second client was not answered: the bridge dropped a reply rather than \
540             forwarding it to the wrong peer"
541        );
542        Ok(())
543    }
544
545    /// An upstream that cannot be reached fails the client, not the process.
546    ///
547    /// A client whose server is unreachable has no bridge to be proxied
548    /// through. Starting a loop anyway would report the failure as a broken
549    /// client pipe, which is the opposite of what happened.
550    #[tokio::test]
551    async fn an_unreachable_upstream_is_reported_as_such() -> Result<(), Failure> {
552        let outcome = run_client(
553            &Unreachable,
554            DegradePolicy::default(),
555            DownstreamConfig::new("bridge", "0.1.0", TransportKind::Stdio)?,
556            crate::application::bridge::content::doubles::Counting::reading_nothing(),
557            Scripted::of(&client_frames()),
558            Written::default(),
559        )
560        .await;
561
562        assert!(
563            matches!(outcome, Err(BridgeClientError::Upstream(_))),
564            "an unreachable upstream was not reported as one: {outcome:?}"
565        );
566        Ok(())
567    }
568
569    /// The two doubles are the ones the trait promises to accept.
570    ///
571    /// A `Send`/`Sync` slip in either would not show up at the one call site a
572    /// test happens to use, and would surface to the first caller trying to
573    /// spawn the loop — so it is pinned here instead.
574    #[test]
575    fn the_doubles_are_the_bounds_the_traits_promise() {
576        fn upstream<T: Send + Sync + Upstream + ?Sized>() {}
577        fn source<T: DynFrameSource + ?Sized>() {}
578        fn sink<T: DynFrameSink + ?Sized>() {}
579        fn codec<T: ToolResultCodec + ?Sized>() {}
580
581        upstream::<Factory>();
582        upstream::<Unreachable>();
583        source::<Scripted>();
584        sink::<Written>();
585        codec::<crate::application::bridge::content::doubles::Counting>();
586    }
587
588    /// An upstream that refuses, as a type rather than a local one.
589    ///
590    /// A local type inside a test body is fine to construct and awkward to name
591    /// in a bound, and the bound is the claim worth making.
592    struct Unreachable;
593
594    impl Upstream for Unreachable {
595        fn open(&self) -> impl Future<Output = Result<OpenedUpstream, UpstreamError>> + Send {
596            std::future::ready(Err(UpstreamError::Unreachable(
597                "connection refused".to_owned(),
598            )))
599        }
600    }
601}