@@ -238,14 +238,14 @@ impl ServiceRole for RoleClient {
238238 }
239239 }
240240
241- // SEP-2260: with no outbound request in flight there is nothing the
242- // server request could be associated with, so reject it. With one in
243- // flight we cannot tell which request it belongs to (no wire field), so
244- // we accept — an under-approximation of the spec's SHOULD.
241+ // SEP-2260: reject restricted server requests that the transport observed
242+ // arriving with no in-flight outbound request association. Transports
243+ // without stream separation (stdio) report `Unknown`, where the coarse
244+ // in-flight check is the best available under-approximation of the SHOULD.
245245 fn enforce_peer_request_association (
246246 peer_request : & Self :: PeerReq ,
247247 peer_info : Option < & Self :: PeerInfo > ,
248- has_pending_outbound_request : bool ,
248+ association : PeerRequestAssociation ,
249249 ) -> Result < ( ) , ErrorData > {
250250 let restricted = matches ! (
251251 peer_request,
@@ -258,13 +258,24 @@ impl ServiceRole for RoleClient {
258258 }
259259 let strict =
260260 peer_info. is_some_and ( |info| info. protocol_version >= ProtocolVersion :: V_2026_07_28 ) ;
261- if strict && !has_pending_outbound_request {
262- return Err ( ErrorData :: invalid_params (
261+ if !strict {
262+ return Ok ( ( ) ) ;
263+ }
264+ let associated = match association {
265+ PeerRequestAssociation :: Associated => true ,
266+ PeerRequestAssociation :: Unassociated => false ,
267+ PeerRequestAssociation :: Unknown {
268+ has_pending_outbound_request,
269+ } => has_pending_outbound_request,
270+ } ;
271+ if associated {
272+ Ok ( ( ) )
273+ } else {
274+ Err ( ErrorData :: invalid_params (
263275 "SEP-2260: server-to-client requests must be associated with an in-flight client request" ,
264276 None ,
265- ) ) ;
277+ ) )
266278 }
267- Ok ( ( ) )
268279 }
269280
270281 async fn invalidate_response_cache ( peer : & Peer < Self > , notification : & Self :: PeerNot ) {
@@ -2215,3 +2226,72 @@ mod tests {
22152226 assert_eq ! ( peer. discover( meta) . await . unwrap( ) , expected) ;
22162227 }
22172228}
2229+
2230+ #[ cfg( test) ]
2231+ mod sep2260_association_tests {
2232+ use super :: * ;
2233+ use crate :: {
2234+ model:: {
2235+ CreateMessageRequest , CreateMessageRequestParams , SamplingMessage , ServerCapabilities ,
2236+ } ,
2237+ service:: PeerRequestAssociation ,
2238+ } ;
2239+
2240+ fn sampling_request ( ) -> ServerRequest {
2241+ ServerRequest :: CreateMessageRequest ( CreateMessageRequest :: new (
2242+ CreateMessageRequestParams :: new ( vec ! [ SamplingMessage :: user_text( "hi" ) ] , 16 ) ,
2243+ ) )
2244+ }
2245+
2246+ fn server_info ( version : ProtocolVersion ) -> ServerInfo {
2247+ let mut info = ServerInfo :: new ( ServerCapabilities :: default ( ) ) ;
2248+ info. protocol_version = version;
2249+ info
2250+ }
2251+
2252+ fn enforce ( info : & ServerInfo , association : PeerRequestAssociation ) -> Result < ( ) , ErrorData > {
2253+ RoleClient :: enforce_peer_request_association ( & sampling_request ( ) , Some ( info) , association)
2254+ }
2255+
2256+ #[ test]
2257+ fn strict_rejects_unassociated ( ) {
2258+ let info = server_info ( ProtocolVersion :: V_2026_07_28 ) ;
2259+ let err = enforce ( & info, PeerRequestAssociation :: Unassociated ) . unwrap_err ( ) ;
2260+ assert_eq ! ( err. code, crate :: model:: ErrorCode :: INVALID_PARAMS ) ;
2261+ }
2262+
2263+ #[ test]
2264+ fn strict_accepts_associated ( ) {
2265+ let info = server_info ( ProtocolVersion :: V_2026_07_28 ) ;
2266+ assert ! ( enforce( & info, PeerRequestAssociation :: Associated ) . is_ok( ) ) ;
2267+ }
2268+
2269+ #[ test]
2270+ fn strict_unknown_falls_back_to_coarse_check ( ) {
2271+ let info = server_info ( ProtocolVersion :: V_2026_07_28 ) ;
2272+ assert ! (
2273+ enforce(
2274+ & info,
2275+ PeerRequestAssociation :: Unknown {
2276+ has_pending_outbound_request: true
2277+ }
2278+ )
2279+ . is_ok( )
2280+ ) ;
2281+ assert ! (
2282+ enforce(
2283+ & info,
2284+ PeerRequestAssociation :: Unknown {
2285+ has_pending_outbound_request: false
2286+ }
2287+ )
2288+ . is_err( )
2289+ ) ;
2290+ }
2291+
2292+ #[ test]
2293+ fn legacy_protocol_accepts_even_unassociated ( ) {
2294+ let info = server_info ( ProtocolVersion :: V_2025_11_25 ) ;
2295+ assert ! ( enforce( & info, PeerRequestAssociation :: Unassociated ) . is_ok( ) ) ;
2296+ }
2297+ }
0 commit comments