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}