protocol_droid/application/bridge/serve.rs
1//! The loop that turns a pair of framed peers into a running bridge.
2//!
3//! [`Bridge::forward`](super::Bridge::forward) moves one envelope. This moves
4//! every envelope, in both directions, until the downstream peer stops talking.
5//!
6//! ## Why a refusal is answered rather than fatal
7//!
8//! The tempting shape is `bridge.forward(..).await?` — one error kills the
9//! loop and the client's socket closes. That reports "the bridge is down" for
10//! a feature the bridge has already explained it cannot serve, and a client
11//! that retries against a perfectly healthy bridge gets the same failure
12//! forever. Every [`BridgeError`] is therefore rendered into a JSON-RPC error
13//! envelope carrying the request's own `id`, and the loop continues. The
14//! client learns which call failed and why, and every other call keeps working.
15//!
16//! ## The same reasoning covers a malformed frame
17//!
18//! A frame that is not JSON is answered `-32700`; a frame that is not a
19//! JSON-RPC message is answered `-32600`. Both carry `id: null`, because a
20//! frame we could not parse has no id to quote. A proxy that died on the first
21//! bad frame from one client would take down every other client with it.
22//!
23//! ## A notification writes nothing
24//!
25//! A conforming peer answers a notification with `202` and no body, so there
26//! is nothing to send back and inventing an empty reply would put a response on
27//! the wire that the server never sent.
28//!
29//! ## Batches
30//!
31//! A frame that is an array is a JSON-RPC batch, and it is answered with a
32//! single array rather than with one frame per element — a client waiting on
33//! one frame would otherwise read the first reply and leave the rest in the
34//! buffer. The one case the specification calls out separately is an
35//! **empty** array: "an Array with at least one value" is the requirement, so
36//! `[]` is a single `-32600`, not an empty array back.
37
38use std::future::Future as _;
39use std::task::Poll;
40
41use serde_json::{Map, Value};
42
43use crate::application::bridge::content::ToolResultCodec;
44use crate::application::bridge::forward::{Bridge, BridgeError, BridgePeer, SendFuture};
45use crate::application::bridge::{DownstreamConfig, negotiate_downstream};
46use crate::application::jsonrpc::{DynFrameSource, SourceError};
47use crate::application::session::{DynFrameSink, SinkError};
48use crate::domain::ir::JsonObject;
49use crate::domain::{Agreement, DegradePolicy, Leg};
50
51/// Which of the two sides had something to say.
52#[derive(Debug)]
53enum Woke {
54 /// The client sent a frame, or `None` once its stream ended.
55 Client(Result<Option<String>, SourceError>),
56 /// The upstream peer has frames parked for the client.
57 Peer,
58}
59
60/// A frame the bridge could not parse at all.
61const PARSE_ERROR: i64 = -32_700;
62/// A frame that parsed but is not a JSON-RPC message.
63const INVALID_REQUEST: i64 = -32_600;
64/// A method the two peers have no shared feature for.
65const METHOD_NOT_FOUND: i64 = -32_601;
66/// A transport or upstream fault.
67const SERVER_ERROR: i64 = -32_000;
68
69/// The handshake request a client opens a connection with.
70///
71/// A constant rather than a literal, because the loop branches on it in two
72/// places — before and after the handshake — and a bridge that answered the
73/// first and forwarded the second would be handing an `InitializeResult` to an
74/// upstream that has no use for it.
75const INITIALIZE: &str = "initialize";
76
77/// The notification that closes a handshake.
78const INITIALIZED: &str = "notifications/initialized";
79
80/// What one pass of the loop moved.
81#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
82pub struct LoopReport {
83 /// Envelopes the upstream peer answered.
84 pub forwarded: u64,
85 /// Envelopes the upstream peer was told about and correctly sent nothing back for.
86 pub notified: u64,
87 /// Envelopes the upstream peer volunteered and the client was told about.
88 ///
89 /// Separate from [`forwarded`](Self::forwarded) and
90 /// [`notified`](Self::notified) because they mean opposite things:
91 /// `forwarded` counts what the bridge carried up and brought back,
92 /// `notified` counts what it carried up and correctly got nothing for, and
93 /// this counts what came *down* with no request behind it at all. A run
94 /// that reports `relayed: 0` against a live session is a bridge that is
95 /// losing half the protocol — the half that is not a request.
96 pub relayed: u64,
97 /// Envelopes the bridge refused or could not read, each answered to the client.
98 pub failed: u64,
99 /// Whether a client completed a handshake.
100 ///
101 /// Separate from the counters because it is the one number that says the
102 /// bridge ever did anything: a run that forwarded nothing and failed
103 /// nothing is either a client that connected and stayed quiet, or a client
104 /// that never arrived, and the report has to tell those apart.
105 pub handshaken: bool,
106}
107
108/// Why the loop stopped.
109#[derive(Debug, thiserror::Error)]
110pub enum LoopError {
111 /// The downstream peer stopped being readable.
112 ///
113 /// A refused envelope is **not** here. Those are answered and counted in
114 /// [`LoopReport::failed`]; only the pipe itself failing ends the loop.
115 #[error("the downstream peer could not be read: {0}")]
116 Source(#[from] SourceError),
117
118 /// A reply did not reach the downstream peer.
119 #[error("the reply could not be written to the downstream peer: {0}")]
120 Sink(#[from] SinkError),
121
122 /// An envelope the bridge itself built could not be written as text.
123 ///
124 /// Every value this loop serialises came out of JSON that already parsed,
125 /// so it cannot happen — but the signature says it can, and an `expect`
126 /// here would be a panic on the proxy's own output rather than on a peer's
127 /// bytes, which is the one place a panic is least defensible.
128 #[error("an envelope could not be written as text: {0}")]
129 Encode(#[from] serde_json::Error),
130
131 /// The upstream peer's out-of-band channel could not be opened.
132 ///
133 /// Its own variant because it is the one failure here that says nothing
134 /// about the downstream pipe: every request already relayed still works, so
135 /// a caller that wanted a degraded bridge can carry on, and one that did not
136 /// now has the name of the thing that broke.
137 #[error("the upstream peer's push channel could not be opened: {0}")]
138 Listen(#[from] BridgeError),
139}
140
141/// What one envelope produced.
142#[derive(Debug)]
143enum Outcome {
144 /// A reply for the downstream peer.
145 Reply(Value),
146 /// The upstream peer had nothing to say, which is correct for a notification.
147 Silent,
148}
149
150/// The two peers a loop forwards between, and the policy it degrades under.
151///
152/// Not a half-built [`Bridge`] and not a way to make one: it is everything
153/// true of a bridge before a client has said anything, gathered so
154/// [`BridgeLoop::new`] takes four arguments rather than eight. The field order
155/// matches [`Bridge::new`] — the same five facts, in the same sequence — so
156/// moving between the two is not a re-reading exercise.
157#[derive(Debug)]
158pub struct Wiring<U, C> {
159 upstream: Leg,
160 policy: DegradePolicy,
161 peer: U,
162 codec: C,
163}
164
165impl<U, C> Wiring<U, C> {
166 /// Wires a bridge to the upstream leg it negotiated when it connected.
167 #[must_use]
168 pub const fn new(upstream: Leg, policy: DegradePolicy, peer: U, codec: C) -> Self {
169 Self {
170 upstream,
171 policy,
172 peer,
173 codec,
174 }
175 }
176}
177
178/// Carries frames from a downstream peer, through a [`Bridge`], back to it.
179///
180/// ## The handshake happens here, not upstream
181///
182/// A bridge is a server to the client in front of it, so the loop answers
183/// `initialize` itself rather than letting the request reach the upstream. That
184/// is what keeps the client on the revision the bridge chose and tells it only
185/// the capabilities the bridge can actually serve; forwarding the handshake
186/// would hand the client the upstream's own answers to both questions.
187///
188/// Before a handshake completes, a request is refused rather than forwarded —
189/// the specification requires it, and a bridge that forwarded one would speak
190/// for a client the upstream had never accepted.
191///
192/// `notifications/initialized` is answered locally and **not** forwarded. The
193/// upstream session was initialised when the bridge connected, before any
194/// client existed; forwarding this one would tell the upstream a second client
195/// had arrived, describing a session it already considers open.
196///
197/// ## What a loop knows before a client arrives
198///
199/// Only one leg. The upstream negotiated with its server when the bridge
200/// connected; the client that supplies the other leg has not shown up yet, and
201/// inventing one would put a second opinion next to the real one. So
202/// [`agreement`](Self::agreement) is `None` until a handshake completes, and a
203/// caller reporting on a bridge can tell "these two sides share nothing" from
204/// "no client ever arrived" — a distinction a placeholder leg would erase.
205///
206/// # Examples
207///
208/// ```
209/// use protocol_droid::application::bridge::{BridgeLoop, ToolResultCodec, Wiring};
210///
211/// # fn documented<H, U, C, S, K>(
212/// # upstream: protocol_droid::domain::Leg,
213/// # config: protocol_droid::application::bridge::DownstreamConfig,
214/// # policy: protocol_droid::domain::DegradePolicy,
215/// # client: H,
216/// # server: U,
217/// # codec: C,
218/// # source: S,
219/// # sink: K,
220/// # ) where
221/// # H: protocol_droid::application::bridge::BridgePeer,
222/// # U: protocol_droid::application::bridge::BridgePeer,
223/// # C: ToolResultCodec,
224/// # S: protocol_droid::application::jsonrpc::DynFrameSource,
225/// # K: protocol_droid::application::session::DynFrameSink,
226/// # {
227/// let wiring = Wiring::new(upstream, policy, server, codec);
228/// let mut bridge_loop = BridgeLoop::new(wiring, config, source, sink);
229/// # let _ = &mut bridge_loop;
230/// # }
231/// ```
232#[derive(Debug)]
233pub struct BridgeLoop<U, C, S, K>
234where
235 U: BridgePeer,
236 C: ToolResultCodec,
237 S: DynFrameSource,
238 K: DynFrameSink,
239{
240 config: DownstreamConfig,
241 wiring: Option<Wiring<U, C>>,
242 live: Option<Box<Bridge<U, C>>>,
243 source: S,
244 sink: K,
245}
246
247impl<U, C, S, K> BridgeLoop<U, C, S, K>
248where
249 U: BridgePeer,
250 C: ToolResultCodec,
251 S: DynFrameSource,
252 K: DynFrameSink,
253{
254 /// A loop waiting for a client's handshake.
255 #[must_use]
256 pub const fn new(wiring: Wiring<U, C>, config: DownstreamConfig, source: S, sink: K) -> Self {
257 Self {
258 config,
259 wiring: Some(wiring),
260 live: None,
261 source,
262 sink,
263 }
264 }
265
266 /// What this bridge knows about its two sides.
267 ///
268 /// `None` until a handshake completes: there is one leg until then, and an
269 /// agreement needs two. Inventing the missing one would put a second,
270 /// fabricated opinion next to the real one.
271 #[must_use]
272 pub fn agreement(&self) -> Option<&Agreement> {
273 self.live.as_deref().map(Bridge::agreement)
274 }
275
276 /// Whether a client has completed a handshake.
277 #[must_use]
278 pub const fn connected(&self) -> bool {
279 self.live.is_some()
280 }
281
282 /// The bridge to forward through, once one exists.
283 fn bridge(&self) -> Option<&Bridge<U, C>> {
284 self.live.as_deref()
285 }
286
287 /// Answers one frame that arrived before any handshake completed.
288 ///
289 /// Two answers, both the specification's: `initialize` settles the leg and
290 /// turns this loop into a live one, and everything else is refused with
291 /// `-32600`. Nothing reaches the upstream, because the upstream has not
292 /// agreed to speak to this client — the one refusal here that is not really
293 /// about the bridge at all.
294 fn handshake(
295 &mut self,
296 envelope: &Map<String, Value>,
297 method: Option<&str>,
298 report: &mut LoopReport,
299 ) -> Outcome {
300 match method {
301 Some(name) if name == INITIALIZE => {}
302 _ => {
303 report.failed = report.failed.saturating_add(1);
304 let id = envelope.get("id").cloned();
305 return Outcome::Reply(error_envelope(
306 id,
307 INVALID_REQUEST,
308 "a client must complete the initialize handshake before sending anything else",
309 ));
310 }
311 }
312
313 // No wiring means the peers were moved into a bridge that this frame
314 // cannot reach, which the dispatch above rules out. Refusing is the
315 // honest answer to a state the loop should never be in, and it keeps a
316 // synthetic agreement out of the code — a made-up upstream would let
317 // this path answer with capabilities no peer ever agreed to.
318 let Some(wiring) = self.wiring.as_ref() else {
319 report.failed = report.failed.saturating_add(1);
320 let id = envelope.get("id").cloned();
321 return Outcome::Reply(error_envelope(
322 id,
323 SERVER_ERROR,
324 "the bridge has no upstream peer to connect a client to",
325 ));
326 };
327
328 let settled = envelope
329 .get("params")
330 .and_then(|value| JsonObject::from_value(value.clone()).ok());
331 let downstream = match settled {
332 Some(params) => negotiate_downstream(¶ms, &wiring.upstream, &self.config),
333 None => Err(crate::application::bridge::DownstreamError::Malformed),
334 };
335
336 match downstream {
337 Ok(downstream) => {
338 let id = envelope.get("id").cloned();
339 let reply = result_envelope(id, downstream.body().as_str());
340 self.establish(&downstream);
341 report.handshaken = true;
342 Outcome::Reply(reply)
343 }
344 Err(error) => {
345 report.failed = report.failed.saturating_add(1);
346 let id = envelope.get("id").cloned();
347 Outcome::Reply(error_envelope(id, INVALID_REQUEST, &error.to_string()))
348 }
349 }
350 }
351
352 /// Turns a settled downstream leg into the bridge this loop forwards through.
353 ///
354 /// The wiring is taken rather than borrowed: `Bridge` owns both peers, and
355 /// a bridge that could forward while its wiring was still held would be two
356 /// owners of one connection. A `None` means a handshake already established
357 /// one, which the caller prevents by routing only un-handshaken frames here
358 /// — so there is nothing to do and nothing to report.
359 fn establish(&mut self, downstream: &crate::application::bridge::Downstream) {
360 let Some(wiring) = self.wiring.take() else {
361 return;
362 };
363 self.live = Some(Box::new(Bridge::new(
364 downstream.agreement().clone(),
365 wiring.policy,
366 wiring.peer,
367 wiring.codec,
368 )));
369 }
370
371 /// Runs until the downstream peer's stream ends.
372 ///
373 /// The handshake is part of this, not a separate step the caller has to
374 /// remember: a caller that forgot it would forward a client's `initialize`
375 /// upstream and answer them with someone else's capabilities.
376 ///
377 /// # Errors
378 ///
379 /// [`LoopError`] when the downstream pipe itself fails, or when a reply the
380 /// bridge built cannot be written. A refusal from the upstream peer is not
381 /// an error here — it is answered to the client and counted in
382 /// [`LoopReport::failed`].
383 pub async fn run(&mut self) -> Result<LoopReport, LoopError> {
384 let mut report = LoopReport::default();
385 // Once per run, and only after a peer exists: asking before the
386 // handshake would be asking a peer that has no session to open a
387 // stream with.
388 let mut listening = false;
389 loop {
390 if !listening && let Some(peer) = self.peer() {
391 peer.listen().await?;
392 listening = true;
393 }
394 // The pair is polled inside a block so the borrow `next_frame` takes
395 // on the source ends before anything below takes `&mut self`. The
396 // block yields *which* future finished, not the work done in
397 // response; a select whose arms called `step` would need the source
398 // and the whole loop at once, and the borrow checker was right to
399 // refuse.
400 let woke = {
401 let mut pending = std::pin::pin!(self.wait_pending());
402 let mut frame = std::pin::pin!(self.source.next_frame());
403 std::future::poll_fn(|cx| {
404 if let Poll::Ready(value) = frame.as_mut().poll(cx) {
405 return Poll::Ready(Woke::Client(value));
406 }
407 if pending.as_mut().poll(cx).is_ready() {
408 return Poll::Ready(Woke::Peer);
409 }
410 Poll::Pending
411 })
412 .await
413 };
414 match woke {
415 Woke::Client(Ok(Some(frame))) => {
416 self.step(&frame, &mut report).await?;
417 self.drain(&mut report).await?;
418 }
419 Woke::Client(Err(error)) => return Err(error.into()),
420 Woke::Client(Ok(None)) => {
421 self.drain(&mut report).await?;
422 break;
423 }
424 Woke::Peer => self.drain(&mut report).await?,
425 }
426 }
427 // The client stopped sending, which says nothing about whether the
428 // server had finished pushing. Whatever arrived between the last
429 // forward and the end of the stream still belongs to the client.
430 self.drain(&mut report).await?;
431 Ok(report)
432 }
433
434 /// Hands the client whatever the upstream pushed on its own.
435 ///
436 /// After each frame rather than alongside it, because the loop reads the
437 /// client on one stream and the peer holds its own — there is no second
438 /// source to select over. The consequence is stated rather than hidden: a
439 /// notification that arrives while the client says nothing waits for the
440 /// client's next frame. Pushing immediately would need the peer's read
441 /// loop to be interruptible, and the two peers that buffer both hold a
442 /// lock across a request for exactly the reason this cannot borrow one
443 /// mid-call.
444 async fn drain(&self, report: &mut LoopReport) -> Result<(), LoopError> {
445 let Some(peer) = self.peer() else {
446 return Ok(());
447 };
448 for envelope in peer.take_pending() {
449 self.write(&Value::Object(envelope)).await?;
450 report.relayed = report.relayed.saturating_add(1);
451 }
452 Ok(())
453 }
454
455 /// A future resolving when the peer has frames parked for the client.
456 ///
457 /// A loop with no peer yet has nothing to wait on, and gets the same
458 /// never-resolving future a peer that opens no standing stream gets.
459 fn wait_pending(&self) -> SendFuture<'static, ()> {
460 self.peer().map_or_else(
461 || Box::pin(std::future::pending()),
462 BridgePeer::wait_pending,
463 )
464 }
465
466 /// The upstream peer, once a client has connected to it.
467 ///
468 /// Through the live [`Bridge`] and not the wiring, which is where the peer
469 /// sits before a handshake completes. That is a deliberate refusal to
470 /// deliver early rather than a convenience: the only drain that runs before
471 /// `establish` is the one after a frame that was *refused* for arriving
472 /// pre-handshake, and handing that client a server notification would push
473 /// a frame at it over a session it has not negotiated — the mirror of the
474 /// rule the loop already applies to requests. A notification the server
475 /// sent before any client arrived is held, not discarded, and goes out as
476 /// soon as one has.
477 fn peer(&self) -> Option<&U> {
478 self.live.as_ref().map(|bridge| bridge.upstream())
479 }
480
481 /// Routes one inbound frame to whatever shape it turned out to have.
482 async fn step(&mut self, frame: &str, report: &mut LoopReport) -> Result<(), LoopError> {
483 let value: Value = match serde_json::from_str(frame) {
484 Ok(value) => value,
485 Err(error) => {
486 report.failed = report.failed.saturating_add(1);
487 return self.refuse(None, PARSE_ERROR, &error.to_string()).await;
488 }
489 };
490
491 match value {
492 Value::Array(members) => self.batch(&members, report).await,
493 Value::Object(envelope) => {
494 let outcome = self.dispatch(&envelope, report).await;
495 self.deliver(outcome).await
496 }
497 // A JSON-RPC message is an object, or an array of them. A number, a
498 // string or `null` is the peer sending something the protocol does
499 // not define, and the specification's answer to that is a refusal.
500 other => {
501 report.failed = report.failed.saturating_add(1);
502 self.refuse(
503 None,
504 INVALID_REQUEST,
505 &format!(
506 "a JSON-RPC message must be an object or an array of them, not {}",
507 kind(&other)
508 ),
509 )
510 .await
511 }
512 }
513 }
514
515 /// Forwards every element and answers with one array.
516 ///
517 /// An empty array is the case the specification separates out: the batch
518 /// requirement is "an Array with at least one value", so `[]` is a single
519 /// invalid-request object rather than an array back — an empty array would
520 /// read to a client as "a batch of zero successful calls".
521 async fn batch(&mut self, members: &[Value], report: &mut LoopReport) -> Result<(), LoopError> {
522 if members.is_empty() {
523 report.failed = report.failed.saturating_add(1);
524 return self
525 .refuse(
526 None,
527 INVALID_REQUEST,
528 "a JSON-RPC batch must carry at least one message",
529 )
530 .await;
531 }
532
533 let mut replies = Vec::new();
534 for member in members {
535 let outcome = match member {
536 Value::Object(envelope) => self.dispatch(envelope, report).await,
537 other => {
538 report.failed = report.failed.saturating_add(1);
539 Outcome::Reply(error_envelope(
540 None,
541 INVALID_REQUEST,
542 &format!(
543 "a JSON-RPC message must be an object or an array of them, not {}",
544 kind(other)
545 ),
546 ))
547 }
548 };
549 if let Outcome::Reply(reply) = outcome {
550 replies.push(reply);
551 }
552 }
553
554 // Every element a notification: the batch has no answers, so it gets
555 // no frame. Writing an empty array here would be the "invent an empty
556 // reply" bug at batch granularity.
557 if replies.is_empty() {
558 return Ok(());
559 }
560 self.write(&Value::Array(replies)).await
561 }
562
563 /// Moves one envelope to the upstream peer and renders what came back.
564 async fn dispatch(
565 &mut self,
566 envelope: &Map<String, Value>,
567 report: &mut LoopReport,
568 ) -> Outcome {
569 let method = envelope.get("method").and_then(Value::as_str);
570
571 // The client's half of a handshake this bridge answered itself. The
572 // upstream session was initialised when the bridge connected, before
573 // any client existed, so forwarding this would tell it a second client
574 // had arrived describing a session it already considers open. Handled
575 // here rather than in the pre-handshake path because this is the order
576 // a conforming client actually sends it in.
577 if method == Some(INITIALIZED) {
578 report.notified = report.notified.saturating_add(1);
579 return Outcome::Silent;
580 }
581
582 if self.live.is_none() {
583 return self.handshake(envelope, method, report);
584 }
585
586 // A second `initialize` is a client that has forgotten it connected,
587 // not a message with anywhere to go. Answering it would renegotiate a
588 // session the bridge has already built a `Bridge` for, and the reply
589 // would promise a profile the running bridge is not using.
590 if method == Some(INITIALIZE) {
591 report.failed = report.failed.saturating_add(1);
592 let id = envelope.get("id").cloned();
593 return Outcome::Reply(error_envelope(
594 id,
595 INVALID_REQUEST,
596 "this connection has already completed its handshake",
597 ));
598 }
599
600 // The only bridge to forward through. `expect`-free: the stage was
601 // matched above and nothing between can have changed it, but a `None`
602 // here is a bug in this function rather than in a peer's bytes, and a
603 // panic in a proxy's own control flow is the least defensible place to
604 // put one.
605 let Some(bridge) = self.bridge() else {
606 report.failed = report.failed.saturating_add(1);
607 let id = envelope.get("id").cloned();
608 return Outcome::Reply(error_envelope(
609 id,
610 SERVER_ERROR,
611 "the bridge is not connected to an upstream peer",
612 ));
613 };
614
615 match bridge.forward(envelope).await {
616 Ok(forwarded) => {
617 if forwarded.reply.is_empty() {
618 report.notified = report.notified.saturating_add(1);
619 return Outcome::Silent;
620 }
621 report.forwarded = report.forwarded.saturating_add(1);
622 Outcome::Reply(Value::Object(forwarded.reply))
623 }
624 Err(error) => {
625 report.failed = report.failed.saturating_add(1);
626 let (code, message) = refusal(&error);
627 let id = envelope.get("id").cloned();
628 Outcome::Reply(error_envelope(id, code, message))
629 }
630 }
631 }
632
633 /// Writes whatever `dispatch` produced, if anything.
634 async fn deliver(&self, outcome: Outcome) -> Result<(), LoopError> {
635 match outcome {
636 Outcome::Reply(reply) => self.write(&reply).await,
637 Outcome::Silent => Ok(()),
638 }
639 }
640
641 async fn write(&self, value: &Value) -> Result<(), LoopError> {
642 self.sink
643 .send(&serde_json::to_string(value)?)
644 .await
645 .map_err(LoopError::from)
646 }
647
648 /// Answers a frame that never became an envelope.
649 async fn refuse(&self, id: Option<Value>, code: i64, message: &str) -> Result<(), LoopError> {
650 self.write(&error_envelope(id, code, message)).await
651 }
652}
653
654/// The JSON-RPC code a bridge-side failure carries downstream.
655///
656/// A refusal is an _answer_, not a failure: the client asked and got a reply.
657/// `-32601` for a withheld feature is deliberate rather than a catch-all,
658/// because it is the code that tells a client "this server does not offer
659/// that", which is the one thing it can act on — retry elsewhere, or offer a
660/// lower revision. `-32000` is reserved for a transport that genuinely failed,
661/// where retrying the same call is the right instinct.
662const fn refusal(error: &BridgeError) -> (i64, &'static str) {
663 match error {
664 BridgeError::FeatureUnavailable { .. } => (
665 METHOD_NOT_FOUND,
666 "the bridge cannot serve this method: the two peers share no feature for it",
667 ),
668 BridgeError::Unreachable { .. } => {
669 (SERVER_ERROR, "the bridge could not reach the upstream peer")
670 }
671 BridgeError::Unreadable(_) => (INVALID_REQUEST, "the envelope is not a JSON-RPC message"),
672 BridgeError::Close(_) => (SERVER_ERROR, "the connection did not close cleanly"),
673 // The same code as a withheld feature, and for the same reason: the
674 // only thing the client can do about either is negotiate a different
675 // revision. A dedicated code would be inventing protocol to express a
676 // distinction no client acts on.
677 BridgeError::NotReadable(_) => (
678 METHOD_NOT_FOUND,
679 "the bridge cannot deliver this result at the revision you negotiated",
680 ),
681 // The same code on the wire as `FeatureUnavailable`; the operator
682 // distinguishes the two by reading the loop's `BridgeReport`, not by
683 // giving them different codes. A dedicated code would invite clients
684 // to write a branch for "not bridged yet" that no client can usefully
685 // act on, and would be inventing protocol to express a follow-up
686 // commit.
687 #[cfg(feature = "a2a")]
688 BridgeError::MethodNotBridged { .. } => (
689 METHOD_NOT_FOUND,
690 "the bridge has no rule for this method on the cross-protocol codec",
691 ),
692 }
693}
694
695/// Builds the result envelope a handshake is answered in.
696///
697/// The counterpart to [`error_envelope`], and deliberately built the same way:
698/// the reply carries the request's own `id`, and `null` where there is none.
699/// `result` is inserted as pre-parsed text rather than as a `Value` so the
700/// negotiated body travels to the client byte-for-byte as the negotiation
701/// built it, instead of through a second serialisation.
702fn result_envelope(id: Option<Value>, body: &str) -> Value {
703 let parsed: Value = serde_json::from_str(body).unwrap_or(Value::Null);
704 let mut envelope = Map::new();
705 envelope.insert("jsonrpc".to_owned(), Value::String("2.0".to_owned()));
706 envelope.insert("id".to_owned(), id.unwrap_or(Value::Null));
707 envelope.insert("result".to_owned(), parsed);
708 Value::Object(envelope)
709}
710
711/// Builds the error envelope a refusal is delivered in.
712///
713/// The `id` is the request's own where there is one, and `null` where there is
714/// not: a frame the bridge could not parse has no id to quote, and a response
715/// carrying someone else's id is worse than one carrying none.
716fn error_envelope(id: Option<Value>, code: i64, message: &str) -> Value {
717 let mut detail = Map::new();
718 detail.insert("code".to_owned(), Value::from(code));
719 detail.insert("message".to_owned(), Value::String(message.to_owned()));
720
721 let mut envelope = Map::new();
722 envelope.insert("jsonrpc".to_owned(), Value::String("2.0".to_owned()));
723 envelope.insert("id".to_owned(), id.unwrap_or(Value::Null));
724 envelope.insert("error".to_owned(), Value::Object(detail));
725 Value::Object(envelope)
726}
727
728/// The JSON type name, for saying what arrived instead of only what did not.
729const fn kind(value: &Value) -> &'static str {
730 match value {
731 Value::Null => "null",
732 Value::Bool(_) => "a boolean",
733 Value::Number(_) => "a number",
734 Value::String(_) => "a string",
735 Value::Array(_) => "an array",
736 Value::Object(_) => "an object",
737 }
738}
739
740#[cfg(test)]
741mod tests {
742 use std::collections::VecDeque;
743 use std::future::Future;
744 use std::sync::{Arc, Mutex};
745
746 use serde_json::{Map, Value, json};
747
748 use crate::application::bridge::BridgeError;
749 use crate::application::bridge::forward::SendFuture;
750 use crate::application::jsonrpc::FrameSource;
751 use crate::application::session::{FrameSink, SinkError};
752 use crate::domain::{
753 Agreement, DegradePolicy, Feature, FeatureProfile, Leg, ProtocolVersion, TransportKind,
754 };
755
756 use super::{BridgeLoop, DownstreamConfig, LoopError, LoopReport, SourceError, Wiring, kind};
757
758 /// Every failure a test can hit, in one type, so `?` works throughout.
759 #[derive(Debug, thiserror::Error)]
760 enum Failure {
761 /// A test double's mutex was poisoned by an earlier panic.
762 #[error("a test double's mutex was poisoned")]
763 Poisoned,
764 /// The bridge refused or could not reach the upstream.
765 #[error(transparent)]
766 Bridge(#[from] BridgeError),
767 /// The loop itself failed.
768 #[error(transparent)]
769 Loop(#[from] LoopError),
770 /// A frame did not parse.
771 #[error(transparent)]
772 Json(#[from] serde_json::Error),
773 /// A downstream leg could not be settled.
774 #[error(transparent)]
775 Downstream(#[from] crate::application::bridge::DownstreamError),
776 /// A test's own complaint.
777 #[error("{0}")]
778 Claim(String),
779 }
780
781 /// An upstream that answers with the request's own id.
782 ///
783 /// The recording is shared rather than owned so a test can read it after the
784 /// loop has dropped the peer into the [`Bridge`]. "What did the upstream
785 /// see" is the whole question several tests here ask, and a double that can
786 /// only be inspected while alive would answer it for nobody.
787 #[derive(Clone, Default)]
788 struct Upstream {
789 seen: Arc<Mutex<Vec<Map<String, Value>>>>,
790 pending: Arc<Mutex<VecDeque<Map<String, Value>>>>,
791 push_on_send: Option<Map<String, Value>>,
792 }
793
794 impl Upstream {
795 /// An upstream and the recording a test keeps hold of.
796 fn recording() -> (Self, Seen) {
797 let seen = Arc::new(Mutex::new(Vec::new()));
798 (
799 Self {
800 seen: Arc::clone(&seen),
801 pending: Arc::new(Mutex::new(VecDeque::new())),
802 push_on_send: None,
803 },
804 Seen(seen),
805 )
806 }
807
808 /// A handle on what this upstream was sent.
809 ///
810 /// The loop moves the peer into the live [`Bridge`] and the test still
811 /// needs to read the recording afterwards, so the handle has to outlive
812 /// the value it came from.
813 fn seen(&self) -> Seen {
814 Seen(Arc::clone(&self.seen))
815 }
816
817 /// An upstream already holding `frame`, before the loop starts.
818 ///
819 /// For the frames a server volunteers rather than answers. Holding one
820 /// before the client has spoken anything is the case only the
821 /// pre-handshake half of the drain can reach, and it is why the drain
822 /// runs after the handshake frame rather than only once a request has
823 /// been forwarded.
824 fn holding(frame: &Map<String, Value>) -> Result<Self, Failure> {
825 let (upstream, _) = Self::recording();
826 upstream.hold(frame)?;
827 Ok(upstream)
828 }
829
830 /// An upstream that holds `frame` every time it answers something.
831 ///
832 /// The mirror image: the frame is pending only once a request has
833 /// already moved the peer out of the wiring and into the live
834 /// [`Bridge`], so draining it proves the loop can still find the peer
835 /// after the handshake moved it.
836 fn pushing_on_send(frame: Map<String, Value>) -> Self {
837 let (upstream, _) = Self::recording();
838 Self {
839 push_on_send: Some(frame),
840 ..upstream
841 }
842 }
843
844 /// Holds `frame` until something drains it.
845 fn hold(&self, frame: &Map<String, Value>) -> Result<(), Failure> {
846 let Ok(mut pending) = self.pending.lock() else {
847 return Err(Failure::Poisoned);
848 };
849 pending.push_back(frame.clone());
850 Ok(())
851 }
852 }
853
854 /// A handle on what an upstream peer was sent.
855 #[derive(Clone)]
856 struct Seen(Arc<Mutex<Vec<Map<String, Value>>>>);
857
858 impl Seen {
859 fn methods(&self) -> Result<Vec<String>, Failure> {
860 let seen = self.0.lock().map_err(|_| Failure::Poisoned)?;
861 Ok(seen
862 .iter()
863 .filter_map(|envelope| envelope.get("method")?.as_str().map(String::from))
864 .collect())
865 }
866
867 fn count(&self) -> Result<usize, Failure> {
868 self.0
869 .lock()
870 .map(|seen| seen.len())
871 .map_err(|_| Failure::Poisoned)
872 }
873 }
874
875 impl crate::application::bridge::BridgePeer for Upstream {
876 fn send(
877 &self,
878 envelope: Map<String, Value>,
879 ) -> SendFuture<'_, Result<Map<String, Value>, BridgeError>> {
880 Box::pin(async move {
881 let id = envelope.get("id").cloned().unwrap_or(Value::Null);
882 let Ok(mut seen) = self.seen.lock() else {
883 return Err(BridgeError::Close("upstream poisoned".to_owned()));
884 };
885 seen.push(envelope);
886 drop(seen);
887 // A server pushing mid-call is why the queue exists: the frame
888 // is held *while* this call is in flight, which is exactly
889 // where the real peers hold theirs.
890 if let Some(frame) = self.push_on_send.clone()
891 && let Ok(mut pending) = self.pending.lock()
892 {
893 pending.push_back(frame);
894 }
895 let mut reply = Map::new();
896 reply.insert("jsonrpc".into(), json!("2.0"));
897 reply.insert("id".into(), id);
898 reply.insert("result".into(), json!({"ok": true}));
899 Ok(reply)
900 })
901 }
902
903 fn notify(&self, envelope: Map<String, Value>) -> SendFuture<'_, Result<(), BridgeError>> {
904 Box::pin(async move {
905 let Ok(mut seen) = self.seen.lock() else {
906 return Err(BridgeError::Close("upstream poisoned".to_owned()));
907 };
908 seen.push(envelope);
909 Ok(())
910 })
911 }
912
913 fn close(&self) -> SendFuture<'_, Result<(), BridgeError>> {
914 Box::pin(async { Ok(()) })
915 }
916
917 /// The frames the server volunteered, which the trait default drops.
918 ///
919 /// Without this the double would agree with the default, and a test
920 /// would pass against a loop that never drained anything — proving
921 /// only that a queue nobody reads is a queue nobody reads.
922 fn take_pending(&self) -> Vec<Map<String, Value>> {
923 let Ok(mut pending) = self.pending.lock() else {
924 return Vec::new();
925 };
926 pending.drain(..).collect()
927 }
928 }
929
930 /// Hands over a fixed list of frames, then reports end-of-stream.
931 #[derive(Default)]
932 struct Scripted {
933 frames: Mutex<VecDeque<String>>,
934 witness: Option<Arc<Mutex<Vec<Vec<String>>>>>,
935 sink: Option<Arc<Mutex<Vec<String>>>>,
936 }
937
938 impl Scripted {
939 fn of<S: AsRef<str>>(frames: &[S]) -> Self {
940 let queue = frames
941 .iter()
942 .map(|frame| frame.as_ref().to_owned())
943 .collect();
944 Self {
945 frames: Mutex::new(queue),
946 witness: None,
947 sink: None,
948 }
949 }
950
951 /// [`of`](Self::of), recording what the client had been sent each time
952 /// it asked for another frame.
953 ///
954 /// The point of the witness is timing. A loop that drains at the very
955 /// end and one that drains after every frame deliver the same frames in
956 /// the same order, so any assertion about the final sink contents calls
957 /// them identical — and a bridge that only drained at the end would
958 /// relay nothing at all for a client that never closes its stream,
959 /// which is every client that stays connected. Reading the sink at the
960 /// moment the client asks its next question is the only way to tell
961 /// them apart.
962 fn witnessed<S: AsRef<str>>(
963 frames: &[S],
964 sink: &Arc<Mutex<Vec<String>>>,
965 ) -> (Self, Arc<Mutex<Vec<Vec<String>>>>) {
966 let witness = Arc::new(Mutex::new(Vec::new()));
967 let mut scripted = Self::of(frames);
968 scripted.witness = Some(Arc::clone(&witness));
969 scripted.sink = Some(Arc::clone(sink));
970 (scripted, witness)
971 }
972
973 /// Records what the client had been sent, as of right now.
974 ///
975 /// Its own function so the guards are dropped before it returns —
976 /// `next_frame` hands back a `Ready`, not a `Result`, so it cannot use
977 /// `?` and must not hold two locks across the record.
978 fn mark(
979 witness: &Mutex<Vec<Vec<String>>>,
980 sink: &Mutex<Vec<String>>,
981 ) -> Result<(), SourceError> {
982 let written = sink.lock().map_err(|_| SourceError::Transport)?.clone();
983 witness
984 .lock()
985 .map_err(|_| SourceError::Transport)?
986 .push(written);
987 Ok(())
988 }
989 }
990
991 impl FrameSource for Scripted {
992 fn next_frame(&self) -> impl Future<Output = Result<Option<String>, SourceError>> + Send {
993 // Not `async`, and deliberately: there is nothing to await, and an
994 // `async` body here holds the guard across a suspension point the
995 // clippy lints would rightly object to. Taking the frame inside
996 // `map` drops the guard before the future is ever built.
997 let next = self
998 .frames
999 .lock()
1000 .map(|mut frames| frames.pop_front())
1001 .map_err(|_| SourceError::Transport);
1002 // After the guard above, so a poisoned witness never leaves the
1003 // sink locked. A failure to record is treated as a failure to read:
1004 // a test that cannot witness the wire cannot be trusted when it
1005 // asserts what the wire carried.
1006 let witnessed = match (&self.witness, &self.sink) {
1007 (Some(witness), Some(sink)) => Self::mark(witness, sink),
1008 _ => Ok(()),
1009 };
1010 std::future::ready(witnessed.and(next))
1011 }
1012 }
1013
1014 /// Keeps every frame written, so a test can read the wire back.
1015 ///
1016 /// The recording is behind an `Arc` rather than the sink itself, because
1017 /// `Arc<Written>` satisfies neither `FrameSink` nor `DynFrameSink` — the
1018 /// blanket impls cover `T: FrameSink`, and an `Arc` is not one.
1019 #[derive(Clone, Default)]
1020 struct Written {
1021 frames: Arc<Mutex<Vec<String>>>,
1022 }
1023
1024 impl FrameSink for Written {
1025 fn send(&self, frame: &str) -> impl Future<Output = Result<(), SinkError>> + Send {
1026 let pushed = self
1027 .frames
1028 .lock()
1029 .map(|mut frames| frames.push(frame.to_owned()))
1030 .map_err(|_| SinkError);
1031 std::future::ready(pushed)
1032 }
1033 }
1034
1035 fn serving(features: &[Feature]) -> FeatureProfile {
1036 features
1037 .iter()
1038 .fold(FeatureProfile::empty(), |profile, f| profile.with(*f))
1039 }
1040
1041 /// An agreement where both peers offer exactly `features`.
1042 ///
1043 /// At the newest revision that still has an `initialize` handshake. The
1044 /// build's newest revision has none — SEP-2575 removed it — so an
1045 /// agreement there could never be settled with a client, and every test
1046 /// built on one would be exercising a path no client can take.
1047 fn agreeing(features: &[Feature]) -> Agreement {
1048 let version = ProtocolVersion::V2025_11_25;
1049 Agreement::between(
1050 Leg::new(version, serving(features)),
1051 Leg::new(version, serving(features)),
1052 )
1053 }
1054
1055 fn written(recording: &Arc<Mutex<Vec<String>>>) -> Result<Vec<Value>, Failure> {
1056 let frames = recording.lock().map_err(|_| Failure::Poisoned)?;
1057 frames
1058 .iter()
1059 .map(|frame| Ok(serde_json::from_str(frame)?))
1060 .collect()
1061 }
1062
1063 /// The `initialize` a scripted client opens with.
1064 fn handshake_frame(version: ProtocolVersion) -> String {
1065 format!(
1066 r#"{{"jsonrpc":"2.0","id":1,"method":"initialize","params":{{"protocolVersion":"{version}","capabilities":{{}},"clientInfo":{{"name":"scripted"}}}}}}"#
1067 )
1068 }
1069
1070 /// Everything one scripted run produced.
1071 struct Session {
1072 report: LoopReport,
1073 /// Every frame written, the handshake reply first when one happened.
1074 written: Vec<Value>,
1075 /// What the upstream peer was sent.
1076 seen: Seen,
1077 }
1078
1079 impl Session {
1080 /// The replies to the test's own frames, without the handshake's.
1081 fn after_handshake(self) -> Vec<Value> {
1082 self.written.into_iter().skip(1).collect()
1083 }
1084 }
1085
1086 /// Runs one loop over `frames`, behind an opening handshake unless
1087 /// `handshake` says otherwise.
1088 async fn serve(
1089 agreement: &Agreement,
1090 frames: &[&str],
1091 handshake: bool,
1092 ) -> Result<Session, Failure> {
1093 let (upstream, _) = Upstream::recording();
1094 serve_with(agreement, frames, handshake, upstream).await
1095 }
1096
1097 /// [`serve`], with the upstream supplied rather than built.
1098 ///
1099 /// The peer a test needs to reason about is not always a plain recorder: a
1100 /// test about frames the server pushes needs one that holds them, and
1101 /// building that into `serve` would mean every test opting out of a
1102 /// default it has no use for.
1103 async fn serve_with(
1104 agreement: &Agreement,
1105 frames: &[&str],
1106 handshake: bool,
1107 upstream: Upstream,
1108 ) -> Result<Session, Failure> {
1109 serve_witnessed(agreement, frames, handshake, upstream)
1110 .await
1111 .map(|(session, _)| session)
1112 }
1113
1114 /// [`serve_with`], also handing back what the client had been sent each
1115 /// time it asked for another frame.
1116 ///
1117 /// The witness is a record of *timing*, not of contents: it says what was
1118 /// already on the wire each time the client came back for more. A test
1119 /// about what a bridge delivers should read the session; a test about
1120 /// *when* should read this, because only it can tell a loop that relays as
1121 /// it goes from one that relays everything at the end — the two produce
1122 /// identical sink contents and differ in every way a client would notice.
1123 async fn serve_witnessed(
1124 agreement: &Agreement,
1125 frames: &[&str],
1126 handshake: bool,
1127 upstream: Upstream,
1128 ) -> Result<(Session, Arc<Mutex<Vec<Vec<String>>>>), Failure> {
1129 let recording: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
1130 let seen = upstream.seen();
1131
1132 let mut script = Vec::new();
1133 if handshake {
1134 script.push(handshake_frame(agreement.upstream().version));
1135 }
1136 script.extend(frames.iter().map(|frame| (*frame).to_owned()));
1137
1138 let (source, witness) = Scripted::witnessed(&script, &recording);
1139
1140 let mut loop_ = BridgeLoop::new(
1141 Wiring::new(
1142 *agreement.upstream(),
1143 DegradePolicy::default(),
1144 upstream,
1145 crate::application::bridge::content::doubles::Counting::reading_nothing(),
1146 ),
1147 DownstreamConfig::new("bridge", "0.1.0", TransportKind::Stdio)?,
1148 source,
1149 Written {
1150 frames: recording.clone(),
1151 },
1152 );
1153
1154 let report = loop_.run().await?;
1155 if handshake && !report.handshaken {
1156 return Err(Failure::Claim(
1157 "the loop never completed a handshake".to_owned(),
1158 ));
1159 }
1160 let session = Session {
1161 report,
1162 written: written(&recording)?,
1163 seen,
1164 };
1165 Ok((session, witness))
1166 }
1167
1168 /// Runs one loop: a handshake, then `frames`.
1169 ///
1170 /// Returns the report and everything written *after* the handshake reply,
1171 /// which is what a test about forwarding cares about. The handshake itself
1172 /// is asserted rather than trusted — a loop that never connected would
1173 /// otherwise leave every such assertion passing for the wrong reason.
1174 async fn run(
1175 agreement: &Agreement,
1176 frames: &[&str],
1177 ) -> Result<(LoopReport, Vec<Value>), Failure> {
1178 let session = serve(agreement, frames, true).await?;
1179 Ok((session.report, session.after_handshake()))
1180 }
1181
1182 /// A request is answered, with the id the client used.
1183 #[tokio::test]
1184 async fn a_request_is_forwarded_and_its_reply_written() -> Result<(), Failure> {
1185 let (report, replies) = run(
1186 &agreeing(&[Feature::Tools]),
1187 &[r#"{"jsonrpc":"2.0","id":7,"method":"tools/list"}"#],
1188 )
1189 .await?;
1190
1191 assert_eq!(report.forwarded, 1);
1192 assert_eq!(replies.len(), 1);
1193 let Some(reply) = replies.first() else {
1194 return Err(Failure::Poisoned);
1195 };
1196 assert_eq!(reply.get("id"), Some(&json!(7)));
1197 assert!(reply.get("result").is_some(), "the reply lost its result");
1198 Ok(())
1199 }
1200
1201 /// A notification produces no frame at all.
1202 ///
1203 /// A conforming peer answers one with `202` and no body, so writing an empty
1204 /// reply would put a response on the wire the server never sent.
1205 #[tokio::test]
1206 async fn a_notification_writes_nothing() -> Result<(), Failure> {
1207 let (report, replies) = run(
1208 &agreeing(&[Feature::Tools]),
1209 &[r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#],
1210 )
1211 .await?;
1212
1213 assert_eq!(report.notified, 1);
1214 assert_eq!(report.forwarded, 0);
1215 assert!(replies.is_empty(), "a notification produced a frame");
1216 Ok(())
1217 }
1218
1219 /// A frame the server volunteered reaches the client *while it is still
1220 /// connected*.
1221 ///
1222 /// The sharp test here, and the one that took a second attempt to write.
1223 /// Asserting that the notification is in the sink at the end is not enough:
1224 /// a loop that drained only after the client's stream ended would pass that,
1225 /// and would relay nothing at all for a client that never closes — which is
1226 /// every client that stays connected, and the only kind a bridge is run
1227 /// for. So this reads the sink at the moment the client asks its *next*
1228 /// question, and requires the notification to be there already.
1229 ///
1230 /// `notifications/tools/list_changed` because that is the one a client acts
1231 /// on: a tool registered upstream mid-session is invisible to it otherwise,
1232 /// which is half of what the project calls DYNAMIC.
1233 #[tokio::test]
1234 async fn a_frame_pushed_before_any_forward_reaches_a_still_connected_client()
1235 -> Result<(), Failure> {
1236 let (session, witness) = serve_witnessed(
1237 &agreeing(&[Feature::Tools]),
1238 &[r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#],
1239 true,
1240 Upstream::holding(&pushed_frame("notifications/tools/list_changed")?)?,
1241 )
1242 .await?;
1243
1244 assert_eq!(session.report.relayed, 1);
1245 assert_eq!(session.report.forwarded, 1);
1246 assert_eq!(
1247 relayed_method(&session.written)?,
1248 Some("notifications/tools/list_changed"),
1249 "the frame that reached the client was not the one the server pushed, and one \
1250 that was not pushed is not there at all"
1251 );
1252 // The client's second frame is the `ping`. By the time it asked for it,
1253 // the notification had to be on the wire — not queued behind the end of
1254 // the stream.
1255 let marks = witness.lock().map_err(|_| Failure::Poisoned)?.clone();
1256 let before_ping = marks
1257 .iter()
1258 .rev()
1259 .find(|mark| !mark.is_empty())
1260 .ok_or_else(|| {
1261 Failure::Claim("the client was never sent anything at all".to_owned())
1262 })?;
1263 assert!(
1264 before_ping
1265 .iter()
1266 .any(|frame| frame.contains("notifications/tools/list_changed")),
1267 "the notification was still queued when the client asked its next question, so a \
1268 client that stayed connected would never have seen it: the loop relays only at \
1269 the end of the stream"
1270 );
1271 Ok(())
1272 }
1273
1274 /// The same frame still arrives once the handshake has moved the peer.
1275 ///
1276 /// [`establish`](crate::application::bridge::BridgeLoop) takes the wiring's
1277 /// peer and drops it into the live [`Bridge`], so a drain that consulted
1278 /// only the wiring would look for a peer that is no longer there and relay
1279 /// nothing. This is the test for that: the notification is held during the
1280 /// very forward that causes the move.
1281 #[tokio::test]
1282 async fn a_frame_pushed_during_a_forward_reaches_the_client() -> Result<(), Failure> {
1283 let session = serve_with(
1284 &agreeing(&[Feature::Tools]),
1285 &[r#"{"jsonrpc":"2.0","id":9,"method":"tools/list"}"#],
1286 true,
1287 Upstream::pushing_on_send(pushed_frame("notifications/message")?),
1288 )
1289 .await?;
1290
1291 assert_eq!(
1292 session.report.relayed, 1,
1293 "the peer had moved into the live bridge and the drain could not find it there"
1294 );
1295 assert_eq!(session.report.forwarded, 1);
1296 assert_eq!(
1297 relayed_method(&session.written)?,
1298 Some("notifications/message"),
1299 "the frame that arrived was not the one the server volunteered"
1300 );
1301 Ok(())
1302 }
1303
1304 /// A server's notification reaches only a client that has handshaken.
1305 ///
1306 /// The first frame here is a request, which the loop refuses because no
1307 /// handshake has completed — and the server has a notification waiting the
1308 /// whole time. Delivering it would push a frame at a client over a session
1309 /// it has not negotiated, the mirror of the rule the loop already applies
1310 /// to requests in the other direction. It is held rather than discarded, and
1311 /// a later drain would still send it.
1312 #[tokio::test]
1313 async fn a_frame_pushed_before_any_handshake_waits_for_one() -> Result<(), Failure> {
1314 let session = serve_with(
1315 &agreeing(&[Feature::Tools]),
1316 &[r#"{"jsonrpc":"2.0","id":3,"method":"tools/list"}"#],
1317 false,
1318 Upstream::holding(&pushed_frame("notifications/tools/list_changed")?)?,
1319 )
1320 .await?;
1321
1322 assert!(
1323 !session.report.handshaken,
1324 "the loop connected without a handshake"
1325 );
1326 assert_eq!(
1327 session.report.relayed, 0,
1328 "a server notification reached a client that had never completed an initialize"
1329 );
1330 assert_eq!(
1331 session.report.failed, 1,
1332 "the pre-handshake request should have been refused, not forwarded"
1333 );
1334 Ok(())
1335 }
1336
1337 /// A server's notification, as the envelope the peers exchange it.
1338 fn pushed_frame(method: &str) -> Result<Map<String, Value>, Failure> {
1339 json!({"jsonrpc": "2.0", "method": method})
1340 .as_object()
1341 .cloned()
1342 .ok_or_else(|| Failure::Claim("the pushed fixture is not an object".to_owned()))
1343 }
1344
1345 /// The method of the one frame in `written` that carries one.
1346 ///
1347 /// Returns `Ok(None)` when there is none, so a test can say "nothing was
1348 /// relayed" without the assertion itself being the thing that fails on an
1349 /// unrelated index.
1350 fn relayed_method(written: &[Value]) -> Result<Option<&str>, Failure> {
1351 let mut found = written
1352 .iter()
1353 .filter_map(|frame| frame.get("method").and_then(Value::as_str));
1354 match (found.next(), found.next()) {
1355 (Some(method), None) => Ok(Some(method)),
1356 _ => Err(Failure::Claim(
1357 "expected exactly one relayed frame, found a different number".to_owned(),
1358 )),
1359 }
1360 }
1361
1362 /// A feature the peers do not share is refused in an answer, not a dead loop.
1363 ///
1364 /// This is the test the module exists for. Killing the loop would report
1365 /// "the bridge is down" for a call the bridge understood perfectly well and
1366 /// declined for a stated reason.
1367 #[tokio::test]
1368 async fn a_withheld_feature_is_answered_and_the_loop_continues() -> Result<(), Failure> {
1369 let (report, replies) = run(
1370 &agreeing(&[]),
1371 &[
1372 r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"echo"}}"#,
1373 r#"{"jsonrpc":"2.0","id":2,"method":"ping"}"#,
1374 ],
1375 )
1376 .await?;
1377
1378 assert_eq!(report.failed, 1);
1379 assert_eq!(replies.len(), 2, "the refusal ended the loop");
1380
1381 let Some(refused) = replies.first() else {
1382 return Err(Failure::Poisoned);
1383 };
1384 assert_eq!(refused.get("id"), Some(&json!(1)));
1385 assert_eq!(
1386 refused.pointer("/error/code").and_then(Value::as_i64),
1387 Some(-32601)
1388 );
1389
1390 // The second call went through, which is the half that matters: the
1391 // bridge is healthy and says so by answering.
1392 let Some(served) = replies.get(1) else {
1393 return Err(Failure::Poisoned);
1394 };
1395 assert_eq!(served.get("id"), Some(&json!(2)));
1396 assert!(served.get("result").is_some());
1397 Ok(())
1398 }
1399
1400 /// A frame that is not JSON is answered, and the next frame still lands.
1401 #[tokio::test]
1402 async fn an_unparseable_frame_is_answered_and_the_loop_continues() -> Result<(), Failure> {
1403 let (report, replies) = run(
1404 &agreeing(&[Feature::Tools]),
1405 &["{not json", r#"{"jsonrpc":"2.0","id":3,"method":"ping"}"#],
1406 )
1407 .await?;
1408
1409 assert_eq!(report.forwarded, 1);
1410 assert_eq!(replies.len(), 2);
1411 let Some(refused) = replies.first() else {
1412 return Err(Failure::Poisoned);
1413 };
1414 assert_eq!(
1415 refused.pointer("/error/code").and_then(Value::as_i64),
1416 Some(-32700)
1417 );
1418 // No id to quote, so the answer carries none rather than someone else's.
1419 assert_eq!(refused.get("id"), Some(&Value::Null));
1420 Ok(())
1421 }
1422
1423 /// An empty batch is one invalid request, not an empty array back.
1424 ///
1425 /// The specification's requirement is "an Array with at least one value",
1426 /// so `[]` is the one batch shape answered with a single object. An empty
1427 /// array would read to a client as a batch of zero successful calls.
1428 #[tokio::test]
1429 async fn an_empty_batch_is_a_single_invalid_request() -> Result<(), Failure> {
1430 let (_report, replies) = run(&agreeing(&[Feature::Tools]), &["[]"]).await?;
1431
1432 assert_eq!(replies.len(), 1);
1433 let Some(refused) = replies.first() else {
1434 return Err(Failure::Poisoned);
1435 };
1436 assert!(refused.is_object(), "an empty batch was not refused");
1437 assert_eq!(
1438 refused.pointer("/error/code").and_then(Value::as_i64),
1439 Some(-32600)
1440 );
1441 Ok(())
1442 }
1443
1444 /// A batch is answered with one array, in one frame.
1445 ///
1446 /// A client waiting on a single frame would read the first reply and leave
1447 /// the rest sitting in its buffer.
1448 #[tokio::test]
1449 async fn a_batch_is_answered_with_one_array() -> Result<(), Failure> {
1450 let (report, replies) = run(
1451 &agreeing(&[Feature::Tools]),
1452 &[r#"[{"jsonrpc":"2.0","id":1,"method":"ping"},
1453 {"jsonrpc":"2.0","id":2,"method":"ping"}]"#],
1454 )
1455 .await?;
1456
1457 assert_eq!(report.forwarded, 2);
1458 assert_eq!(
1459 replies.len(),
1460 1,
1461 "a batch answered with more than one frame"
1462 );
1463 let Some(Value::Array(members)) = replies.first() else {
1464 return Err(Failure::Poisoned);
1465 };
1466 assert_eq!(members.len(), 2);
1467 Ok(())
1468 }
1469
1470 /// A frame that is valid JSON but not a message names what arrived.
1471 #[tokio::test]
1472 async fn a_frame_that_is_not_a_message_is_refused() -> Result<(), Failure> {
1473 let (_report, replies) = run(&agreeing(&[Feature::Tools]), &["42"]).await?;
1474
1475 let Some(refused) = replies.first() else {
1476 return Err(Failure::Poisoned);
1477 };
1478 assert_eq!(
1479 refused.pointer("/error/code").and_then(Value::as_i64),
1480 Some(-32600)
1481 );
1482 let message = refused.pointer("/error/message").and_then(Value::as_str);
1483 assert!(
1484 message.is_some_and(|text| text.contains(kind(&Value::from(42)))),
1485 "the refusal did not say what arrived"
1486 );
1487 Ok(())
1488 }
1489
1490 /// End of stream ends the loop and reports what it moved.
1491 #[tokio::test]
1492 async fn end_of_stream_ends_the_loop() -> Result<(), Failure> {
1493 let (report, replies) = run(&agreeing(&[Feature::Tools]), &[]).await?;
1494
1495 assert!(report.handshaken, "the client connected and said nothing");
1496 assert_eq!(report.forwarded, 0);
1497 assert_eq!(report.notified, 0);
1498 assert_eq!(report.failed, 0);
1499 assert!(replies.is_empty());
1500 Ok(())
1501 }
1502
1503 /// The whole reason the loop grew a handshake: the bridge answers
1504 /// `initialize` itself, and the upstream never sees one.
1505 ///
1506 /// A bridge that forwarded the handshake would answer the client with the
1507 /// upstream's revision and the upstream's capabilities — which is a
1508 /// pass-through wearing a proxy's name, and tells the client nothing about
1509 /// whether *this* bridge can serve them.
1510 #[tokio::test]
1511 async fn the_bridge_answers_initialize_itself() -> Result<(), Failure> {
1512 let session = serve(&agreeing(&[Feature::Tools]), &[], true).await?;
1513
1514 assert!(session.report.handshaken, "no handshake was recorded");
1515 assert_eq!(
1516 session.seen.count()?,
1517 0,
1518 "the upstream saw the handshake: {:?}",
1519 session.seen.methods()?
1520 );
1521
1522 let name = session
1523 .written
1524 .first()
1525 .and_then(|reply| reply.get("result"))
1526 .and_then(|result| result.get("serverInfo"))
1527 .and_then(|info| info.get("name"))
1528 .and_then(Value::as_str);
1529 assert_eq!(name, Some("bridge"), "{:?}", session.written);
1530 Ok(())
1531 }
1532
1533 /// The reply carries the id the client used, like every other reply.
1534 #[tokio::test]
1535 async fn the_handshake_reply_carries_the_requests_own_id() -> Result<(), Failure> {
1536 let session = serve(&agreeing(&[Feature::Tools]), &[], true).await?;
1537 let id = session
1538 .written
1539 .first()
1540 .and_then(|reply| reply.get("id"))
1541 .cloned();
1542 assert_eq!(id, Some(json!(1)), "{:?}", session.written);
1543 Ok(())
1544 }
1545
1546 /// A request that arrives before any handshake is refused, not forwarded.
1547 ///
1548 /// The specification requires it, and the reason is the one that matters:
1549 /// the upstream has never accepted this client, so forwarding would speak
1550 /// for somebody it has not met.
1551 #[tokio::test]
1552 async fn a_request_before_the_handshake_is_refused() -> Result<(), Failure> {
1553 let session = serve(
1554 &agreeing(&[Feature::Tools]),
1555 &[r#"{"jsonrpc":"2.0","id":4,"method":"tools/list"}"#],
1556 false,
1557 )
1558 .await?;
1559
1560 assert!(!session.report.handshaken);
1561 assert_eq!(
1562 session.seen.count()?,
1563 0,
1564 "a pre-handshake request reached the upstream"
1565 );
1566 let code = session
1567 .written
1568 .first()
1569 .and_then(|reply| reply.get("error"))
1570 .and_then(|error| error.get("code"))
1571 .and_then(Value::as_i64);
1572 assert_eq!(code, Some(-32_600), "{:?}", session.written);
1573 Ok(())
1574 }
1575
1576 /// The loop still accepts a handshake after a refusal.
1577 ///
1578 /// A client whose first frame was malformed, or which sent a request too
1579 /// early, is not poisoned for it — a proxy that killed the connection on a
1580 /// recoverable mistake would fail the one client most likely to make one.
1581 #[tokio::test]
1582 async fn a_handshake_after_a_refusal_still_connects() -> Result<(), Failure> {
1583 let session = serve(
1584 &agreeing(&[Feature::Tools]),
1585 &[
1586 r#"{"jsonrpc":"2.0","id":4,"method":"tools/list"}"#,
1587 &handshake_frame(ProtocolVersion::latest_known()),
1588 ],
1589 false,
1590 )
1591 .await?;
1592
1593 assert!(session.report.handshaken, "the second frame was ignored");
1594 Ok(())
1595 }
1596
1597 /// `notifications/initialized` is consumed by the bridge, not forwarded.
1598 ///
1599 /// The upstream session was initialised when the bridge connected, before
1600 /// any client existed. Forwarding this one would tell the upstream a second
1601 /// client had arrived, describing a session it already considers open.
1602 #[tokio::test]
1603 async fn the_initialized_notification_is_not_forwarded() -> Result<(), Failure> {
1604 let session = serve(
1605 &agreeing(&[Feature::Tools]),
1606 &[r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#],
1607 true,
1608 )
1609 .await?;
1610
1611 assert_eq!(
1612 session.seen.methods()?,
1613 Vec::<String>::new(),
1614 "the bridge forwarded a notification about a session it owns"
1615 );
1616 assert_eq!(session.written.len(), 1, "a notification was answered");
1617 Ok(())
1618 }
1619
1620 /// A client that handshakes twice is refused the second time.
1621 ///
1622 /// The first handshake is what the running bridge was built from. Answering
1623 /// a second would renegotiate a session the forwarding path is already
1624 /// committed to, and the reply would promise a profile that path is not
1625 /// using.
1626 #[tokio::test]
1627 async fn a_second_handshake_is_refused() -> Result<(), Failure> {
1628 let session = serve(
1629 &agreeing(&[Feature::Tools]),
1630 &[r#"{"jsonrpc":"2.0","id":9,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"p"}}}"#],
1631 true,
1632 )
1633 .await?;
1634
1635 let second = session.written.get(1);
1636 let code = second
1637 .and_then(|reply| reply.get("error"))
1638 .and_then(|error| error.get("code"))
1639 .and_then(Value::as_i64);
1640 assert_eq!(code, Some(-32_600), "{:?}", session.written);
1641 assert_eq!(
1642 session.report.forwarded, 0,
1643 "a renegotiating client put something on the wire"
1644 );
1645 Ok(())
1646 }
1647
1648 /// A client offering a revision the build cannot speak is walked back.
1649 ///
1650 /// `2026-07-28` removed `initialize` entirely (SEP-2575), so answering it
1651 /// is impossible; the client gets the newest revision that still has a
1652 /// handshake instead, which is what the negotiation is for.
1653 #[tokio::test]
1654 async fn a_client_offering_a_handshakeless_revision_is_walked_back() -> Result<(), Failure> {
1655 let session = serve(
1656 &agreeing(&[Feature::Tools]),
1657 &[r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"p"}}}"#],
1658 false,
1659 )
1660 .await?;
1661
1662 let answered = session
1663 .written
1664 .first()
1665 .and_then(|reply| reply.get("result"))
1666 .and_then(|result| result.get("protocolVersion"))
1667 .and_then(Value::as_str)
1668 .map(str::to_owned);
1669 let Some(answered) = answered else {
1670 return Err(Failure::Claim("the reply named no revision".to_owned()));
1671 };
1672 let parsed = answered
1673 .parse::<ProtocolVersion>()
1674 .map_err(|e| Failure::Claim(format!("the bridge answered {answered:?}: {e}")))?;
1675 assert!(
1676 crate::domain::handshake_defined_in(parsed),
1677 "the bridge answered with {answered:?}, which has no initialize to answer"
1678 );
1679 assert_ne!(
1680 parsed,
1681 ProtocolVersion::V2026_07_28,
1682 "the client was walked forward to the revision that removed the handshake"
1683 );
1684 Ok(())
1685 }
1686
1687 /// A client offering an older revision gets that revision, not the newest.
1688 ///
1689 /// The bridge is the version-translating half of this project; answering a
1690 /// `2024-11-05` client with a newer revision would be the one thing it
1691 /// must never do.
1692 #[tokio::test]
1693 async fn an_older_client_keeps_its_revision() -> Result<(), Failure> {
1694 let session = serve(
1695 &agreeing(&[Feature::Tools]),
1696 &[r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"p"}}}"#],
1697 false,
1698 )
1699 .await?;
1700
1701 let answered = session
1702 .written
1703 .first()
1704 .and_then(|reply| reply.get("result"))
1705 .and_then(|result| result.get("protocolVersion"))
1706 .and_then(Value::as_str);
1707 assert_eq!(answered, Some("2024-11-05"), "{:?}", session.written);
1708 Ok(())
1709 }
1710
1711 /// The advertisement is the agreement, not the revision's full capability
1712 /// list.
1713 ///
1714 /// `agreeing(&[Feature::Tools])` is an upstream that offered tools and
1715 /// nothing else, so a reply advertising anything more is a bridge promising
1716 /// to forward something its upstream never agreed to.
1717 #[tokio::test]
1718 async fn the_advertisement_is_the_upstreams_not_the_revisions() -> Result<(), Failure> {
1719 let session = serve(&agreeing(&[Feature::Tools]), &[], true).await?;
1720
1721 let capabilities = session
1722 .written
1723 .first()
1724 .and_then(|reply| reply.get("result"))
1725 .and_then(|result| result.get("capabilities"));
1726 let Some(capabilities) = capabilities else {
1727 return Err(Failure::Claim("the reply advertised nothing".to_owned()));
1728 };
1729 assert!(
1730 capabilities.get("tools").is_some(),
1731 "the one agreed feature is missing: {capabilities}"
1732 );
1733 assert_eq!(
1734 capabilities.get("resources"),
1735 None,
1736 "the bridge advertised a feature the upstream never agreed to: {capabilities}"
1737 );
1738 Ok(())
1739 }
1740
1741 /// Every refusal carries the request's own id, so a client can match it.
1742 #[tokio::test]
1743 async fn a_refusal_answers_with_the_requests_own_id() -> Result<(), Failure> {
1744 let (_report, replies) = run(
1745 &agreeing(&[]),
1746 &[r#"{"jsonrpc":"2.0","id":"call-9","method":"tools/list"}"#],
1747 )
1748 .await?;
1749
1750 let Some(refused) = replies.first() else {
1751 return Err(Failure::Poisoned);
1752 };
1753 assert_eq!(refused.get("id"), Some(&json!("call-9")));
1754 Ok(())
1755 }
1756}