Skip to main content

net/adapter/net/cortex/
rpc.rs

1//! nRPC — request/response on top of CortEX folds.
2//!
3//! See `docs/internal/misc/NRPC_DESIGN.md` for the full architectural framing.
4//! In short: an RPC server is a `RedexFold` whose state is the
5//! in-flight call set, whose events are typed `(REQUEST, RESPONSE,
6//! CANCEL, DEADLINE_EXCEEDED)`, whose `EventMeta::seq_or_ts` is the
7//! correlation id, and whose `EventMeta::origin_hash` is the
8//! AEAD-verified caller. The mesh-channel layer's queue-group
9//! subscription mode (see `channel::SubscriptionMode`) does the
10//! one-of-N work distribution across replica servers.
11//!
12//! This module is the **wire codec layer**: dispatch constants for
13//! `EventMeta::dispatch`, payload structs for `RpcRequestPayload` /
14//! `RpcResponsePayload`, and the `RpcStatus` enumeration. The fold
15//! types and the `Mesh::serve_rpc` / `Mesh::call` glue layer build
16//! on top.
17
18use bytes::{Buf, BufMut, Bytes};
19use parking_lot::Mutex;
20use std::collections::HashMap;
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::sync::Arc;
23use tokio::sync::Notify;
24
25use super::super::redex::{RedexError, RedexEvent, RedexFold};
26use super::meta::{EventMeta, EVENT_META_SIZE};
27
28// ============================================================================
29// `EventMeta::dispatch` byte assignments for nRPC.
30//
31// All four values live in the cortex-internal range (`0x00..0x7F`).
32// Application/vendor dispatches stay in `0x80..0xFF`. Adapters that
33// don't care about RPC ignore unknown dispatches as they ignore any
34// other.
35// ============================================================================
36
37/// Caller → server. The first frame of an RPC call. `EventMeta::seq_or_ts`
38/// is the caller-generated `call_id`; `EventMeta::origin_hash` is the
39/// AEAD-verified caller. Payload is an [`RpcRequestPayload`].
40pub const DISPATCH_RPC_REQUEST: u8 = 0x10;
41
42/// Server → caller. The (terminal, for unary) frame of an RPC call.
43/// `EventMeta::seq_or_ts` matches the request's `call_id`. Payload is
44/// an [`RpcResponsePayload`].
45pub const DISPATCH_RPC_RESPONSE: u8 = 0x11;
46
47/// Caller → server. Cancellation signal. `EventMeta::seq_or_ts` matches
48/// the request's `call_id`. Empty payload — the dispatch byte plus
49/// the matching `call_id` is the whole signal. Server's fold removes
50/// the in-flight entry and (if cooperative) flips the handler's
51/// `CancellationToken`.
52pub const DISPATCH_RPC_CANCEL: u8 = 0x12;
53
54/// Server → caller. Deadline-exceeded signal. Emitted when the
55/// server's fold sees `now_ns() > request.deadline_ns` before
56/// starting the handler (or, optionally, when a long-running handler
57/// is aborted by the deadline timer). `EventMeta::seq_or_ts` matches
58/// the request's `call_id`. Empty payload.
59pub const DISPATCH_RPC_DEADLINE_EXCEEDED: u8 = 0x13;
60
61/// Caller → server. Stream credit grant. Carries a 4-byte
62/// big-endian `u32` in the payload after `EventMeta`: the number
63/// of additional response chunks the caller is willing to accept
64/// for the streaming call identified by `EventMeta::seq_or_ts`.
65///
66/// Only meaningful when the caller opted into flow control via
67/// the `nrpc-stream-window-initial` request header
68/// ([`HEADER_NRPC_STREAM_WINDOW_INITIAL`]). On a flow-controlled
69/// stream the server's pump task awaits one credit per chunk; on
70/// a non-flow-controlled stream (no header) the server ignores
71/// every GRANT.
72///
73/// Phase 3.
74pub const DISPATCH_RPC_STREAM_GRANT: u8 = 0x14;
75
76/// Caller → server. Continuation chunk of a client-streaming or
77/// duplex REQUEST. Carries an [`RpcRequestChunkPayload`] after the
78/// `EventMeta` prefix. `EventMeta::seq_or_ts` matches the initial
79/// REQUEST's `call_id`. Non-terminal chunks have
80/// `flags & FLAG_RPC_REQUEST_END == 0`; the terminal upload chunk
81/// sets [`FLAG_RPC_REQUEST_END`].
82///
83/// Only meaningful for calls whose initial REQUEST set
84/// [`FLAG_RPC_CLIENT_STREAMING_REQUEST`]; otherwise the server
85/// silently drops the chunk (caller bug; no observable effect).
86pub const DISPATCH_RPC_REQUEST_CHUNK: u8 = 0x15;
87
88/// Server → caller. Request-direction stream-credit grant. Mirror
89/// of [`DISPATCH_RPC_STREAM_GRANT`] for the upload direction.
90/// Carries an [`RpcRequestGrantPayload`] after `EventMeta`: a
91/// `call_id` plus a `u32` credit count. `EventMeta::seq_or_ts`
92/// matches the call's `call_id` (redundant with the payload, but
93/// kept symmetric with the rest of the dispatch family).
94///
95/// Only meaningful when the caller opted into request-direction
96/// flow control via the `nrpc-request-window-initial` header
97/// ([`HEADER_NRPC_REQUEST_WINDOW_INITIAL`]). Caller's sink
98/// awaits one credit per `REQUEST_CHUNK`; absent header →
99/// unbounded credit (sink emits as fast as the publish path can
100/// take it).
101pub const DISPATCH_RPC_REQUEST_GRANT: u8 = 0x16;
102
103// ============================================================================
104// OA2-E0.2 — RpcRouteV1 frame discriminator.
105//
106// Every nRPC frame is laid out `EventMeta ‖ RpcRouteV1 ‖ payload`,
107// where RpcRouteV1 is the CANONICAL u64 ChannelHash of the physical
108// channel the frame rides (caller → provider: `<service>.requests`;
109// provider → caller: the reply channel). The wire packet header
110// carries only a `u16` bucket, which can collide; this discriminator
111// lets mesh ingress select EXACTLY ONE registered canonical
112// dispatcher instead of fanning the frame to every candidate and
113// asking each fold to self-filter (the removed ambiguity).
114//
115// This is a coordinated nRPC frame-version change — every nRPC
116// sender writes it and mesh ingress requires it for RPC event types.
117// ============================================================================
118
119/// Size of the [`RpcRouteV1`](encode_rpc_route) discriminator: one
120/// canonical u64 `ChannelHash`.
121pub const RPC_ROUTE_V1_SIZE: usize = 8;
122
123/// Byte offset of the frame-specific payload inside an nRPC frame:
124/// past the `EventMeta` prefix AND the RpcRouteV1 discriminator.
125pub const RPC_FRAME_BODY_OFFSET: usize = EVENT_META_SIZE + RPC_ROUTE_V1_SIZE;
126
127/// Append the RpcRouteV1 discriminator (`canonical` channel hash)
128/// to a frame buffer that already holds the `EventMeta` prefix
129/// (OA2-E0.2). Every nRPC frame builder calls this immediately
130/// after `meta.to_bytes()`.
131#[inline]
132pub fn encode_rpc_route(buf: &mut Vec<u8>, canonical: crate::adapter::net::channel::ChannelHash) {
133    buf.extend_from_slice(&canonical.to_le_bytes());
134}
135
136/// Insert the RpcRouteV1 discriminator into an already-assembled
137/// `EventMeta ‖ payload` frame, producing `EventMeta ‖ route ‖
138/// payload` (OA2-E0.2). Used at the centralized response publish
139/// choke point (`publish_response_to_caller`), where the frame is
140/// built before the reply-channel hash is threaded in — one small
141/// copy per response frame. Direct request-direction builders use
142/// [`encode_rpc_route`] inline instead (no copy). Frames shorter
143/// than `EVENT_META_SIZE` are returned unchanged (never a real
144/// nRPC frame).
145pub fn insert_rpc_route(
146    frame: bytes::Bytes,
147    canonical: crate::adapter::net::channel::ChannelHash,
148) -> bytes::Bytes {
149    if frame.len() < EVENT_META_SIZE {
150        return frame;
151    }
152    let mut out = Vec::with_capacity(frame.len() + RPC_ROUTE_V1_SIZE);
153    out.extend_from_slice(&frame[..EVENT_META_SIZE]);
154    out.extend_from_slice(&canonical.to_le_bytes());
155    out.extend_from_slice(&frame[EVENT_META_SIZE..]);
156    bytes::Bytes::from(out)
157}
158
159/// Read the RpcRouteV1 discriminator from a full nRPC frame
160/// (`EventMeta ‖ route ‖ payload`). `None` if the frame is too
161/// short to carry it — a malformed/legacy frame that ingress drops.
162#[inline]
163pub fn decode_rpc_route(frame: &[u8]) -> Option<crate::adapter::net::channel::ChannelHash> {
164    let bytes = frame.get(EVENT_META_SIZE..EVENT_META_SIZE + RPC_ROUTE_V1_SIZE)?;
165    let arr: [u8; RPC_ROUTE_V1_SIZE] = bytes.try_into().ok()?;
166    Some(u64::from_le_bytes(arr))
167}
168
169/// `true` iff `event_type` is an nRPC dispatch frame that carries
170/// the RpcRouteV1 discriminator (OA2-E0.2). Mesh ingress uses this
171/// to apply route-based single-select to RPC frames while leaving
172/// non-RPC dispatcher registrations (e.g. the sensing intake) on
173/// the legacy fan-out path.
174#[inline]
175pub fn is_rpc_dispatch_frame(event_type: u8) -> bool {
176    matches!(
177        event_type,
178        DISPATCH_RPC_REQUEST
179            | DISPATCH_RPC_RESPONSE
180            | DISPATCH_RPC_CANCEL
181            | DISPATCH_RPC_DEADLINE_EXCEEDED
182            | DISPATCH_RPC_STREAM_GRANT
183            | DISPATCH_RPC_REQUEST_CHUNK
184            | DISPATCH_RPC_REQUEST_GRANT
185    )
186}
187
188/// Peek the self-declared `service` field of an initial-REQUEST
189/// frame (`EventMeta ‖ RpcRouteV1 ‖ RpcRequestPayload`) WITHOUT a
190/// full payload decode (OA2-E0.2 P0).
191///
192/// The route discriminator (E0.2) already selected a dispatcher by
193/// *canonical channel hash*, but `RpcRequestPayload` also carries
194/// its OWN `service` string — the first body field (`u8` length ‖
195/// bytes, see [`RpcRequestPayload::encode_into`]). The serve bridge
196/// uses this to enforce `payload.service == captured_service` before
197/// the capability gate or any fold state, so a frame routed to
198/// `admin.requests` whose payload names `echo` never reaches the
199/// admin handler.
200///
201/// Returns `None` when the service field is unreadable — frame too
202/// short (missing the route or the length byte), an empty or
203/// over-cap length, or non-UTF-8 bytes. Those exactly mirror the
204/// `Err` arms of [`RpcRequestPayload::decode`], so an unreadable
205/// service falls through to the fold's full decode, which rejects it
206/// (`UnknownVersion`) — no handler runs either way. A `Some(svc)`
207/// return borrows the service bytes straight out of `frame`.
208///
209/// Only meaningful for `DISPATCH_RPC_REQUEST` frames; control frames
210/// (CANCEL / CHUNK / GRANT) carry no service and inherit the
211/// route-selected active call, so callers gate on the dispatch type
212/// before calling this.
213#[inline]
214pub fn peek_request_service(frame: &[u8]) -> Option<&str> {
215    let body = frame.get(RPC_FRAME_BODY_OFFSET..)?;
216    let (&svc_len, rest) = body.split_first()?;
217    let svc_len = svc_len as usize;
218    if svc_len == 0 || svc_len > MAX_RPC_SERVICE_NAME_LEN {
219        return None;
220    }
221    let svc = rest.get(..svc_len)?;
222    std::str::from_utf8(svc).ok()
223}
224
225// ============================================================================
226// `RpcRequestPayload::flags` bit assignments.
227// ============================================================================
228
229// Bit 0 (`1 << 0`) is RESERVED — was previously documented as
230// `FLAG_RPC_IDEMPOTENT`, but the server-side replay-cache (LRU of
231// `(origin_hash, call_id) -> RpcResponsePayload`) was never landed,
232// so the flag silently no-op'd despite a load-bearing contract in
233// its doc-string. Removed to avoid shipping a documented behavior
234// the runtime doesn't implement; reservation kept so a future
235// re-add (with the LRU) preserves wire compatibility.
236
237/// Set if the server may emit multiple `DISPATCH_RPC_RESPONSE` events
238/// for this call. Without it, the first response terminates the
239/// call. With it, each response except the terminal one carries
240/// `headers["nrpc-streaming"] = b"continue"`; the terminal response
241/// has either `b"end"` (success) or a non-`Ok` status.
242pub const FLAG_RPC_STREAMING_RESPONSE: u16 = 1 << 1;
243
244/// Set if the request carries W3C Trace Context headers
245/// (`traceparent`, `tracestate`). Server propagates them to its own
246/// span emission. Phase 3.
247pub const FLAG_RPC_PROPAGATE_TRACE: u16 = 1 << 2;
248
249// Bit `1 << 3` reserved — symmetric to the reserved bit 0 above,
250// kept as breathing room for a future protocol-level flag without
251// pushing every existing live bit.
252
253/// Set on the initial REQUEST if the caller will follow up with
254/// one or more [`DISPATCH_RPC_REQUEST_CHUNK`] events. Distinguishes
255/// client-streaming / duplex calls from unary at the very first
256/// frame so the server's fold knows to open a request-side stream
257/// instead of treating the REQUEST as complete.
258///
259/// Combined with [`FLAG_RPC_STREAMING_RESPONSE`] on the same
260/// REQUEST: full duplex.
261///
262/// Bidi streaming plan (Phase A).
263pub const FLAG_RPC_CLIENT_STREAMING_REQUEST: u16 = 1 << 4;
264
265/// Set on a [`DISPATCH_RPC_REQUEST_CHUNK`] (or on the initial
266/// REQUEST itself) to signal the terminal upload frame for a
267/// client-streaming or duplex call. After receiving this, the
268/// server's request-side stream yields `None` and the handler
269/// proceeds to its terminal response.
270///
271/// Setting this on the initial REQUEST is the degenerate "client-
272/// streaming with exactly one item" path — saves a round-trip
273/// for the trivial case.
274///
275/// Bidi streaming plan (Phase A).
276pub const FLAG_RPC_REQUEST_END: u16 = 1 << 5;
277
278// Bits `6..=15` reserved; producers MUST write zero, consumers MUST
279// ignore unknown bits (forward-compat with future flags).
280
281// ============================================================================
282// `RpcResponsePayload::status` enumeration.
283// ============================================================================
284
285/// Outcome of an nRPC call. Net-native numbering with documented
286/// gRPC equivalents (see comments). Numeric stability: callers and
287/// servers across versions agree on `0x0000..=0x7FFF`; the
288/// application-defined range is `0x8000..=0xFFFF`.
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
290#[repr(u16)]
291pub enum RpcStatus {
292    /// Success. Payload carries the application response. Terminal
293    /// (or, for streaming responses, may be one of many — see the
294    /// streaming flag).
295    /// gRPC equivalent: `OK` (0).
296    Ok = 0x0000,
297    /// No service registered with the requested name on the server.
298    /// gRPC equivalent: `NOT_FOUND` (5).
299    NotFound = 0x0001,
300    /// Caller's token doesn't list the requested service in scope,
301    /// or the channel-level capability check failed.
302    /// gRPC equivalent: `PERMISSION_DENIED` (7).
303    Unauthorized = 0x0002,
304    /// Server observed `now_ns() > deadline_ns` before starting work.
305    /// (For the in-flight case after the handler started, see
306    /// [`DISPATCH_RPC_DEADLINE_EXCEEDED`].)
307    /// gRPC equivalent: `DEADLINE_EXCEEDED` (4).
308    Timeout = 0x0003,
309    /// Server's per-service queue is at `max_in_flight` capacity.
310    /// gRPC equivalent: `RESOURCE_EXHAUSTED` (8).
311    Backpressure = 0x0004,
312    /// Caller emitted `DISPATCH_RPC_CANCEL` before the server
313    /// completed.
314    /// gRPC equivalent: `CANCELLED` (1).
315    Cancelled = 0x0005,
316    /// Handler panicked or returned an error not classified as one
317    /// of the above. Payload carries a UTF-8 diagnostic.
318    /// gRPC equivalent: `INTERNAL` (13).
319    Internal = 0x0006,
320    /// Request payload version not supported by the server. Should
321    /// normally be caught earlier by subprotocol-version
322    /// negotiation; the in-payload guard is the floor.
323    /// gRPC equivalent: `UNIMPLEMENTED` (12).
324    UnknownVersion = 0x0007,
325    /// v0.4 capability-auth: the target's `CapabilityAnnouncement`
326    /// either does not list the requested `nrpc:<service>` tag, or
327    /// lists it with allow-lists the caller does not match. See
328    /// `docs/internal/plans/CAPABILITY_AUTH_PLAN.md` §3. Distinct from
329    /// `Unauthorized` (channel-auth / token-scope failures) so
330    /// operators can tell the two enforcement surfaces apart in
331    /// audit logs.
332    /// gRPC equivalent: `PERMISSION_DENIED` (7) — same outward
333    /// shape as `Unauthorized` but a separate substrate code.
334    CapabilityDenied = 0x0008,
335    /// OA-2 org admission (E2.2): a PROTECTED service denied this call —
336    /// the caller's `net-org-admission` proof failed verification, the
337    /// provider cannot admit right now, or the call shape is unsupported.
338    /// A COARSE reason (denied / not-supported / unavailable) rides the
339    /// response; the DETAILED `AdmissionDenied` variant stays provider-
340    /// side audit only, so denial is not a credential oracle. Distinct
341    /// from `CapabilityDenied` (v0.4 allow-list) and `Unauthorized`
342    /// (channel-auth / token-scope) so operators can tell the
343    /// enforcement surfaces apart.
344    /// gRPC equivalent: `PERMISSION_DENIED` (7).
345    AdmissionDenied = 0x0009,
346    /// Application-defined status. The wire carries the raw u16;
347    /// callers / servers agree on the meaning out of band.
348    Application(u16),
349}
350
351impl RpcStatus {
352    /// Encode to the wire `u16`.
353    pub fn to_wire(self) -> u16 {
354        match self {
355            Self::Ok => 0x0000,
356            Self::NotFound => 0x0001,
357            Self::Unauthorized => 0x0002,
358            Self::Timeout => 0x0003,
359            Self::Backpressure => 0x0004,
360            Self::Cancelled => 0x0005,
361            Self::Internal => 0x0006,
362            Self::UnknownVersion => 0x0007,
363            Self::CapabilityDenied => 0x0008,
364            Self::AdmissionDenied => 0x0009,
365            Self::Application(v) => v,
366        }
367    }
368
369    /// Decode from the wire `u16`. Reserved values
370    /// (`0x000A..=0x7FFF`) decode as `Application(v)` rather than
371    /// failing — forward-compat with future status assignments.
372    pub fn from_wire(v: u16) -> Self {
373        match v {
374            0x0000 => Self::Ok,
375            0x0001 => Self::NotFound,
376            0x0002 => Self::Unauthorized,
377            0x0003 => Self::Timeout,
378            0x0004 => Self::Backpressure,
379            0x0005 => Self::Cancelled,
380            0x0006 => Self::Internal,
381            0x0007 => Self::UnknownVersion,
382            0x0008 => Self::CapabilityDenied,
383            0x0009 => Self::AdmissionDenied,
384            other => Self::Application(other),
385        }
386    }
387
388    /// True iff `self == Ok`. Convenience for the hot caller-side
389    /// success-or-error branch.
390    #[inline]
391    pub fn is_ok(self) -> bool {
392        matches!(self, Self::Ok)
393    }
394}
395
396// ============================================================================
397// Request / response payloads.
398//
399// These ride in the bytes AFTER the 24-byte `EventMeta` prefix on a
400// CortEX-adapted event. The cortex adapter handles meta + tail
401// concatenation; this codec produces only the tail.
402// ============================================================================
403
404/// Header name + value pair. Used for trace-context propagation,
405/// idempotency-key carriage, content-type hints. Names are
406/// case-sensitive UTF-8; values are opaque bytes.
407pub type RpcHeader = (String, Vec<u8>);
408
409/// Maximum service-name length on the wire (matches
410/// `MAX_CHANNEL_NAME_LEN` upstream; reasonable upper bound for a
411/// human-readable identifier).
412pub const MAX_RPC_SERVICE_NAME_LEN: usize = 255;
413
414/// Maximum number of headers in a single request or response.
415/// Prevents pathological `headers.len()` reads from a malformed
416/// peer; legitimate callers stay well below this.
417pub const MAX_RPC_HEADERS: usize = 32;
418
419/// Maximum length of a single header name (UTF-8 bytes).
420pub const MAX_RPC_HEADER_NAME_LEN: usize = 64;
421
422/// Maximum length of a single header value (bytes).
423pub const MAX_RPC_HEADER_VALUE_LEN: usize = 4096;
424
425/// Maximum length of a request or response body. Larger payloads
426/// must use streaming responses (Phase 3) or chunk at the
427/// application layer. Comparable to gRPC's default `max_message_size`
428/// of 4 MiB; tuned downward to match RedEX's
429/// `MAX_REDEX_HEAP_PAYLOAD` ceiling.
430pub const MAX_RPC_BODY_LEN: usize = 4 * 1024 * 1024;
431
432/// nRPC request payload. Lives after the 24-byte `EventMeta` prefix
433/// in a `DISPATCH_RPC_REQUEST` event.
434#[derive(Debug, Clone, PartialEq, Eq)]
435pub struct RpcRequestPayload {
436    /// Service-name dispatch key. The server's fold looks this up
437    /// in its `serve_rpc` registry and routes to the registered
438    /// handler.
439    pub service: String,
440    /// Absolute deadline (unix nanos). `0` means no deadline; the
441    /// caller will cancel via `DISPATCH_RPC_CANCEL` if it changes
442    /// its mind.
443    pub deadline_ns: u64,
444    /// Bitfield of `FLAG_RPC_*` constants.
445    pub flags: u16,
446    /// Headers (trace context, idempotency key, content-type, etc.).
447    /// Capped at `MAX_RPC_HEADERS` entries, name <= `MAX_RPC_HEADER_NAME_LEN`,
448    /// value <= `MAX_RPC_HEADER_VALUE_LEN`.
449    pub headers: Vec<RpcHeader>,
450    /// Application-defined request body. Caller and server agree on
451    /// the codec out-of-band; nRPC doesn't interpret these bytes.
452    ///
453    /// Held as [`Bytes`] so [`Self::decode`] can zero-copy `slice_ref`
454    /// the body out of the inbound event's `Bytes` payload — pre-fix
455    /// perf #84 in `docs/internal/performance/net-perf-analysis.md` this was
456    /// `Vec<u8>` and every decode did a `data[body_start..body_end].to_vec()`
457    /// (a memcpy per frame). For high-RPS systems doing 100K+ RPCs/sec
458    /// with 1 KB+ bodies that was 100+ MB/sec of pure memcpy.
459    pub body: Bytes,
460}
461
462/// Continuation chunk for a client-streaming or duplex REQUEST.
463/// Lives after the 24-byte `EventMeta` prefix in a
464/// [`DISPATCH_RPC_REQUEST_CHUNK`] event.
465///
466/// Unlike the initial [`RpcRequestPayload`] there is no
467/// `service` field (server already routed by service at the
468/// initial REQUEST) and no `deadline_ns` (the initial REQUEST's
469/// deadline applies to the whole call). The `call_id` field is
470/// redundant with `EventMeta::seq_or_ts` but kept on the
471/// payload so the codec is self-contained — a reader handed a
472/// chunk's bytes without the meta header can still recover its
473/// correlation id.
474///
475/// Bidi streaming plan (Phase A).
476#[derive(Debug, Clone, PartialEq, Eq)]
477pub struct RpcRequestChunkPayload {
478    /// Matches `EventMeta::seq_or_ts` and the original REQUEST's
479    /// `call_id`. Kept on the payload so the codec round-trips
480    /// in isolation.
481    pub call_id: u64,
482    /// Bitfield of `FLAG_RPC_*` constants. The only flag that
483    /// makes sense on a chunk today is [`FLAG_RPC_REQUEST_END`];
484    /// other flags MUST be zero on the wire so future protocol
485    /// extensions can claim them without colliding with
486    /// existing chunks.
487    pub flags: u16,
488    /// Per-chunk metadata. Typically empty; reserved for
489    /// trace-span continuity across long uploads, content-type
490    /// changes mid-stream, or other rare per-chunk concerns.
491    /// Capped at `MAX_RPC_HEADERS` entries with the same
492    /// per-field caps as `RpcRequestPayload::headers`.
493    pub headers: Vec<RpcHeader>,
494    /// Application-defined chunk body. Cap is `MAX_RPC_BODY_LEN`
495    /// (4 MiB), same as the initial REQUEST body — clients that
496    /// need >4 MiB total payload chunk their upload across
497    /// multiple `REQUEST_CHUNK` events.
498    ///
499    /// See [`RpcRequestPayload::body`] for the `Bytes`-vs-`Vec<u8>`
500    /// rationale.
501    pub body: Bytes,
502}
503
504/// Request-direction credit grant. Lives after the 24-byte
505/// `EventMeta` prefix in a [`DISPATCH_RPC_REQUEST_GRANT`] event.
506/// Mirror of the response-direction [`encode_stream_grant`] /
507/// [`decode_stream_grant`] pair, but with an explicit `call_id`
508/// in the payload (instead of relying solely on
509/// `EventMeta::seq_or_ts`) so the codec is self-contained — same
510/// rationale as [`RpcRequestChunkPayload::call_id`].
511///
512/// Bidi streaming plan (Phase A).
513#[derive(Debug, Clone, Copy, PartialEq, Eq)]
514pub struct RpcRequestGrantPayload {
515    /// Matches the call's `call_id`.
516    pub call_id: u64,
517    /// Additional REQUEST_CHUNK frames the server is willing to
518    /// admit beyond the current credit. Server's incoming-credit
519    /// counter is capped defensively (see PHASE-B server fold)
520    /// so a misbehaving grant can't overflow.
521    pub credits: u32,
522}
523
524/// nRPC response payload. Lives after the 24-byte `EventMeta`
525/// prefix in a `DISPATCH_RPC_RESPONSE` event.
526#[derive(Debug, Clone, PartialEq, Eq)]
527pub struct RpcResponsePayload {
528    /// Outcome of the call. Decoded on the caller side via
529    /// [`RpcStatus::from_wire`].
530    pub status: RpcStatus,
531    /// Headers (trace context, content-type, content-encoding,
532    /// etc.). Same caps as `RpcRequestPayload::headers`.
533    pub headers: Vec<RpcHeader>,
534    /// For `status == Ok`: the application response body.
535    /// For non-`Ok` statuses: a UTF-8 diagnostic string (callers
536    /// `String::from_utf8_lossy` for display; the bytes are not
537    /// guaranteed to be valid UTF-8 against a malicious server).
538    ///
539    /// See [`RpcRequestPayload::body`] for the `Bytes`-vs-`Vec<u8>`
540    /// rationale.
541    pub body: Bytes,
542}
543
544// ============================================================================
545// Codec.
546//
547// All wire integers are little-endian. Lengths are u32_le where the
548// upper bound exceeds u16, u16_le where it fits, u8 where it fits.
549// ============================================================================
550
551/// Errors from the request / response codecs.
552#[derive(Debug, thiserror::Error)]
553pub enum RpcCodecError {
554    /// Buffer ended mid-field.
555    #[error("truncated payload at {0}")]
556    Truncated(&'static str),
557    /// Length prefix exceeds the configured maximum.
558    #[error("length {actual} exceeds limit {limit} for {field}")]
559    TooLarge {
560        /// Field name whose declared length exceeded the cap (e.g.
561        /// `"body"`, `"headers"`, `"service"`). Stable strings —
562        /// callers may match on them for diagnostics.
563        field: &'static str,
564        /// The length the wire claimed for the field.
565        actual: usize,
566        /// The maximum the codec accepts (one of the `MAX_RPC_*`
567        /// constants).
568        limit: usize,
569    },
570    /// String field contains non-UTF-8 bytes.
571    #[error("non-utf8 string in {0}")]
572    InvalidUtf8(&'static str),
573}
574
575impl RpcRequestPayload {
576    /// Validate every length field against the wire ceilings the codec's
577    /// `u8` / `u16` / `u32` length prefixes assume, BEFORE any encode.
578    /// In release builds [`Self::encode_into`] truncates an over-cap
579    /// length via its `as u8` / `as u16` / `as u32` casts (debug builds
580    /// `debug_assert`), which would let an oversized, publicly-
581    /// constructed request produce an ambiguous / non-round-tripping
582    /// encoding. Any caller that must NOT silently truncate — the org
583    /// request digest (AV-7 item 7), and any future checked send path —
584    /// validates first and refuses rather than hashing a collision-prone
585    /// encoding.
586    pub fn validate_wire_bounds(&self) -> Result<(), RpcCodecError> {
587        if self.service.len() > MAX_RPC_SERVICE_NAME_LEN {
588            return Err(RpcCodecError::TooLarge {
589                field: "service",
590                actual: self.service.len(),
591                limit: MAX_RPC_SERVICE_NAME_LEN,
592            });
593        }
594        if self.headers.len() > MAX_RPC_HEADERS {
595            return Err(RpcCodecError::TooLarge {
596                field: "headers",
597                actual: self.headers.len(),
598                limit: MAX_RPC_HEADERS,
599            });
600        }
601        for (name, value) in &self.headers {
602            if name.len() > MAX_RPC_HEADER_NAME_LEN {
603                return Err(RpcCodecError::TooLarge {
604                    field: "header name",
605                    actual: name.len(),
606                    limit: MAX_RPC_HEADER_NAME_LEN,
607                });
608            }
609            if value.len() > MAX_RPC_HEADER_VALUE_LEN {
610                return Err(RpcCodecError::TooLarge {
611                    field: "header value",
612                    actual: value.len(),
613                    limit: MAX_RPC_HEADER_VALUE_LEN,
614                });
615            }
616        }
617        if self.body.len() > MAX_RPC_BODY_LEN {
618            return Err(RpcCodecError::TooLarge {
619                field: "body",
620                actual: self.body.len(),
621                limit: MAX_RPC_BODY_LEN,
622            });
623        }
624        Ok(())
625    }
626
627    /// Compute the encoded byte length WITHOUT actually encoding.
628    /// Used by [`request_wire_size`] and any caller that needs to
629    /// budget event size at the bus layer (e.g., to refuse a
630    /// request that wouldn't fit in the configured packet budget)
631    /// without paying the encode cost.
632    pub fn encoded_len(&self) -> usize {
633        // service: u8 length + bytes
634        1 + self.service.len()
635            // deadline_ns: u64
636            + 8
637            // flags: u16
638            + 2
639            // headers: u8 count + per-header (u8 name_len + name + u16 value_len + value)
640            + 1
641            + self
642                .headers
643                .iter()
644                .map(|(n, v)| 1 + n.len() + 2 + v.len())
645                .sum::<usize>()
646            // body: u32 length + bytes
647            + 4
648            + self.body.len()
649    }
650
651    /// Encode to the wire format. The result is the bytes that
652    /// follow the 24-byte `EventMeta` prefix in the RedEX payload.
653    ///
654    /// **Encoder bounds:** every field that has a `MAX_RPC_*` cap
655    /// is asserted against that cap. In debug builds an oversize
656    /// field panics with a useful diagnostic so the programmer
657    /// notices in tests; in release builds the assert is dropped
658    /// (the decoder side still enforces the cap, so a malformed
659    /// frame would be rejected by the receiver — but constructing
660    /// one is always a caller bug).
661    pub fn encode(&self) -> Vec<u8> {
662        let mut buf = Vec::with_capacity(self.encoded_len());
663        self.encode_into(&mut buf);
664        buf
665    }
666
667    /// Encode directly into `buf`, appending the wire bytes (audit T2.2).
668    /// Callers that already hold an `EventMeta`-prefixed buffer use this to
669    /// skip `encode()`'s intermediate `Vec` allocation + copy. Produces the
670    /// identical bytes `encode()` returns. Pre-`reserve(encoded_len())` for an
671    /// exact-fit single allocation when starting from an empty buffer.
672    pub fn encode_into(&self, buf: &mut Vec<u8>) {
673        // service
674        let svc = self.service.as_bytes();
675        debug_assert!(
676            svc.len() <= MAX_RPC_SERVICE_NAME_LEN,
677            "service name {} exceeds MAX_RPC_SERVICE_NAME_LEN ({})",
678            svc.len(),
679            MAX_RPC_SERVICE_NAME_LEN,
680        );
681        buf.put_u8(svc.len() as u8);
682        buf.extend_from_slice(svc);
683        // deadline_ns
684        buf.put_u64_le(self.deadline_ns);
685        // flags
686        buf.put_u16_le(self.flags);
687        // headers
688        encode_headers(&self.headers, buf);
689        // body
690        debug_assert!(
691            self.body.len() <= MAX_RPC_BODY_LEN,
692            "body length {} exceeds MAX_RPC_BODY_LEN ({})",
693            self.body.len(),
694            MAX_RPC_BODY_LEN,
695        );
696        buf.put_u32_le(self.body.len() as u32);
697        buf.extend_from_slice(&self.body);
698    }
699
700    /// Decode from the wire bytes following the `EventMeta` prefix.
701    /// All length fields are bounded by the `MAX_RPC_*` constants;
702    /// over-cap inputs error rather than allocate unbounded
703    /// buffers.
704    ///
705    /// Takes [`Bytes`] (not `&[u8]`) so the decoded `body` field
706    /// can be a zero-copy `data.slice(..)` instead of an owned
707    /// `to_vec` — see perf #84.
708    pub fn decode(data: Bytes) -> Result<Self, RpcCodecError> {
709        let mut cur = std::io::Cursor::new(data.as_ref());
710        // service
711        if cur.remaining() < 1 {
712            return Err(RpcCodecError::Truncated("service length"));
713        }
714        let svc_len = cur.get_u8() as usize;
715        if svc_len == 0 {
716            return Err(RpcCodecError::Truncated("empty service name"));
717        }
718        if svc_len > MAX_RPC_SERVICE_NAME_LEN {
719            return Err(RpcCodecError::TooLarge {
720                field: "service",
721                actual: svc_len,
722                limit: MAX_RPC_SERVICE_NAME_LEN,
723            });
724        }
725        if cur.remaining() < svc_len {
726            return Err(RpcCodecError::Truncated("service bytes"));
727        }
728        let svc_start = cur.position() as usize;
729        let svc_end = svc_start + svc_len;
730        let service = std::str::from_utf8(&data[svc_start..svc_end])
731            .map_err(|_| RpcCodecError::InvalidUtf8("service"))?
732            .to_string();
733        cur.set_position(svc_end as u64);
734        // deadline_ns
735        if cur.remaining() < 8 {
736            return Err(RpcCodecError::Truncated("deadline_ns"));
737        }
738        let deadline_ns = cur.get_u64_le();
739        // flags
740        if cur.remaining() < 2 {
741            return Err(RpcCodecError::Truncated("flags"));
742        }
743        let flags = cur.get_u16_le();
744        // headers
745        let headers = decode_headers(&mut cur, &data)?;
746        // body
747        if cur.remaining() < 4 {
748            return Err(RpcCodecError::Truncated("body length"));
749        }
750        let body_len = cur.get_u32_le() as usize;
751        if body_len > MAX_RPC_BODY_LEN {
752            return Err(RpcCodecError::TooLarge {
753                field: "body",
754                actual: body_len,
755                limit: MAX_RPC_BODY_LEN,
756            });
757        }
758        if cur.remaining() < body_len {
759            return Err(RpcCodecError::Truncated("body bytes"));
760        }
761        let body_start = cur.position() as usize;
762        let body_end = body_start + body_len;
763        // Zero-copy slice over the input — refcount bump only.
764        let body = data.slice(body_start..body_end);
765        Ok(Self {
766            service,
767            deadline_ns,
768            flags,
769            headers,
770            body,
771        })
772    }
773}
774
775impl RpcRequestChunkPayload {
776    /// Compute the encoded byte length WITHOUT actually encoding.
777    /// See [`RpcRequestPayload::encoded_len`] for the rationale.
778    pub fn encoded_len(&self) -> usize {
779        // call_id: u64
780        8
781            // flags: u16
782            + 2
783            // headers: u8 count + per-header (u8 name_len + name + u16 value_len + value)
784            + 1
785            + self
786                .headers
787                .iter()
788                .map(|(n, v)| 1 + n.len() + 2 + v.len())
789                .sum::<usize>()
790            // body: u32 length + bytes
791            + 4
792            + self.body.len()
793    }
794
795    /// Encode to the wire bytes that follow the 24-byte `EventMeta`
796    /// prefix in a [`DISPATCH_RPC_REQUEST_CHUNK`] event. Same
797    /// encoder-bounds policy as [`RpcRequestPayload::encode`]:
798    /// oversize fields panic in debug, the decoder enforces in
799    /// release.
800    pub fn encode(&self) -> Vec<u8> {
801        let mut buf = Vec::with_capacity(self.encoded_len());
802        self.encode_into(&mut buf);
803        buf
804    }
805
806    /// Encode directly into `buf`, appending the wire bytes (audit T2.2).
807    /// See [`RpcRequestPayload::encode_into`].
808    pub fn encode_into(&self, buf: &mut Vec<u8>) {
809        // call_id
810        buf.put_u64_le(self.call_id);
811        // flags
812        buf.put_u16_le(self.flags);
813        // headers
814        encode_headers(&self.headers, buf);
815        // body
816        debug_assert!(
817            self.body.len() <= MAX_RPC_BODY_LEN,
818            "body length {} exceeds MAX_RPC_BODY_LEN ({})",
819            self.body.len(),
820            MAX_RPC_BODY_LEN,
821        );
822        buf.put_u32_le(self.body.len() as u32);
823        buf.extend_from_slice(&self.body);
824    }
825
826    /// Decode from the wire bytes following the `EventMeta` prefix.
827    /// Bounded by the same `MAX_RPC_*` caps as the initial REQUEST.
828    /// Takes [`Bytes`] for zero-copy `body` slicing — see perf #84.
829    pub fn decode(data: Bytes) -> Result<Self, RpcCodecError> {
830        let mut cur = std::io::Cursor::new(data.as_ref());
831        // call_id
832        if cur.remaining() < 8 {
833            return Err(RpcCodecError::Truncated("call_id"));
834        }
835        let call_id = cur.get_u64_le();
836        // flags
837        if cur.remaining() < 2 {
838            return Err(RpcCodecError::Truncated("flags"));
839        }
840        let flags = cur.get_u16_le();
841        // headers
842        let headers = decode_headers(&mut cur, &data)?;
843        // body
844        if cur.remaining() < 4 {
845            return Err(RpcCodecError::Truncated("body length"));
846        }
847        let body_len = cur.get_u32_le() as usize;
848        if body_len > MAX_RPC_BODY_LEN {
849            return Err(RpcCodecError::TooLarge {
850                field: "body",
851                actual: body_len,
852                limit: MAX_RPC_BODY_LEN,
853            });
854        }
855        if cur.remaining() < body_len {
856            return Err(RpcCodecError::Truncated("body bytes"));
857        }
858        let body_start = cur.position() as usize;
859        let body_end = body_start + body_len;
860        let body = data.slice(body_start..body_end);
861        Ok(Self {
862            call_id,
863            flags,
864            headers,
865            body,
866        })
867    }
868}
869
870impl RpcResponsePayload {
871    /// Compute the encoded byte length WITHOUT actually encoding.
872    /// See [`RpcRequestPayload::encoded_len`].
873    pub fn encoded_len(&self) -> usize {
874        // status: u16
875        2
876            // headers: u8 count + per-header
877            + 1
878            + self
879                .headers
880                .iter()
881                .map(|(n, v)| 1 + n.len() + 2 + v.len())
882                .sum::<usize>()
883            // body: u32 length + bytes
884            + 4
885            + self.body.len()
886    }
887
888    /// Encode to the wire format. The result is the bytes that
889    /// follow the 24-byte `EventMeta` prefix in the RedEX payload.
890    /// Same encoder-bounds policy as
891    /// [`RpcRequestPayload::encode`] — see that method's doc.
892    pub fn encode(&self) -> Vec<u8> {
893        let mut buf = Vec::with_capacity(self.encoded_len());
894        self.encode_into(&mut buf);
895        buf
896    }
897
898    /// Encode directly into `buf`, appending the wire bytes (audit T2.2).
899    /// See [`RpcRequestPayload::encode_into`].
900    pub fn encode_into(&self, buf: &mut Vec<u8>) {
901        buf.put_u16_le(self.status.to_wire());
902        encode_headers(&self.headers, buf);
903        debug_assert!(
904            self.body.len() <= MAX_RPC_BODY_LEN,
905            "body length {} exceeds MAX_RPC_BODY_LEN ({})",
906            self.body.len(),
907            MAX_RPC_BODY_LEN,
908        );
909        buf.put_u32_le(self.body.len() as u32);
910        buf.extend_from_slice(&self.body);
911    }
912
913    /// Decode from the wire bytes following the `EventMeta` prefix.
914    /// Takes [`Bytes`] for zero-copy `body` slicing — see perf #84.
915    pub fn decode(data: Bytes) -> Result<Self, RpcCodecError> {
916        let mut cur = std::io::Cursor::new(data.as_ref());
917        if cur.remaining() < 2 {
918            return Err(RpcCodecError::Truncated("status"));
919        }
920        let status = RpcStatus::from_wire(cur.get_u16_le());
921        let headers = decode_headers(&mut cur, &data)?;
922        if cur.remaining() < 4 {
923            return Err(RpcCodecError::Truncated("body length"));
924        }
925        let body_len = cur.get_u32_le() as usize;
926        if body_len > MAX_RPC_BODY_LEN {
927            return Err(RpcCodecError::TooLarge {
928                field: "body",
929                actual: body_len,
930                limit: MAX_RPC_BODY_LEN,
931            });
932        }
933        if cur.remaining() < body_len {
934            return Err(RpcCodecError::Truncated("body bytes"));
935        }
936        let body_start = cur.position() as usize;
937        let body_end = body_start + body_len;
938        let body = data.slice(body_start..body_end);
939        Ok(Self {
940            status,
941            headers,
942            body,
943        })
944    }
945}
946
947/// Pull `traceparent` / `tracestate` out of `headers` if present.
948/// Caller-side helper: callers building an `RpcRequestPayload`
949/// with a `TraceContext` use [`build_trace_headers`] to emit the
950/// matching headers; this is the inverse on the server side.
951///
952/// Returns `Some(TraceContext)` if `traceparent` is present;
953/// `None` otherwise. `tracestate` defaults to empty when absent
954/// — W3C says tracestate is optional even when traceparent is
955/// set.
956pub fn extract_trace_context(headers: &[RpcHeader]) -> Option<TraceContext> {
957    let mut traceparent: Option<String> = None;
958    let mut tracestate = String::new();
959    for (name, value) in headers {
960        // Header names are case-insensitive (matches W3C and HTTP
961        // convention) — same comparison style as `parse_stream_
962        // window_initial` for consistency. The wire spec doesn't
963        // mandate case so a peer that emits `Traceparent` (capital
964        // T) shouldn't be silently ignored.
965        if name.eq_ignore_ascii_case("traceparent") {
966            if let Ok(s) = std::str::from_utf8(value) {
967                traceparent = Some(s.to_string());
968            }
969        } else if name.eq_ignore_ascii_case("tracestate") {
970            if let Ok(s) = std::str::from_utf8(value) {
971                tracestate = s.to_string();
972            }
973        }
974    }
975    traceparent.map(|tp| TraceContext {
976        traceparent: tp,
977        tracestate,
978    })
979}
980
981/// Build the headers a caller appends to its
982/// `RpcRequestPayload::headers` to propagate the trace context
983/// across the call. Set `RpcRequestPayload::flags |= FLAG_RPC_PROPAGATE_TRACE`
984/// alongside this so the server's fold knows to extract them.
985///
986/// Always emits `traceparent`. Emits `tracestate` only when
987/// non-empty (matches the W3C convention of skipping empty
988/// tracestate values on the wire).
989pub fn build_trace_headers(ctx: &TraceContext) -> Vec<RpcHeader> {
990    let mut headers = Vec::with_capacity(2);
991    headers.push((
992        "traceparent".to_string(),
993        ctx.traceparent.clone().into_bytes(),
994    ));
995    if !ctx.tracestate.is_empty() {
996        headers.push((
997            "tracestate".to_string(),
998            ctx.tracestate.clone().into_bytes(),
999        ));
1000    }
1001    headers
1002}
1003
1004fn encode_headers(headers: &[RpcHeader], buf: &mut Vec<u8>) {
1005    debug_assert!(
1006        headers.len() <= MAX_RPC_HEADERS,
1007        "headers count {} exceeds MAX_RPC_HEADERS ({})",
1008        headers.len(),
1009        MAX_RPC_HEADERS,
1010    );
1011    buf.put_u8(headers.len() as u8);
1012    for (name, value) in headers {
1013        let nbytes = name.as_bytes();
1014        debug_assert!(
1015            nbytes.len() <= MAX_RPC_HEADER_NAME_LEN,
1016            "header name {} exceeds MAX_RPC_HEADER_NAME_LEN ({})",
1017            nbytes.len(),
1018            MAX_RPC_HEADER_NAME_LEN,
1019        );
1020        debug_assert!(
1021            value.len() <= MAX_RPC_HEADER_VALUE_LEN,
1022            "header value {} exceeds MAX_RPC_HEADER_VALUE_LEN ({})",
1023            value.len(),
1024            MAX_RPC_HEADER_VALUE_LEN,
1025        );
1026        buf.put_u8(nbytes.len() as u8);
1027        buf.extend_from_slice(nbytes);
1028        buf.put_u16_le(value.len() as u16);
1029        buf.extend_from_slice(value);
1030    }
1031}
1032
1033fn decode_headers(
1034    cur: &mut std::io::Cursor<&[u8]>,
1035    data: &[u8],
1036) -> Result<Vec<RpcHeader>, RpcCodecError> {
1037    if cur.remaining() < 1 {
1038        return Err(RpcCodecError::Truncated("headers count"));
1039    }
1040    let count = cur.get_u8() as usize;
1041    if count > MAX_RPC_HEADERS {
1042        return Err(RpcCodecError::TooLarge {
1043            field: "headers",
1044            actual: count,
1045            limit: MAX_RPC_HEADERS,
1046        });
1047    }
1048    let mut headers = Vec::with_capacity(count);
1049    for _ in 0..count {
1050        if cur.remaining() < 1 {
1051            return Err(RpcCodecError::Truncated("header name length"));
1052        }
1053        let name_len = cur.get_u8() as usize;
1054        if name_len == 0 {
1055            return Err(RpcCodecError::Truncated("empty header name"));
1056        }
1057        if name_len > MAX_RPC_HEADER_NAME_LEN {
1058            return Err(RpcCodecError::TooLarge {
1059                field: "header name",
1060                actual: name_len,
1061                limit: MAX_RPC_HEADER_NAME_LEN,
1062            });
1063        }
1064        if cur.remaining() < name_len {
1065            return Err(RpcCodecError::Truncated("header name bytes"));
1066        }
1067        let nstart = cur.position() as usize;
1068        let nend = nstart + name_len;
1069        let name = std::str::from_utf8(&data[nstart..nend])
1070            .map_err(|_| RpcCodecError::InvalidUtf8("header name"))?
1071            .to_string();
1072        cur.set_position(nend as u64);
1073
1074        if cur.remaining() < 2 {
1075            return Err(RpcCodecError::Truncated("header value length"));
1076        }
1077        let value_len = cur.get_u16_le() as usize;
1078        if value_len > MAX_RPC_HEADER_VALUE_LEN {
1079            return Err(RpcCodecError::TooLarge {
1080                field: "header value",
1081                actual: value_len,
1082                limit: MAX_RPC_HEADER_VALUE_LEN,
1083            });
1084        }
1085        if cur.remaining() < value_len {
1086            return Err(RpcCodecError::Truncated("header value bytes"));
1087        }
1088        let vstart = cur.position() as usize;
1089        let vend = vstart + value_len;
1090        let value = data[vstart..vend].to_vec();
1091        cur.set_position(vend as u64);
1092        headers.push((name, value));
1093    }
1094    Ok(headers)
1095}
1096
1097/// Convenience: the byte layout of an `RpcRequestPayload` that lands
1098/// after the `EventMeta` prefix in a `DISPATCH_RPC_REQUEST` event.
1099/// Exposed so callers can budget the total event size at the bus
1100/// layer without doing the encode first.
1101pub fn request_wire_size(payload: &RpcRequestPayload) -> usize {
1102    // OA2-E0.2: EventMeta + RpcRouteV1 discriminator + payload.
1103    RPC_FRAME_BODY_OFFSET + payload.encoded_len()
1104}
1105
1106/// Same for `RpcResponsePayload` after the `EventMeta` prefix in a
1107/// `DISPATCH_RPC_RESPONSE` event.
1108pub fn response_wire_size(payload: &RpcResponsePayload) -> usize {
1109    RPC_FRAME_BODY_OFFSET + payload.encoded_len()
1110}
1111
1112// ============================================================================
1113// Mesh inbound dispatch hook.
1114//
1115// `MeshNode::dispatch_packet` normally pushes inbound channel
1116// events onto a per-shard `inbound` queue keyed by `shard_id`. The
1117// channel name / hash is stripped on the way in — by the time the
1118// event lands in the queue, only the payload remains.
1119//
1120// RPC needs per-channel routing (events for `<service>.requests`
1121// drive the server fold; events for `<service>.replies.<origin>`
1122// drive the client fold). Without channel info on the queued
1123// event, we can't filter from the consumer side.
1124//
1125// The hook below adds a per-channel-hash dispatcher map that the
1126// mesh's inbound dispatch consults BEFORE pushing to the shard
1127// queue. If a dispatcher is registered for the event's
1128// canonical [`ChannelHash`], the event is routed there directly
1129// (bypassing the shard queue); otherwise the existing shard-queue
1130// path runs.
1131//
1132// **Collision posture.** The dispatch event carries the canonical
1133// 32-bit [`ChannelHash`] (joint-collision threshold ~65 K
1134// channels, well above realistic deployment); the wire
1135// `NetHeader::channel_hash` is `u16` and may bucket-collide at
1136// scale, so the mesh's inbound dispatch indexes by the wire `u16`
1137// and dispatches every canonical entry registered in that bucket
1138// (the canonical match resolves on the dispatcher side). At
1139// typical sizing this is a single entry per bucket.
1140// ============================================================================
1141
1142/// One inbound event delivered to a registered RPC dispatcher.
1143#[derive(Debug, Clone)]
1144pub struct RpcInboundEvent {
1145    /// Canonical [`ChannelHash`](crate::adapter::net::channel::ChannelHash)
1146    /// (u32) of the channel this event arrived on — widened from the
1147    /// per-packet wire `u16` `NetHeader::channel_hash` via the
1148    /// registered-dispatcher table at receive time.
1149    /// Collision-resistant at realistic scale; the wire `u16` may
1150    /// bucket-collide but the canonical hash uniquely identifies the
1151    /// registered dispatcher target.
1152    pub channel_hash: super::super::channel::ChannelHash,
1153    /// Caller's `origin_hash` from the packet header — the full
1154    /// 64-bit `EntityKeypair::origin_hash()` mirroring the wire
1155    /// field's width post-`WIRE_ORIGIN_HASH_64BIT`. The dispatcher
1156    /// should treat this as routing metadata, not identity
1157    /// authentication (the AEAD-verified `session_node` field
1158    /// below carries that).
1159    pub origin_hash: u64,
1160    /// Wire-session peer's `NodeId` resolved at packet receive
1161    /// time from the AEAD-verified session_id. Distinct from
1162    /// `origin_hash`: this is the full 64-bit network identity
1163    /// of the peer that delivered the packet. Used by
1164    /// `RpcClientPending::deliver`
1165    /// to reject spoofed RESPONSE frames whose call_id happens
1166    /// to match an in-flight request but whose session peer
1167    /// isn't the recorded target.
1168    ///
1169    /// Set to `0` on test / loopback paths that don't have a
1170    /// session to resolve against; callers that register
1171    /// pending entries with `target_node = 0` opt out of the
1172    /// binding gate (and trust the call_id randomness alone).
1173    ///
1174    /// **Production wire-path invariant**: real over-the-wire
1175    /// inbound delivery MUST NOT produce `from_node = 0`. The
1176    /// dispatcher in `mesh.rs` (`handle_inbound_user_payload`)
1177    /// drops the event when the wire session has no resolvable
1178    /// `NodeId`, rather than forwarding under the sentinel — see
1179    /// the explicit drop + warn at the
1180    /// `dropping cortex-RPC event: wire session has no resolvable NodeId`
1181    /// log site. The v0.4 capability-auth callee-side gate in
1182    /// `MeshNode::serve_rpc`'s bridge relies on this: it skips
1183    /// permissively when `from_node == 0` (loopback compat), so
1184    /// a wire-path leak of the sentinel would silently re-open
1185    /// the gate. If you change the dispatcher to fall back to 0
1186    /// instead of dropping, you ALSO have to teach the bridge
1187    /// to deny on the sentinel.
1188    pub from_node: super::super::behavior::placement::NodeId,
1189    /// Event payload bytes — the same bytes that would have been
1190    /// pushed onto the shard inbound queue. For RPC events these
1191    /// start with a 24-byte `EventMeta` followed by the
1192    /// `RpcRequestPayload` / `RpcResponsePayload` encoding.
1193    pub payload: bytes::Bytes,
1194}
1195
1196/// Type-erased callback fired by the mesh's inbound dispatch
1197/// when an event arrives for a registered `channel_hash`. The
1198/// callback runs on the mesh's dispatch task, so the body should
1199/// be quick (push the event onto an mpsc / fold consumer rather
1200/// than do real work).
1201pub type RpcInboundDispatcher = Arc<dyn Fn(RpcInboundEvent) + Send + Sync + 'static>;
1202
1203// ============================================================================
1204// Streaming-response protocol markers.
1205//
1206// When a caller sets `FLAG_RPC_STREAMING_RESPONSE` on the request,
1207// the server emits multiple `DISPATCH_RPC_RESPONSE` events for the
1208// same `call_id`. Non-terminal chunks carry the
1209// `nrpc-streaming = continue` header; the terminal chunk carries
1210// `nrpc-streaming = end` (or any non-`Ok` status, which is also
1211// terminal). The client-side stream collects chunks until it sees
1212// a terminal marker.
1213// ============================================================================
1214
1215/// Header name nRPC uses to mark streaming-response chunks.
1216/// Present on every chunk of a streaming response, with one of two
1217/// values defined below.
1218pub const HEADER_NRPC_STREAMING: &str = "nrpc-streaming";
1219
1220/// `nrpc-streaming` value on a non-terminal chunk. The client-side
1221/// stream yields the chunk's body and continues waiting for more.
1222pub const HEADER_NRPC_STREAMING_CONTINUE: &[u8] = b"continue";
1223
1224/// `nrpc-streaming` value on the terminal chunk. The client-side
1225/// stream yields the chunk's body (if non-empty) and then closes.
1226/// A non-`Ok` status is also terminal, regardless of header — the
1227/// stream yields the error and closes.
1228pub const HEADER_NRPC_STREAMING_END: &[u8] = b"end";
1229
1230/// Header on a streaming REQUEST that opts into flow control with
1231/// the given initial credit window. Value is the ASCII decimal
1232/// representation of a `u32` (e.g. `"32"`). When present, the
1233/// server's streaming fold creates a per-call semaphore initialized
1234/// to that count and the pump awaits one credit per emitted chunk.
1235/// The caller refills via [`DISPATCH_RPC_STREAM_GRANT`] events.
1236///
1237/// Absent → unbounded credit (the pump emits chunks as fast as
1238/// the publish path can take them). Long-running streams that
1239/// could outpace a slow consumer SHOULD opt into flow control —
1240/// without it, the server's sink mpsc grows unbounded under a
1241/// stalled caller.
1242pub const HEADER_NRPC_STREAM_WINDOW_INITIAL: &str = "nrpc-stream-window-initial";
1243
1244/// Encode a stream-grant payload — 4 bytes big-endian `u32`
1245/// representing additional credit. Pair with [`decode_stream_grant`]
1246/// on the server side.
1247pub fn encode_stream_grant(amount: u32) -> Vec<u8> {
1248    amount.to_be_bytes().to_vec()
1249}
1250
1251/// Decode a stream-grant payload. Returns `None` if the slice is
1252/// not exactly 4 bytes — defends the server fold against
1253/// malformed grants without killing the cortex adapter.
1254pub fn decode_stream_grant(payload: &[u8]) -> Option<u32> {
1255    if payload.len() != 4 {
1256        return None;
1257    }
1258    let mut bytes = [0u8; 4];
1259    bytes.copy_from_slice(payload);
1260    Some(u32::from_be_bytes(bytes))
1261}
1262
1263/// Parse the `nrpc-stream-window-initial` header from a request's
1264/// header list. Returns `Some(window)` if a valid u32 ASCII-decimal
1265/// value is present, else `None` (no header / malformed value /
1266/// non-utf8 — all treated as "no flow control").
1267pub fn parse_stream_window_initial(headers: &[RpcHeader]) -> Option<u32> {
1268    for (name, value) in headers {
1269        if name.eq_ignore_ascii_case(HEADER_NRPC_STREAM_WINDOW_INITIAL) {
1270            return std::str::from_utf8(value).ok()?.parse::<u32>().ok();
1271        }
1272    }
1273    None
1274}
1275
1276/// Header on the initial REQUEST of a client-streaming or duplex
1277/// call that opts the upload direction into flow control with the
1278/// given initial credit window. Value is the ASCII decimal
1279/// representation of a `u32`. When present, the server's
1280/// streaming-request fold creates a per-call semaphore and the
1281/// caller's sink awaits one credit per `REQUEST_CHUNK`. The server
1282/// refills via [`DISPATCH_RPC_REQUEST_GRANT`] events.
1283///
1284/// Absent → unbounded credit (caller's sink emits as fast as the
1285/// publish path can take it). Long client-streaming calls that
1286/// could outpace a slow handler SHOULD opt into flow control —
1287/// without it, the server's chunk mpsc grows unbounded under a
1288/// stalled handler.
1289///
1290/// Bidi streaming plan (Phase A).
1291pub const HEADER_NRPC_REQUEST_WINDOW_INITIAL: &str = "nrpc-request-window-initial";
1292
1293/// Encode a request-grant payload — `call_id` (u64 little-endian)
1294/// followed by additional credit (u32 big-endian). Big-endian on
1295/// the credit field matches [`encode_stream_grant`]; little-endian
1296/// on `call_id` matches the rest of the RPC codec's u64 fields.
1297///
1298/// Pair with [`decode_request_grant`] on the caller side.
1299pub fn encode_request_grant(call_id: u64, credits: u32) -> Vec<u8> {
1300    let mut buf = Vec::with_capacity(12);
1301    buf.put_u64_le(call_id);
1302    buf.extend_from_slice(&credits.to_be_bytes());
1303    buf
1304}
1305
1306/// Decode a request-grant payload. Returns `None` if the slice is
1307/// not exactly 12 bytes — defends the caller's fold against
1308/// malformed grants without killing the cortex adapter.
1309pub fn decode_request_grant(payload: &[u8]) -> Option<RpcRequestGrantPayload> {
1310    if payload.len() != 12 {
1311        return None;
1312    }
1313    let mut cid = [0u8; 8];
1314    cid.copy_from_slice(&payload[..8]);
1315    let call_id = u64::from_le_bytes(cid);
1316    let mut credits = [0u8; 4];
1317    credits.copy_from_slice(&payload[8..]);
1318    Some(RpcRequestGrantPayload {
1319        call_id,
1320        credits: u32::from_be_bytes(credits),
1321    })
1322}
1323
1324/// Parse the `nrpc-request-window-initial` header from a request's
1325/// header list. Same semantics as [`parse_stream_window_initial`]
1326/// but for the upload direction.
1327pub fn parse_request_window_initial(headers: &[RpcHeader]) -> Option<u32> {
1328    for (name, value) in headers {
1329        if name.eq_ignore_ascii_case(HEADER_NRPC_REQUEST_WINDOW_INITIAL) {
1330            return std::str::from_utf8(value).ok()?.parse::<u32>().ok();
1331        }
1332    }
1333    None
1334}
1335
1336/// Inspect a `RpcResponsePayload`'s headers and decide whether
1337/// it's a non-terminal streaming chunk (`continue`), a terminal
1338/// streaming chunk (`end` OR non-`Ok` status), OR a unary
1339/// response (no streaming header at all). Used by the client-side
1340/// fold to demux streaming vs unary responses without needing a
1341/// separate flag.
1342#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1343pub enum StreamingChunkKind {
1344    /// Non-terminal chunk — yield body, continue waiting.
1345    Continue,
1346    /// Terminal chunk — yield body (if any), close stream.
1347    Terminal,
1348    /// Not a streaming response — unary semantics apply.
1349    Unary,
1350}
1351
1352/// Classify a response per the streaming-protocol markers.
1353pub fn classify_streaming_chunk(resp: &RpcResponsePayload) -> StreamingChunkKind {
1354    // Non-Ok status is always terminal regardless of header — the
1355    // stream surfaces the error and closes.
1356    if !resp.status.is_ok() {
1357        return StreamingChunkKind::Terminal;
1358    }
1359    // Walk headers for the streaming marker. Absence = unary
1360    // semantics (caller used `call`, not `call_streaming`).
1361    for (name, value) in &resp.headers {
1362        if name == HEADER_NRPC_STREAMING {
1363            return if value.as_slice() == HEADER_NRPC_STREAMING_END {
1364                StreamingChunkKind::Terminal
1365            } else if value.as_slice() == HEADER_NRPC_STREAMING_CONTINUE {
1366                StreamingChunkKind::Continue
1367            } else {
1368                // Unknown marker value — be defensive, treat as
1369                // terminal so a misbehaving server doesn't keep
1370                // a stream open forever.
1371                StreamingChunkKind::Terminal
1372            };
1373        }
1374    }
1375    StreamingChunkKind::Unary
1376}
1377
1378// ============================================================================
1379// Server-side fold.
1380//
1381// `RpcServerFold` is the `RedexFold` half of the server. It sees
1382// REQUEST events on the channel the cortex adapter is opened against,
1383// spawns the user handler in a tokio task, and emits the RESPONSE
1384// via a callback the `Mesh::serve_rpc` glue layer wires up. The
1385// fold itself is small and pure — all I/O happens in the spawned
1386// task and the emitter callback.
1387//
1388// Cancellation: each in-flight call gets an `RpcCancellationToken`
1389// that the handler can `select!` on. CANCEL events flip the
1390// matching token; the handler observes `cancellation.cancelled()`
1391// firing and aborts cooperatively.
1392// ============================================================================
1393
1394/// Cancellation signal for an in-flight RPC handler.
1395///
1396/// Created when the fold dispatches a REQUEST; cloned into the
1397/// handler's `RpcContext` and held in the fold's in-flight map. A
1398/// matching CANCEL event flips the token; handlers observe via
1399/// either [`Self::is_cancelled`] (synchronous probe) or
1400/// [`Self::cancelled`] (await for the signal).
1401#[derive(Clone, Default)]
1402pub struct RpcCancellationToken {
1403    inner: Arc<RpcCancellationInner>,
1404}
1405
1406#[derive(Default)]
1407struct RpcCancellationInner {
1408    fired: AtomicBool,
1409    notify: Notify,
1410}
1411
1412impl RpcCancellationToken {
1413    /// Construct a fresh, un-fired token.
1414    pub fn new() -> Self {
1415        Self::default()
1416    }
1417
1418    /// Flip the token. Idempotent — repeated calls are no-ops.
1419    /// Wakes any task currently in [`Self::cancelled`].
1420    pub fn cancel(&self) {
1421        // Release pairs with the Acquire load in `is_cancelled`
1422        // so a handler that observes `is_cancelled() == true` is
1423        // guaranteed to see every prior write the canceller did.
1424        self.inner.fired.store(true, Ordering::Release);
1425        self.inner.notify.notify_waiters();
1426    }
1427
1428    /// Synchronous probe. `true` once `cancel()` has been called.
1429    #[inline]
1430    pub fn is_cancelled(&self) -> bool {
1431        self.inner.fired.load(Ordering::Acquire)
1432    }
1433
1434    /// Await the cancellation. Returns immediately if already
1435    /// cancelled. Otherwise registers as a waiter and returns when
1436    /// `cancel()` is called.
1437    ///
1438    /// Race-safe: registering the `notified()` future BEFORE the
1439    /// `is_cancelled` check means a `cancel()` racing this method
1440    /// either (a) is observed by the post-register check and we
1441    /// return immediately, OR (b) lands after we register and wakes
1442    /// our future. Either way we don't sleep past a cancellation.
1443    pub async fn cancelled(&self) {
1444        let notified = self.inner.notify.notified();
1445        if self.is_cancelled() {
1446            return;
1447        }
1448        notified.await;
1449    }
1450}
1451
1452/// W3C Trace Context — `traceparent` and `tracestate` headers
1453/// propagated through nRPC for distributed-tracing systems.
1454///
1455/// `traceparent` carries the trace id, parent span id, and flags;
1456/// `tracestate` carries vendor-specific tracing extensions. nRPC
1457/// is **transport-only** for these — it doesn't parse or generate
1458/// IDs, doesn't emit spans, doesn't talk to any tracing backend.
1459/// Application code (typically via `tracing-opentelemetry` or a
1460/// Datadog client) reads these on the server side and continues
1461/// the trace.
1462///
1463/// See <https://www.w3.org/TR/trace-context/> for the wire format
1464/// of each field.
1465#[derive(Debug, Clone, Default, PartialEq, Eq)]
1466pub struct TraceContext {
1467    /// `traceparent` header value (e.g.
1468    /// `"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"`).
1469    /// Required by the W3C spec; nRPC treats it as opaque bytes.
1470    pub traceparent: String,
1471    /// `tracestate` header value — vendor-specific extensions.
1472    /// Optional in W3C; empty string when absent.
1473    pub tracestate: String,
1474}
1475
1476/// Context handed to a `RpcHandler::call`. Carries what the handler
1477/// needs to fulfill the request: caller routing attribution, the
1478/// request payload, and a cancellation token.
1479///
1480/// **This context does not carry an authenticated end-to-end caller
1481/// identity.** See [`RpcContext::caller_origin`]; handlers that need
1482/// to authorize a caller must either run behind the PROTECTED-service
1483/// admission gate (and read [`RpcContext::org_admission`]) or carry
1484/// their own application-level signature.
1485pub struct RpcContext {
1486    /// Caller's `origin_hash`, copied verbatim from the inbound
1487    /// packet header ([`RpcInboundEvent::origin_hash`]).
1488    ///
1489    /// **Routing metadata, not identity authentication — do not
1490    /// authorize on this.** It is a value carried on the wire, so a
1491    /// peer chooses what it says. Comparing it against an identity
1492    /// claimed elsewhere in the request compares two claims from the
1493    /// same untrusted source and proves nothing.
1494    ///
1495    /// The authenticated fields, and what they actually mean:
1496    ///
1497    /// - the AEAD-verified *wire-session peer* is the node that
1498    ///   delivered the packet — the last hop, not necessarily the
1499    ///   originator (it is `RpcInboundEvent::from_node`, the wire-session
1500    ///   peer's `NodeId`);
1501    /// - [`RpcContext::org_admission`] carries a verified four-party
1502    ///   identity, but only for calls admitted through the
1503    ///   PROTECTED-service gate; it is `None` for public calls.
1504    ///
1505    /// For anything stronger on a public service, the handler needs
1506    /// an application-level signature over a transcript that binds
1507    /// the destination and carries its own freshness.
1508    pub caller_origin: u64,
1509    /// Caller-generated correlation id. Same value on the matching
1510    /// CANCEL or RESPONSE.
1511    pub call_id: u64,
1512    /// Decoded request payload.
1513    pub payload: RpcRequestPayload,
1514    /// Cancellation signal. Handlers should `select!` on
1515    /// `cancellation.cancelled()` if their work is async-cancellable;
1516    /// long-running synchronous handlers should periodically check
1517    /// `cancellation.is_cancelled()`.
1518    pub cancellation: RpcCancellationToken,
1519    /// W3C Trace Context propagated from the caller, if the
1520    /// caller set `FLAG_RPC_PROPAGATE_TRACE` and supplied
1521    /// `traceparent` / `tracestate` headers in the request. The
1522    /// server's handler reads this to continue the distributed
1523    /// trace. `None` for calls that didn't propagate trace
1524    /// context.
1525    pub trace_context: Option<TraceContext>,
1526    /// OA-2 org-admission attribution (E1.6). `Some(Admitted)` for a
1527    /// call that passed the PROTECTED-service admission gate — the
1528    /// four-party verified identity (caller, acting org, provider org,
1529    /// exact provider, capability). The raw `net-org-admission` proof
1530    /// header is STRIPPED from `payload.headers` before the handler
1531    /// sees it, so application code receives verified attribution, never
1532    /// raw credential material. `None` for public
1533    /// (`PublicAuthenticated`) calls, which keep existing header
1534    /// behavior.
1535    pub org_admission: Option<crate::adapter::net::behavior::org_admission::Admitted>,
1536}
1537
1538/// Handler-side error that doesn't fit the application's normal
1539/// `Ok(RpcResponsePayload)` channel. The fold maps these onto a
1540/// failure-status `RpcResponsePayload` for the caller.
1541#[derive(Debug, thiserror::Error)]
1542pub enum RpcHandlerError {
1543    /// Application-defined error. The fold encodes this as
1544    /// `RpcStatus::Application(code)` with `message` as the body.
1545    #[error("application error {code:#06x}: {message}")]
1546    Application {
1547        /// Application error code; surfaces as `RpcStatus::Application(code)`
1548        /// to the caller. Use `0x8000..=0xFFFF` to avoid the
1549        /// reserved canonical range.
1550        code: u16,
1551        /// Diagnostic. Becomes the response body (UTF-8 bytes).
1552        message: String,
1553    },
1554    /// Catch-all for handler-internal failures. The fold encodes this
1555    /// as `RpcStatus::Internal` with `message` as the body.
1556    #[error("internal: {0}")]
1557    Internal(String),
1558}
1559
1560/// User-supplied handler. Implementors typically wrap their state
1561/// (or an `Arc<Mutex<State>>`) and route to the appropriate logic
1562/// based on `ctx.payload.service` or per-handler dispatch.
1563///
1564/// Multiple `Mesh::serve_rpc` registrations on different services
1565/// each install their own handler; a single handler typically
1566/// services one service.
1567#[async_trait::async_trait]
1568pub trait RpcHandler: Send + Sync + 'static {
1569    /// Process one request and return the response payload. The
1570    /// fold spawns this in a tokio task; the fold itself doesn't
1571    /// block on it. Handlers should respect `ctx.cancellation` for
1572    /// cooperative early-abort.
1573    async fn call(&self, ctx: RpcContext) -> Result<RpcResponsePayload, RpcHandlerError>;
1574}
1575
1576/// Callback the fold invokes to publish a response back to the
1577/// caller. Wired up by `Mesh::serve_rpc` to publish on
1578/// `<service>.replies.<caller_origin>`. Type-erased so the fold
1579/// doesn't depend on the mesh layer directly.
1580///
1581/// Arguments: `(from_node, caller_origin, call_id, response_payload)`.
1582/// `from_node` (R2-5) is the AEAD-authenticated session peer that
1583/// delivered the REQUEST — the authoritative response destination, so
1584/// two sessions pinned to the same entity/origin submitting the same
1585/// call_id each route their response to their OWN session.
1586pub type RpcResponseEmitter =
1587    Arc<dyn Fn(u64, u64, u64, RpcResponsePayload) + Send + Sync + 'static>;
1588
1589/// Async counterpart of [`RpcResponseEmitter`] used by the
1590/// streaming fold's pump task to serialize per-call publishes.
1591///
1592/// The streaming pump awaits each emit before reading the next
1593/// chunk from the sink — this guarantees that chunks for one
1594/// `call_id` reach the network publish path in the order the
1595/// handler emitted them. (The unary fold has no such requirement
1596/// — it emits exactly one RESPONSE per call — so it sticks with
1597/// the simpler sync `RpcResponseEmitter`.)
1598pub type RpcAsyncResponseEmitter = Arc<
1599    dyn Fn(u64, u64, u64, RpcResponsePayload) -> futures::future::BoxFuture<'static, ()>
1600        + Send
1601        + Sync
1602        + 'static,
1603>;
1604
1605/// `(from_node, caller_origin, call_id)` → cancellation token for an
1606/// in-flight call. `from_node` (AV-1 item 1) is the AEAD-authenticated
1607/// last-hop session peer, so a control frame that copies another peer's
1608/// origin + call_id lands under a distinct, absent key and cannot
1609/// cancel or otherwise mutate the victim's call. Shared across all four
1610/// server folds.
1611type InFlightCalls = Arc<Mutex<HashMap<(u64, u64, u64), RpcCancellationToken>>>;
1612
1613/// Server-side fold. Sees REQUEST events on the configured channel,
1614/// dispatches to the user-supplied handler, emits RESPONSE events
1615/// via the supplied emitter. CANCEL events flip the matching
1616/// in-flight token.
1617///
1618/// State `()` — the user's state lives on whatever the `RpcHandler`
1619/// captures (typically `Arc<Mutex<S>>`). The fold's own state (the
1620/// in-flight map) lives on `&mut self` and is shared with spawned
1621/// handler tasks via `Arc<Mutex<...>>` so the task can self-clean
1622/// on completion.
1623pub struct RpcServerFold {
1624    handler: Arc<dyn RpcHandler>,
1625    emit: RpcResponseEmitter,
1626    /// (from_node, caller_origin, call_id) → cancellation token for
1627    /// the in-flight handler. `from_node` is the AEAD-authenticated
1628    /// last-hop session peer (AV-1 item 1): binding it into the key
1629    /// means a peer that copies another peer's origin + call_id onto
1630    /// a forged CANCEL looks up a distinct, absent key and no-ops
1631    /// rather than cancelling the victim's call. Inserted on REQUEST,
1632    /// removed by either the spawned handler task on completion or by
1633    /// the fold on CANCEL. Wrapped in `Arc<Mutex<...>>` so spawned
1634    /// tasks can remove their own entries without going back through
1635    /// the fold.
1636    in_flight: InFlightCalls,
1637    /// Optional per-service metrics handle. When `Some`, the
1638    /// spawned handler task bumps `handler_invocations_total` /
1639    /// `handler_in_flight` / `handler_panics_total` and records
1640    /// per-task wall-clock durations. `None` → no metrics
1641    /// (test-only path; production `Mesh::serve_rpc` always
1642    /// supplies one).
1643    metrics: Option<Arc<crate::adapter::net::mesh_rpc_metrics::ServiceMetricsAtomic>>,
1644    /// Optional clock override for tests. `None` → real wall-clock
1645    /// `unix_nanos`. `Some(...)` → fixed value, lets tests pin
1646    /// deadline-already-passed behavior without sleeping.
1647    #[cfg(test)]
1648    test_now_ns: Option<u64>,
1649}
1650
1651impl RpcServerFold {
1652    /// Construct a server fold around `handler`. `emit` is the
1653    /// callback that publishes RESPONSE events to the caller's
1654    /// reply channel — `Mesh::serve_rpc` wires this to the
1655    /// publisher for `<service>.replies.<caller_origin>`.
1656    /// Constructed without a metrics handle; production callers
1657    /// chain `.with_metrics(...)` to opt into per-service
1658    /// counters.
1659    pub fn new(handler: Arc<dyn RpcHandler>, emit: RpcResponseEmitter) -> Self {
1660        Self {
1661            handler,
1662            emit,
1663            in_flight: Arc::new(Mutex::new(HashMap::new())),
1664            metrics: None,
1665            #[cfg(test)]
1666            test_now_ns: None,
1667        }
1668    }
1669
1670    /// Attach a per-service metrics handle. Hooks the spawned
1671    /// handler task to bump `handler_invocations_total`, balance
1672    /// `handler_in_flight`, count panics, and record handler
1673    /// duration into the histogram.
1674    pub fn with_metrics(
1675        mut self,
1676        metrics: Arc<crate::adapter::net::mesh_rpc_metrics::ServiceMetricsAtomic>,
1677    ) -> Self {
1678        self.metrics = Some(metrics);
1679        self
1680    }
1681
1682    /// Test-only: pin the clock the fold uses for deadline
1683    /// short-circuit. Lets a unit test exercise the
1684    /// deadline-already-passed branch without waiting for wall
1685    /// time.
1686    #[cfg(test)]
1687    pub fn with_test_now_ns(mut self, now_ns: u64) -> Self {
1688        self.test_now_ns = Some(now_ns);
1689        self
1690    }
1691
1692    /// Test-only: snapshot of the in-flight call set.
1693    #[cfg(test)]
1694    pub fn in_flight_keys(&self) -> Vec<(u64, u64, u64)> {
1695        self.in_flight.lock().keys().copied().collect()
1696    }
1697
1698    fn now_ns(&self) -> u64 {
1699        #[cfg(test)]
1700        if let Some(t) = self.test_now_ns {
1701            return t;
1702        }
1703        std::time::SystemTime::now()
1704            .duration_since(std::time::UNIX_EPOCH)
1705            .map(|d| d.as_nanos() as u64)
1706            .unwrap_or(0)
1707    }
1708
1709    /// `true` if the request's deadline has already elapsed at
1710    /// the server's current wall-clock — accounting for a small
1711    /// tolerance window that absorbs clock skew between caller
1712    /// and server. Without the tolerance, a request from a peer
1713    /// whose clock is a few hundred ms ahead of the server's
1714    /// would be timed out before the handler even saw it. Matches
1715    /// gRPC's default deadline-clock-skew tolerance shape (gRPC
1716    /// uses ~10 s).
1717    fn deadline_already_passed(&self, deadline_ns: u64) -> bool {
1718        if deadline_ns == 0 {
1719            return false;
1720        }
1721        self.now_ns().saturating_sub(DEADLINE_SKEW_TOLERANCE_NS) > deadline_ns
1722    }
1723}
1724
1725/// Tolerance for clock skew between caller and server when the
1726/// server short-circuits a request whose `deadline_ns` looks like
1727/// it has already elapsed. The check is
1728/// `now_ns - SKEW > deadline_ns`, so a request from a peer whose
1729/// clock is up to `SKEW` nanoseconds ahead of ours never hits the
1730/// short-circuit path. 10 s matches gRPC's default and is well
1731/// within the threshold an NTP-disciplined cluster ever drifts to.
1732pub const DEADLINE_SKEW_TOLERANCE_NS: u64 = 10_000_000_000; // 10 seconds
1733
1734impl RpcServerFold {
1735    /// Production-path entry point. The serve bridge calls this with
1736    /// the AEAD-verified session peer's `NodeId` in `ev.from_node`;
1737    /// all per-call state is keyed by `(from_node, claimed_origin,
1738    /// call_id)` so no peer can create, cancel, or otherwise mutate
1739    /// another peer's call by copying its origin + call_id (AV-1
1740    /// item 1).
1741    pub fn apply_inbound(&mut self, ev: &RpcInboundEvent) -> Result<(), RedexError> {
1742        self.apply_frame(ev.from_node, &ev.payload, None)
1743    }
1744
1745    /// OA-2 protected entry point (E1.6). The protected serve bridge
1746    /// calls this AFTER `verify_org_admission` succeeds, handing the
1747    /// four-party [`Admitted`](crate::adapter::net::behavior::org_admission::Admitted)
1748    /// attribution the fold places into `RpcContext::org_admission`; the
1749    /// fold also STRIPS every `net-org-admission` header from the payload
1750    /// so the handler never sees the raw proof. Unary-only — the
1751    /// streaming/duplex folds have no admitted entry point (E1.8).
1752    pub fn apply_inbound_admitted(
1753        &mut self,
1754        ev: &RpcInboundEvent,
1755        admitted: crate::adapter::net::behavior::org_admission::Admitted,
1756    ) -> Result<(), RedexError> {
1757        self.apply_frame(ev.from_node, &ev.payload, Some(admitted))
1758    }
1759
1760    /// Core frame application shared by [`Self::apply_inbound`] (real
1761    /// authenticated `from_node`) and the [`RedexFold`] loopback shim
1762    /// (`from_node = 0`, test / loopback paths with no session peer).
1763    /// `org_admission` is `Some` only on the initial REQUEST of an
1764    /// admitted protected call; it is placed into the handler's
1765    /// `RpcContext` and its presence triggers the proof-header strip.
1766    fn apply_frame(
1767        &mut self,
1768        from_node: u64,
1769        frame: &Bytes,
1770        org_admission: Option<crate::adapter::net::behavior::org_admission::Admitted>,
1771    ) -> Result<(), RedexError> {
1772        // Decode the meta header. A garbled meta means the event
1773        // doesn't even claim to be an RPC packet — log and skip
1774        // rather than killing the fold. Returning `Err(Decode)`
1775        // here would stop the entire cortex adapter for one
1776        // malformed event, which is wrong for an RPC server that
1777        // needs to keep serving.
1778        let Some(meta) = (if frame.len() >= EVENT_META_SIZE {
1779            EventMeta::from_bytes(&frame[..EVENT_META_SIZE])
1780        } else {
1781            None
1782        }) else {
1783            tracing::warn!(
1784                payload_len = frame.len(),
1785                "rpc server fold: event payload too short for EventMeta; skipping",
1786            );
1787            return Ok(());
1788        };
1789        let key = (from_node, meta.origin_hash, meta.seq_or_ts);
1790        match meta.dispatch {
1791            DISPATCH_RPC_REQUEST => {
1792                let mut payload =
1793                    match RpcRequestPayload::decode(frame.slice(RPC_FRAME_BODY_OFFSET..)) {
1794                        Ok(p) => p,
1795                        Err(e) => {
1796                            // Malformed request payload. Surface as
1797                            // `UnknownVersion` to the caller — they sent
1798                            // bytes we couldn't parse, which usually
1799                            // means a wire-format mismatch (the most
1800                            // common cause). Log so operators can
1801                            // diagnose.
1802                            tracing::warn!(
1803                                error = %e,
1804                                caller_origin = format!("{:#x}", meta.origin_hash),
1805                                call_id = meta.seq_or_ts,
1806                                "rpc server fold: malformed request payload",
1807                            );
1808                            let resp = RpcResponsePayload {
1809                                status: RpcStatus::UnknownVersion,
1810                                headers: vec![],
1811                                body: Bytes::from(format!("malformed request: {e}")),
1812                            };
1813                            (self.emit)(from_node, meta.origin_hash, meta.seq_or_ts, resp);
1814                            return Ok(());
1815                        }
1816                    };
1817                // E1.6: an admitted protected call STRIPS the raw
1818                // `net-org-admission` proof header(s) before the handler
1819                // sees the payload — application code receives verified
1820                // attribution via `RpcContext::org_admission`, never the
1821                // credential material. Public calls (`org_admission` None)
1822                // keep their headers untouched.
1823                if org_admission.is_some() {
1824                    payload.headers.retain(|(name, _)| {
1825                        name != crate::adapter::net::behavior::org_call::ORG_ADMISSION_HEADER
1826                    });
1827                }
1828                // Fast deadline-already-passed short-circuit.
1829                // Server-side `Timeout` without invoking the
1830                // handler. Includes a clock-skew tolerance window
1831                // so a peer with a slightly-fast clock isn't
1832                // prematurely timed out — see
1833                // `deadline_already_passed`.
1834                if self.deadline_already_passed(payload.deadline_ns) {
1835                    let resp = RpcResponsePayload {
1836                        status: RpcStatus::Timeout,
1837                        headers: vec![],
1838                        body: Bytes::from_static(b"deadline already passed when request landed"),
1839                    };
1840                    (self.emit)(from_node, meta.origin_hash, meta.seq_or_ts, resp);
1841                    return Ok(());
1842                }
1843                // Refuse a duplicate REQUEST with the same
1844                // `(origin_hash, call_id)` — see streaming fold for
1845                // the full rationale. For the unary fold this would
1846                // spawn a second handler under the same key, and
1847                // whichever handler completes first removes the
1848                // in-flight entry — leaving the second handler's
1849                // CANCEL handling broken (CANCEL events look up
1850                // the now-missing key and no-op). Cleaner to refuse.
1851                {
1852                    let in_flight = self.in_flight.lock();
1853                    if in_flight.contains_key(&key) {
1854                        drop(in_flight);
1855                        tracing::warn!(
1856                            caller_origin = format!("{:#x}", meta.origin_hash),
1857                            call_id = meta.seq_or_ts,
1858                            "rpc server fold: duplicate REQUEST for in-flight call_id; refusing",
1859                        );
1860                        let resp = RpcResponsePayload {
1861                            status: RpcStatus::Internal,
1862                            headers: vec![],
1863                            body: Bytes::from_static(
1864                                b"duplicate REQUEST for already-in-flight call_id",
1865                            ),
1866                        };
1867                        (self.emit)(from_node, meta.origin_hash, meta.seq_or_ts, resp);
1868                        return Ok(());
1869                    }
1870                }
1871                let cancellation = RpcCancellationToken::new();
1872                self.in_flight.lock().insert(key, cancellation.clone());
1873                let handler = self.handler.clone();
1874                let emit = self.emit.clone();
1875                let in_flight = self.in_flight.clone();
1876                let caller_origin = meta.origin_hash;
1877                let call_id = meta.seq_or_ts;
1878                // Decode the W3C Trace Context if the caller
1879                // signaled it via `FLAG_RPC_PROPAGATE_TRACE` and
1880                // included the `traceparent` / `tracestate`
1881                // headers. nRPC is transport-only — application
1882                // code reads `ctx.trace_context` to continue the
1883                // trace via whatever backend it has wired up.
1884                let trace_context = if payload.flags & FLAG_RPC_PROPAGATE_TRACE != 0 {
1885                    extract_trace_context(&payload.headers)
1886                } else {
1887                    None
1888                };
1889                let metrics = self.metrics.clone();
1890                // Keep a probe handle so the spawned task can detect
1891                // a CANCEL that fired during handler execution and
1892                // override its response with `RpcStatus::Cancelled`.
1893                let cancel_probe = cancellation.clone();
1894                tokio::spawn(async move {
1895                    // Server-side metrics: count this invocation;
1896                    // bump in_flight; time the handler; tally
1897                    // panics. Only fires when a metrics handle was
1898                    // attached via `with_metrics(...)` — test-only
1899                    // folds construct without one.
1900                    if let Some(m) = metrics.as_ref() {
1901                        m.handler_invocations_total
1902                            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1903                        m.handler_in_flight
1904                            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1905                    }
1906                    let handler_started = std::time::Instant::now();
1907                    let ctx = RpcContext {
1908                        caller_origin,
1909                        call_id,
1910                        payload,
1911                        cancellation,
1912                        trace_context,
1913                        org_admission,
1914                    };
1915                    // Catch panics so a misbehaving handler can't
1916                    // take down the runtime. `AssertUnwindSafe` is
1917                    // load-bearing because `RpcHandler::call`
1918                    // returns a future that may borrow non-
1919                    // `UnwindSafe` types from the handler; we
1920                    // accept the assertion because the handler's
1921                    // state is untouched on panic (we just don't
1922                    // observe its in-progress mutations).
1923                    let outcome = futures::FutureExt::catch_unwind(std::panic::AssertUnwindSafe(
1924                        handler.call(ctx),
1925                    ))
1926                    .await;
1927                    if let Some(m) = metrics.as_ref() {
1928                        m.handler_in_flight
1929                            .fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
1930                        m.record_handler_duration(handler_started.elapsed());
1931                        if outcome.is_err() {
1932                            m.handler_panics_total
1933                                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1934                        }
1935                    }
1936                    // CANCEL-wins ordering: if the cancellation
1937                    // token fired at any point during handler
1938                    // execution, override the handler's outcome
1939                    // with `RpcStatus::Cancelled` so the caller
1940                    // (or hedge primary, retry layer, etc.) sees
1941                    // the documented `Cancelled` status code rather
1942                    // than whatever the handler happened to return
1943                    // before / despite cancellation. A cooperative
1944                    // handler that observes the token and bails
1945                    // early gets the same Cancelled framing as a
1946                    // handler that ignored cancellation and ran to
1947                    // completion — the caller's view is uniform.
1948                    let resp = if cancel_probe.is_cancelled() {
1949                        RpcResponsePayload {
1950                            status: RpcStatus::Cancelled,
1951                            headers: vec![],
1952                            body: Bytes::from_static(
1953                                b"server observed CANCEL during handler execution",
1954                            ),
1955                        }
1956                    } else {
1957                        match outcome {
1958                            Ok(Ok(payload)) => payload,
1959                            Ok(Err(RpcHandlerError::Application { code, message })) => {
1960                                RpcResponsePayload {
1961                                    status: RpcStatus::Application(code),
1962                                    headers: vec![],
1963                                    body: Bytes::from(message),
1964                                }
1965                            }
1966                            Ok(Err(RpcHandlerError::Internal(message))) => RpcResponsePayload {
1967                                status: RpcStatus::Internal,
1968                                headers: vec![],
1969                                body: Bytes::from(message),
1970                            },
1971                            Err(panic) => {
1972                                let panic_msg = panic
1973                                    .downcast_ref::<&'static str>()
1974                                    .map(|s| s.to_string())
1975                                    .or_else(|| panic.downcast_ref::<String>().cloned())
1976                                    .unwrap_or_else(|| "<non-string panic>".into());
1977                                tracing::error!(
1978                                    caller_origin = format!("{:#x}", caller_origin),
1979                                    call_id,
1980                                    panic = %panic_msg,
1981                                    "rpc server handler panicked",
1982                                );
1983                                RpcResponsePayload {
1984                                    status: RpcStatus::Internal,
1985                                    headers: vec![],
1986                                    body: Bytes::from(format!("handler panicked: {panic_msg}")),
1987                                }
1988                            }
1989                        }
1990                    };
1991                    in_flight.lock().remove(&key);
1992                    emit(from_node, caller_origin, call_id, resp);
1993                });
1994            }
1995            DISPATCH_RPC_CANCEL => {
1996                if let Some(token) = self.in_flight.lock().remove(&key) {
1997                    token.cancel();
1998                }
1999                // Idempotent — CANCEL for an unknown call_id (e.g.
2000                // a CANCEL that races the handler's completion) is
2001                // a no-op rather than an error. The spawned handler
2002                // task observes `cancel_probe.is_cancelled()` after
2003                // its future resolves and overrides the response
2004                // with `RpcStatus::Cancelled` so the caller sees a
2005                // documented status code rather than the handler's
2006                // accidental Ok / Internal payload.
2007            }
2008            // RESPONSE / DEADLINE_EXCEEDED are server-emitted; if
2009            // the server's own fold sees them (e.g. from a replay)
2010            // there's nothing to do.
2011            _ => {}
2012        }
2013        Ok(())
2014    }
2015}
2016
2017impl RedexFold<()> for RpcServerFold {
2018    /// Loopback / test shim: no session peer to resolve, so this
2019    /// drives `apply_frame` with `from_node = 0` (AV-1 item 1). The
2020    /// production wire path uses [`RpcServerFold::apply_inbound`].
2021    fn apply(&mut self, ev: &RedexEvent, _state: &mut ()) -> Result<(), RedexError> {
2022        self.apply_frame(0, &ev.payload, None)
2023    }
2024}
2025
2026// ============================================================================
2027// Streaming server-side: handler trait + sink + fold.
2028// ============================================================================
2029
2030/// Sink the handler writes to in order to emit streaming-response
2031/// chunks. Each `send` produces one non-terminal `RESPONSE` event
2032/// to the caller. The terminal frame is emitted automatically when
2033/// the sink is dropped — the handler returning `Ok(())` drops the
2034/// sink, which closes the stream cleanly. Returning
2035/// `Err(RpcHandlerError)` drops the sink and emits the error as a
2036/// terminal non-`Ok` RESPONSE.
2037///
2038/// `send` is best-effort and infallible: the underlying mpsc is
2039/// **bounded** at [`STREAMING_PUMP_CAPACITY`] chunks. If the pump
2040/// can't keep up (publish path is congested, caller hasn't granted
2041/// flow-control credits), `send` discards on overflow — same
2042/// observable shape as a closed receiver (caller cancelled mid-
2043/// stream). Counts the drop in `streaming_chunks_dropped_total` so
2044/// operators can see backpressure occurring. Cooperative
2045/// cancellation via `ctx.cancellation` is the right way for the
2046/// handler to notice the consumer is gone; opt-in flow control via
2047/// `CallOptions::stream_window_initial` is the right way to
2048/// throttle a fast handler against a slow consumer.
2049pub struct RpcResponseSink {
2050    inner: tokio::sync::mpsc::Sender<bytes::Bytes>,
2051    /// Optional metrics handle so a dropped-on-full chunk bumps the
2052    /// `streaming_chunks_dropped_total` counter. `None` for unit-
2053    /// test folds that construct without metrics.
2054    metrics: Option<Arc<crate::adapter::net::mesh_rpc_metrics::ServiceMetricsAtomic>>,
2055}
2056
2057impl RpcResponseSink {
2058    /// Emit one non-terminal chunk. Cheap (`try_send` on a
2059    /// [`STREAMING_PUMP_CAPACITY`]-bounded mpsc); never blocks. On
2060    /// overflow OR receiver-closed, the chunk is dropped and (when
2061    /// metrics are wired) `streaming_chunks_dropped_total` is
2062    /// incremented for the service.
2063    pub fn send(&self, body: impl Into<bytes::Bytes>) {
2064        if self.inner.try_send(body.into()).is_err() {
2065            if let Some(m) = self.metrics.as_ref() {
2066                m.streaming_chunks_dropped_total
2067                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2068            }
2069        }
2070    }
2071
2072    /// Emit one non-terminal chunk, **waiting** for pump-queue room
2073    /// instead of dropping on overflow. The backpressure-aware
2074    /// sibling of [`Self::send`]: when the per-call pump queue
2075    /// ([`STREAMING_PUMP_CAPACITY`]) is full — e.g. a flow-controlled
2076    /// caller has stopped granting credit — this parks until the
2077    /// pump drains a slot rather than silently discarding the chunk.
2078    ///
2079    /// Returns [`RpcSinkClosed`] when the pump receiver is gone (the
2080    /// call is torn down); the chunk was not sent and the handler
2081    /// should stop producing.
2082    ///
2083    /// Use this from handlers whose stream carries deltas that must
2084    /// not be lost silently (e.g. the `tool.watch` subscription,
2085    /// whose overflow contract requires an explicit resync frame
2086    /// instead of a drop); keep [`Self::send`] for streams where
2087    /// dropping under backpressure is acceptable.
2088    pub async fn send_wait(&self, body: impl Into<bytes::Bytes>) -> Result<(), RpcSinkClosed> {
2089        self.inner
2090            .send(body.into())
2091            .await
2092            .map_err(|_| RpcSinkClosed)
2093    }
2094}
2095
2096/// Error returned by [`RpcResponseSink::send_wait`] when the
2097/// per-call pump receiver has shut down (the streaming call is
2098/// being torn down server-side). The chunk was not sent.
2099#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2100pub struct RpcSinkClosed;
2101
2102impl std::fmt::Display for RpcSinkClosed {
2103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2104        write!(
2105            f,
2106            "rpc streaming sink: pump receiver closed; chunk not sent"
2107        )
2108    }
2109}
2110
2111impl std::error::Error for RpcSinkClosed {}
2112
2113/// Bounded capacity for the streaming pump's internal mpsc. A
2114/// runaway handler that produces chunks faster than the publish
2115/// path can drain them stops blocking the runtime past this many
2116/// queued chunks — additional chunks are dropped (and counted via
2117/// `streaming_chunks_dropped_total`). 1024 is generous for typical
2118/// streaming patterns; opt-in flow control via
2119/// `CallOptions::stream_window_initial` is the right primitive for
2120/// strict throttling.
2121pub const STREAMING_PUMP_CAPACITY: usize = 1024;
2122
2123/// Bounded capacity for the client-streaming server fold's
2124/// per-call request mpsc. Mirror of [`STREAMING_PUMP_CAPACITY`]
2125/// for the upload direction. A runaway caller that emits
2126/// REQUEST_CHUNKs faster than the handler can drain stops
2127/// queueing past this many chunks — additional chunks are
2128/// dropped (and counted via `streaming_request_chunks_dropped_total`
2129/// when metrics are wired). Opt-in flow control via the
2130/// `nrpc-request-window-initial` header is the right primitive
2131/// for strict throttling on the upload side.
2132///
2133/// Bidi streaming plan (Phase B).
2134pub const STREAMING_REQUEST_PUMP_CAPACITY: usize = 1024;
2135
2136// ============================================================================
2137// Phase B — server-side primitives for client-streaming.
2138// ============================================================================
2139
2140/// Context handed to an [`RpcClientStreamingHandler::call`]. Same
2141/// shape as [`RpcContext`] minus the eager `payload` (the request
2142/// stream delivers chunk bodies on the fly) and plus the
2143/// per-call `deadline_ns` (which would otherwise have ridden on
2144/// the eager payload).
2145///
2146/// Bidi streaming plan (Phase B).
2147pub struct RpcStreamingContext {
2148    /// Caller's `origin_hash`, from the inbound packet header. Same
2149    /// source, and the same caveat, as [`RpcContext::caller_origin`]:
2150    /// **routing metadata, not identity authentication — do not
2151    /// authorize on this.**
2152    pub caller_origin: u64,
2153    /// Caller-generated correlation id. Matches the initial
2154    /// REQUEST's `call_id` and every subsequent REQUEST_CHUNK /
2155    /// CANCEL / REQUEST_GRANT for this call.
2156    pub call_id: u64,
2157    /// Absolute deadline (unix nanos) from the initial REQUEST.
2158    /// `0` means no deadline; the fold does NOT auto-cancel on
2159    /// deadline (handlers self-supervise via tokio timers, same
2160    /// contract as the unary fold).
2161    pub deadline_ns: u64,
2162    /// Per-chunk metadata headers from the initial REQUEST.
2163    /// Per-REQUEST_CHUNK headers are NOT surfaced at the substrate
2164    /// layer — the typed SDK veneer (Phase E) is where header
2165    /// inspection across chunks lives (if it lands at all; the
2166    /// plan defers per-chunk-headers as opt-in raw-path access).
2167    pub headers: Vec<RpcHeader>,
2168    /// Cancellation signal. Flipped by the fold when a
2169    /// `DISPATCH_RPC_CANCEL` arrives for this call's `call_id`.
2170    /// Long-running handlers should `select!` on
2171    /// `cancellation.cancelled()`; the request stream also
2172    /// terminates on cancellation, but the token is the
2173    /// authoritative signal (the stream's terminator is shared
2174    /// with REQUEST_END).
2175    pub cancellation: RpcCancellationToken,
2176    /// W3C Trace Context propagated from the caller's initial
2177    /// REQUEST. Same semantics as [`RpcContext::trace_context`].
2178    pub trace_context: Option<TraceContext>,
2179}
2180
2181/// Callback the fold invokes to publish a [`DISPATCH_RPC_REQUEST_GRANT`]
2182/// event back to the caller. Wired up by the `Mesh` glue (Phase C)
2183/// to publish on the caller's reply channel. Type-erased so the
2184/// fold doesn't depend on the mesh layer directly.
2185///
2186/// Arguments: `(caller_origin, call_id, credits)`. Synchronous —
2187/// the publish itself is non-blocking (the underlying transport
2188/// has its own internal queueing); the fold fires-and-forgets
2189/// every grant, so dropped grants are at worst a latency wobble,
2190/// not a correctness issue (the caller's send sink will retry
2191/// when its credit budget refills via the next grant or via the
2192/// initial window).
2193///
2194/// Bidi streaming plan (Phase B).
2195/// Arguments: `(from_node, caller_origin, call_id, credits)`. R3-1 —
2196/// the AEAD-authenticated `from_node` is carried so an upload grant is
2197/// session-scoped: coalesced and published per `(from_node, origin,
2198/// call_id)`, and (for a trusted direct route) delivered ONLY to the
2199/// session that issued the call, never roster-fanned to a same-origin
2200/// sibling whose request semaphore it would otherwise wrongly refill.
2201pub type RpcRequestGrantEmitter = Arc<dyn Fn(u64, u64, u64, u32) + Send + Sync + 'static>;
2202
2203/// Server-side stream of inbound request chunk bodies for one
2204/// client-streaming (or duplex) call. Yields one `Bytes` per
2205/// `DISPATCH_RPC_REQUEST` / `DISPATCH_RPC_REQUEST_CHUNK` frame
2206/// (including empty bodies — the semantics of "empty bytes" are
2207/// the application's concern, not the substrate's). Closes on
2208/// `FLAG_RPC_REQUEST_END` or on CANCEL.
2209///
2210/// **Stream item ordering convention**: the first item this
2211/// stream yields corresponds to the initial REQUEST body; every
2212/// subsequent item corresponds to a REQUEST_CHUNK body, in the
2213/// order the chunks were received from the wire. The substrate
2214/// does not tag items with their frame kind — the SDK veneer
2215/// (Phase E) is responsible for the Init / Data classification
2216/// via its `Chunk<T>` enum.
2217///
2218/// **Auto-grant behavior**: when the caller opted into
2219/// request-direction flow control via
2220/// [`HEADER_NRPC_REQUEST_WINDOW_INITIAL`], every successful
2221/// `poll_next()` fires one credit back to the caller via the
2222/// captured `grant_emitter`. This keeps the in-flight window
2223/// at the caller's initial value as the handler drains the
2224/// stream. When the caller did NOT opt in (no header), the
2225/// `grant_emitter` is `None` and the auto-grant path is a no-op
2226/// (caller is on the unbounded-credit fast path).
2227///
2228/// Bidi streaming plan (Phase B).
2229pub struct RequestStream {
2230    inner: tokio::sync::mpsc::Receiver<bytes::Bytes>,
2231    grant_emitter: Option<RpcRequestGrantEmitter>,
2232    /// AEAD-authenticated session that issued this call (R3-1) — the
2233    /// grant identity, so a grant refills only THIS call's semaphore.
2234    from_node: u64,
2235    caller_origin: u64,
2236    call_id: u64,
2237}
2238
2239impl RequestStream {
2240    /// Visible to the fold (and only the fold) for constructing
2241    /// a stream tied to a specific receiver + caller. The
2242    /// `grant_emitter` is `None` when the caller didn't opt into
2243    /// flow control; `Some(...)` when they did. `from_node` is the
2244    /// authenticated session the fold bound the call to (R3-1).
2245    pub(crate) fn new(
2246        inner: tokio::sync::mpsc::Receiver<bytes::Bytes>,
2247        grant_emitter: Option<RpcRequestGrantEmitter>,
2248        from_node: u64,
2249        caller_origin: u64,
2250        call_id: u64,
2251    ) -> Self {
2252        Self {
2253            inner,
2254            grant_emitter,
2255            from_node,
2256            caller_origin,
2257            call_id,
2258        }
2259    }
2260}
2261
2262impl futures::Stream for RequestStream {
2263    type Item = bytes::Bytes;
2264
2265    fn poll_next(
2266        mut self: std::pin::Pin<&mut Self>,
2267        cx: &mut std::task::Context<'_>,
2268    ) -> std::task::Poll<Option<Self::Item>> {
2269        match self.inner.poll_recv(cx) {
2270            std::task::Poll::Ready(Some(bytes)) => {
2271                // Auto-grant fires on every successful pull when
2272                // flow control was opted into. Cheap and
2273                // fire-and-forget; missed grants are recovered
2274                // by subsequent pulls.
2275                if let Some(emit) = self.grant_emitter.as_ref() {
2276                    emit(self.from_node, self.caller_origin, self.call_id, 1);
2277                }
2278                std::task::Poll::Ready(Some(bytes))
2279            }
2280            other => other,
2281        }
2282    }
2283}
2284
2285/// User-supplied handler for a client-streaming RPC. Receives an
2286/// [`RpcStreamingContext`] (caller identity, deadline, cancellation,
2287/// trace context, initial REQUEST headers) plus a [`RequestStream`]
2288/// of chunk bodies. Returns one terminal [`RpcResponsePayload`] —
2289/// the fold publishes it as the call's single RESPONSE frame.
2290///
2291/// **Cancellation contract.** Long-running handlers should
2292/// `select!` on `ctx.cancellation.cancelled()` so a caller-side
2293/// drop / deadline correctly stops the handler. The request
2294/// stream also terminates on cancellation (yields `None`), but
2295/// the token is the authoritative signal — the stream's `None`
2296/// is shared with the clean REQUEST_END path, so handlers can't
2297/// distinguish "caller finished cleanly" from "caller cancelled"
2298/// without consulting the token.
2299///
2300/// **Auto-grant.** When the caller opted into request-direction
2301/// flow control via [`HEADER_NRPC_REQUEST_WINDOW_INITIAL`], every
2302/// `stream.next().await` that yields `Some` fires one
2303/// REQUEST_GRANT back to the caller, maintaining the in-flight
2304/// window at the caller's initial value. Handlers don't need to
2305/// think about credit management for the common case.
2306///
2307/// Bidi streaming plan (Phase B).
2308#[async_trait::async_trait]
2309pub trait RpcClientStreamingHandler: Send + Sync + 'static {
2310    /// Process a client-streaming call. Drain the request stream,
2311    /// produce one terminal response payload (or an
2312    /// [`RpcHandlerError`] for failure mapping).
2313    async fn call(
2314        &self,
2315        ctx: RpcStreamingContext,
2316        requests: RequestStream,
2317    ) -> Result<RpcResponsePayload, RpcHandlerError>;
2318}
2319
2320/// User-supplied handler for a duplex RPC — many requests in,
2321/// many responses out, interleaved. Receives an [`RpcStreamingContext`]
2322/// plus a [`RequestStream`] of chunk bodies plus an
2323/// [`RpcResponseSink`] for emitting response chunks. The handler's
2324/// return value is its terminal status, NOT a final payload:
2325/// `Ok(())` closes the response stream cleanly with a terminal
2326/// `Ok` frame, `Err(RpcHandlerError)` closes with the matching
2327/// error status.
2328///
2329/// **Composition.** A duplex handler is a hybrid of an
2330/// [`RpcClientStreamingHandler`] (drains request chunks) and an
2331/// [`RpcStreamingHandler`] (emits response chunks). The two
2332/// directions are independent — a handler can finish emitting
2333/// responses before reading all requests, or vice versa. The
2334/// server fold serializes RESPONSE chunk publishes per call_id
2335/// so wire order matches handler order.
2336///
2337/// **Cancellation contract.** Identical to
2338/// [`RpcClientStreamingHandler`]: long-running work should
2339/// `select!` on `ctx.cancellation.cancelled()`.
2340///
2341/// **Auto-grant.** Identical to [`RpcClientStreamingHandler`]:
2342/// every successful `requests.next().await` emits one
2343/// REQUEST_GRANT back to the caller (when the caller opted in).
2344///
2345/// Bidi streaming plan (Phase D).
2346#[async_trait::async_trait]
2347pub trait RpcDuplexHandler: Send + Sync + 'static {
2348    /// Process one duplex call. Drain inbound chunks via
2349    /// `requests.next().await`; emit outbound chunks via
2350    /// `responses.send(...)`. Return `Ok(())` for clean close,
2351    /// `Err(RpcHandlerError)` for failure mapping.
2352    async fn call(
2353        &self,
2354        ctx: RpcStreamingContext,
2355        requests: RequestStream,
2356        responses: RpcResponseSink,
2357    ) -> Result<(), RpcHandlerError>;
2358}
2359
2360/// User-supplied streaming handler. Receives the same `RpcContext`
2361/// as a unary handler plus a `RpcResponseSink` for emitting chunks.
2362/// Returning `Ok(())` closes the stream cleanly with a terminal
2363/// `Ok` RESPONSE; `Err(RpcHandlerError)` closes the stream with a
2364/// terminal non-`Ok` RESPONSE carrying the diagnostic.
2365///
2366/// **Cancellation contract.** Long-running streams should
2367/// `select!` on `ctx.cancellation.cancelled()` so a caller-side
2368/// drop / deadline correctly stops the handler. Continuing to
2369/// `send` after cancellation is harmless (sink discards) but
2370/// wastes work.
2371#[async_trait::async_trait]
2372pub trait RpcStreamingHandler: Send + Sync + 'static {
2373    /// Process one streaming request. Emit chunks via `sink.send(...)`.
2374    /// Drop the sink (or return) to close the stream.
2375    async fn call(&self, ctx: RpcContext, sink: RpcResponseSink) -> Result<(), RpcHandlerError>;
2376}
2377
2378/// Per-call flow-control map type. Keyed on
2379/// `(caller_origin_hash, call_id)`; value is a tokio
2380/// `Semaphore` shared between the pump task (which awaits
2381/// permits) and the fold's `apply()` method handling
2382/// STREAM_GRANT events (which add permits).
2383// Keyed on `(from_node, caller_origin, call_id)` (AV-1 item 1): the
2384// authenticated last-hop session peer is part of the key so a peer
2385// cannot refill another peer's flow-control window by copying its
2386// origin + call_id onto a forged STREAM_GRANT.
2387type FlowControlMap = Arc<Mutex<HashMap<(u64, u64, u64), Arc<tokio::sync::Semaphore>>>>;
2388
2389/// Server-side fold for streaming RPC. Parallel to `RpcServerFold`
2390/// but multi-fire emit: each handler invocation may produce many
2391/// `RESPONSE` events for the same `call_id`, marked
2392/// non-terminal/terminal via the `nrpc-streaming` header.
2393///
2394/// State `()` — like the unary fold, the handler owns user state
2395/// via captured `Arc<Mutex<S>>`. The fold's own state (in-flight
2396/// cancellation tokens) lives on `&mut self`.
2397pub struct RpcServerStreamingFold {
2398    handler: Arc<dyn RpcStreamingHandler>,
2399    emit: RpcAsyncResponseEmitter,
2400    /// (from_node, caller_origin, call_id) → cancellation token —
2401    /// authenticated-peer-scoped so a forged CANCEL can't cancel
2402    /// another peer's stream (AV-1 item 1).
2403    in_flight: InFlightCalls,
2404    /// Per-call flow-control semaphore (when the caller opted in).
2405    /// `Some(sem)` means "pump must `acquire().await` one permit
2406    /// per chunk before emitting; STREAM_GRANT events
2407    /// `add_permits(n)`". Absence of an entry for a `(origin,
2408    /// call_id)` key means unbounded credit (no flow control —
2409    /// pump emits as fast as the publish path can take chunks).
2410    flow_control: FlowControlMap,
2411    /// Optional per-service metrics handle. Same shape as
2412    /// `RpcServerFold::metrics`; the streaming fold ALSO bumps
2413    /// `streaming_chunks_emitted_total` from the pump task on
2414    /// every chunk.
2415    metrics: Option<Arc<crate::adapter::net::mesh_rpc_metrics::ServiceMetricsAtomic>>,
2416}
2417
2418impl RpcServerStreamingFold {
2419    /// Construct a streaming server fold. `emit` publishes
2420    /// individual chunks (and the terminal frame) on the caller's
2421    /// reply channel.
2422    ///
2423    /// Uses the **async** emitter variant so the pump task can
2424    /// serialize per-call publishes — without that ordering
2425    /// guarantee, two chunks emitted in succession can race into
2426    /// the publish path and arrive at the caller out of order
2427    /// (or be eclipsed by the terminal frame and lost entirely).
2428    pub fn new(handler: Arc<dyn RpcStreamingHandler>, emit: RpcAsyncResponseEmitter) -> Self {
2429        Self {
2430            handler,
2431            emit,
2432            in_flight: Arc::new(Mutex::new(HashMap::new())),
2433            flow_control: Arc::new(Mutex::new(HashMap::new())),
2434            metrics: None,
2435        }
2436    }
2437
2438    /// Attach a per-service metrics handle. Hooks the spawned
2439    /// handler task to bump `handler_invocations_total` /
2440    /// `handler_in_flight` / `handler_panics_total` /
2441    /// `handler_duration_*`, and the pump task to bump
2442    /// `streaming_chunks_emitted_total` per emitted chunk.
2443    pub fn with_metrics(
2444        mut self,
2445        metrics: Arc<crate::adapter::net::mesh_rpc_metrics::ServiceMetricsAtomic>,
2446    ) -> Self {
2447        self.metrics = Some(metrics);
2448        self
2449    }
2450
2451    /// Test-only: snapshot of the in-flight call set.
2452    #[cfg(test)]
2453    pub fn in_flight_keys(&self) -> Vec<(u64, u64, u64)> {
2454        self.in_flight.lock().keys().copied().collect()
2455    }
2456
2457    /// Test-only: available flow-control permits for a call key
2458    /// `(from_node, origin, call_id)`, or `None` if no per-call
2459    /// semaphore is installed. Lets the AV-1 STREAM_GRANT-hijack
2460    /// witness prove a forged grant from a foreign session does not
2461    /// refill the victim's window.
2462    #[cfg(test)]
2463    pub fn flow_control_permits(&self, key: (u64, u64, u64)) -> Option<usize> {
2464        self.flow_control
2465            .lock()
2466            .get(&key)
2467            .map(|s| s.available_permits())
2468    }
2469}
2470
2471impl RpcServerStreamingFold {
2472    /// Production-path entry point. Keys per-call state (in-flight
2473    /// token + flow-control semaphore) by `(from_node,
2474    /// claimed_origin, call_id)` so a forged CANCEL / STREAM_GRANT
2475    /// from another peer misses the map and no-ops (AV-1 item 1).
2476    pub fn apply_inbound(&mut self, ev: &RpcInboundEvent) -> Result<(), RedexError> {
2477        self.apply_frame(ev.from_node, &ev.payload)
2478    }
2479
2480    /// Core frame application shared by [`Self::apply_inbound`] (real
2481    /// `from_node`) and the [`RedexFold`] loopback shim (`0`).
2482    fn apply_frame(&mut self, from_node: u64, frame: &Bytes) -> Result<(), RedexError> {
2483        let Some(meta) = (if frame.len() >= EVENT_META_SIZE {
2484            EventMeta::from_bytes(&frame[..EVENT_META_SIZE])
2485        } else {
2486            None
2487        }) else {
2488            tracing::warn!(
2489                payload_len = frame.len(),
2490                "rpc streaming server fold: event payload too short for EventMeta",
2491            );
2492            return Ok(());
2493        };
2494        let key = (from_node, meta.origin_hash, meta.seq_or_ts);
2495        match meta.dispatch {
2496            DISPATCH_RPC_REQUEST => {
2497                let payload = match RpcRequestPayload::decode(frame.slice(RPC_FRAME_BODY_OFFSET..))
2498                {
2499                    Ok(p) => p,
2500                    Err(e) => {
2501                        tracing::warn!(
2502                            error = %e,
2503                            caller_origin = format!("{:#x}", meta.origin_hash),
2504                            call_id = meta.seq_or_ts,
2505                            "rpc streaming server fold: malformed request payload",
2506                        );
2507                        // Surface as a terminal error chunk. Spawn
2508                        // because the apply method is sync and the
2509                        // emit is async; this is a one-shot publish
2510                        // so ordering doesn't matter here.
2511                        let resp = RpcResponsePayload {
2512                            status: RpcStatus::UnknownVersion,
2513                            headers: vec![(
2514                                HEADER_NRPC_STREAMING.to_string(),
2515                                HEADER_NRPC_STREAMING_END.to_vec(),
2516                            )],
2517                            body: Bytes::from(format!("malformed request: {e}")),
2518                        };
2519                        let emit = self.emit.clone();
2520                        let caller_origin = meta.origin_hash;
2521                        let call_id = meta.seq_or_ts;
2522                        tokio::spawn(async move {
2523                            emit(from_node, caller_origin, call_id, resp).await;
2524                        });
2525                        return Ok(());
2526                    }
2527                };
2528                // Refuse a duplicate REQUEST with the same
2529                // `(origin_hash, call_id)`. Without this, a retry
2530                // that arrives while the first attempt's pump is
2531                // still draining will overwrite the prior
2532                // semaphore Arc in `flow_control`, leaving the
2533                // first pump awaiting an orphaned semaphore (the
2534                // terminal cleanup keys on `key` and removes the
2535                // *new* entry, so the orphan never gets dropped
2536                // and the first handler hangs forever).
2537                //
2538                // Idempotent for the caller: we emit a terminal
2539                // `Internal` chunk so the duplicate sender sees a
2540                // clean refusal rather than waiting on a stream
2541                // that will never produce output.
2542                {
2543                    let in_flight = self.in_flight.lock();
2544                    if in_flight.contains_key(&key) {
2545                        drop(in_flight);
2546                        tracing::warn!(
2547                            caller_origin = format!("{:#x}", meta.origin_hash),
2548                            call_id = meta.seq_or_ts,
2549                            "rpc streaming server fold: duplicate REQUEST for in-flight call_id; refusing",
2550                        );
2551                        let resp = RpcResponsePayload {
2552                            status: RpcStatus::Internal,
2553                            headers: vec![(
2554                                HEADER_NRPC_STREAMING.to_string(),
2555                                HEADER_NRPC_STREAMING_END.to_vec(),
2556                            )],
2557                            body: Bytes::from_static(
2558                                b"duplicate REQUEST for already-in-flight call_id",
2559                            ),
2560                        };
2561                        let emit = self.emit.clone();
2562                        let caller_origin = meta.origin_hash;
2563                        let call_id = meta.seq_or_ts;
2564                        tokio::spawn(async move {
2565                            emit(from_node, caller_origin, call_id, resp).await;
2566                        });
2567                        return Ok(());
2568                    }
2569                }
2570                // Cancellation token + in-flight bookkeeping —
2571                // identical to the unary fold's pattern.
2572                let cancellation = RpcCancellationToken::new();
2573                self.in_flight.lock().insert(key, cancellation.clone());
2574                // Flow-control opt-in: parse the
2575                // `nrpc-stream-window-initial` header. When
2576                // present, install a per-call semaphore the pump
2577                // task will await per chunk; subsequent
2578                // STREAM_GRANT events refill it. When absent, no
2579                // entry → pump skips the await (back-compat).
2580                let flow_sem = parse_stream_window_initial(&payload.headers).map(|n| {
2581                    let sem = Arc::new(tokio::sync::Semaphore::new(n as usize));
2582                    self.flow_control.lock().insert(key, sem.clone());
2583                    sem
2584                });
2585                let handler = self.handler.clone();
2586                let emit = self.emit.clone();
2587                let in_flight = self.in_flight.clone();
2588                let flow_control = self.flow_control.clone();
2589                let caller_origin = meta.origin_hash;
2590                let call_id = meta.seq_or_ts;
2591                let trace_context = if payload.flags & FLAG_RPC_PROPAGATE_TRACE != 0 {
2592                    extract_trace_context(&payload.headers)
2593                } else {
2594                    None
2595                };
2596                let metrics = self.metrics.clone();
2597                // See unary fold for rationale — clone the
2598                // cancellation handle so the spawned task can probe
2599                // it after the handler returns and override the
2600                // terminal frame with `RpcStatus::Cancelled`.
2601                let cancel_probe = cancellation.clone();
2602                tokio::spawn(async move {
2603                    if let Some(m) = metrics.as_ref() {
2604                        m.handler_invocations_total
2605                            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2606                        m.handler_in_flight
2607                            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2608                    }
2609                    let handler_started = std::time::Instant::now();
2610                    let ctx = RpcContext {
2611                        caller_origin,
2612                        call_id,
2613                        payload,
2614                        cancellation,
2615                        trace_context,
2616                        // Streaming is never protected (E1.8) — always public.
2617                        org_admission: None,
2618                    };
2619                    // Build the sink + receive end. Spawn a
2620                    // pump that forwards each chunk to the emit
2621                    // closure. The handler's `sink.send(...)`
2622                    // calls show up here as items on the receiver.
2623                    // **Bounded** at STREAMING_PUMP_CAPACITY: a
2624                    // runaway handler that produces chunks faster
2625                    // than the publish path can drain stops
2626                    // blocking the runtime past this many queued
2627                    // chunks; additional chunks are dropped and
2628                    // counted via streaming_chunks_dropped_total.
2629                    let (tx, mut rx) =
2630                        tokio::sync::mpsc::channel::<bytes::Bytes>(STREAMING_PUMP_CAPACITY);
2631                    let sink = RpcResponseSink {
2632                        inner: tx,
2633                        metrics: metrics.clone(),
2634                    };
2635                    let pump_emit = emit.clone();
2636                    let pump_metrics = metrics.clone();
2637                    let pump_flow = flow_sem.clone();
2638                    let pump = tokio::spawn(async move {
2639                        while let Some(chunk) = rx.recv().await {
2640                            // Flow control: when the caller opted
2641                            // in, await one semaphore permit per
2642                            // chunk before publishing. The semaphore
2643                            // starts at the caller's `initial_window`
2644                            // and refills when the caller sends
2645                            // STREAM_GRANT events. `forget()`
2646                            // consumes the slot — each chunk uses
2647                            // exactly one credit, never returned.
2648                            // No-op when `pump_flow` is None
2649                            // (back-compat path).
2650                            if let Some(sem) = pump_flow.as_ref() {
2651                                let permit = match sem.clone().acquire_owned().await {
2652                                    Ok(p) => p,
2653                                    Err(_) => {
2654                                        // Semaphore was closed —
2655                                        // shouldn't happen during
2656                                        // normal operation; bail.
2657                                        break;
2658                                    }
2659                                };
2660                                permit.forget();
2661                            }
2662                            if let Some(m) = pump_metrics.as_ref() {
2663                                m.streaming_chunks_emitted_total
2664                                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2665                            }
2666                            let resp = RpcResponsePayload {
2667                                status: RpcStatus::Ok,
2668                                headers: vec![(
2669                                    HEADER_NRPC_STREAMING.to_string(),
2670                                    HEADER_NRPC_STREAMING_CONTINUE.to_vec(),
2671                                )],
2672                                body: chunk.clone(),
2673                            };
2674                            // Await per-chunk publish so chunks for
2675                            // one call_id reach the network in send
2676                            // order. Without this, two chunks emitted
2677                            // in tight succession can race into the
2678                            // publish path and arrive out of order
2679                            // (or be eclipsed by the terminal frame
2680                            // and lost entirely on the caller side).
2681                            pump_emit(from_node, caller_origin, call_id, resp).await;
2682                        }
2683                    });
2684                    // Run the handler. Catch panics so a
2685                    // misbehaving handler can't take down the
2686                    // runtime — same shape as the unary fold.
2687                    let outcome = futures::FutureExt::catch_unwind(std::panic::AssertUnwindSafe(
2688                        handler.call(ctx, sink),
2689                    ))
2690                    .await;
2691                    // The handler dropped the sink (either by
2692                    // returning or by panicking through the
2693                    // catch_unwind). Wait for the pump to drain
2694                    // any final in-flight chunks.
2695                    let _ = pump.await;
2696                    if let Some(m) = metrics.as_ref() {
2697                        m.handler_in_flight
2698                            .fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
2699                        m.record_handler_duration(handler_started.elapsed());
2700                        if outcome.is_err() {
2701                            m.handler_panics_total
2702                                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2703                        }
2704                    }
2705                    // Emit the terminal frame. CANCEL-wins ordering
2706                    // matches the unary fold: if the cancellation
2707                    // token fired during execution, override the
2708                    // handler's terminal with `RpcStatus::Cancelled`.
2709                    let terminal = if cancel_probe.is_cancelled() {
2710                        RpcResponsePayload {
2711                            status: RpcStatus::Cancelled,
2712                            headers: vec![],
2713                            body: Bytes::from_static(
2714                                b"server observed CANCEL during streaming handler execution",
2715                            ),
2716                        }
2717                    } else {
2718                        match outcome {
2719                            Ok(Ok(())) => RpcResponsePayload {
2720                                status: RpcStatus::Ok,
2721                                headers: vec![(
2722                                    HEADER_NRPC_STREAMING.to_string(),
2723                                    HEADER_NRPC_STREAMING_END.to_vec(),
2724                                )],
2725                                body: Bytes::new(),
2726                            },
2727                            Ok(Err(RpcHandlerError::Application { code, message })) => {
2728                                RpcResponsePayload {
2729                                    status: RpcStatus::Application(code),
2730                                    headers: vec![],
2731                                    body: Bytes::from(message),
2732                                }
2733                            }
2734                            Ok(Err(RpcHandlerError::Internal(message))) => RpcResponsePayload {
2735                                status: RpcStatus::Internal,
2736                                headers: vec![],
2737                                body: Bytes::from(message),
2738                            },
2739                            Err(panic) => {
2740                                let panic_msg = panic
2741                                    .downcast_ref::<&'static str>()
2742                                    .map(|s| s.to_string())
2743                                    .or_else(|| panic.downcast_ref::<String>().cloned())
2744                                    .unwrap_or_else(|| "<non-string panic>".into());
2745                                tracing::error!(
2746                                    caller_origin = format!("{:#x}", caller_origin),
2747                                    call_id,
2748                                    panic = %panic_msg,
2749                                    "rpc streaming server handler panicked",
2750                                );
2751                                RpcResponsePayload {
2752                                    status: RpcStatus::Internal,
2753                                    headers: vec![],
2754                                    body: Bytes::from(format!("handler panicked: {panic_msg}")),
2755                                }
2756                            }
2757                        }
2758                    };
2759                    in_flight.lock().remove(&key);
2760                    // Drop the per-call flow-control semaphore
2761                    // (if any) so a stale GRANT arriving after
2762                    // termination is silently dropped — the entry
2763                    // is gone, lookup misses.
2764                    flow_control.lock().remove(&key);
2765                    // Await the terminal frame's publish too so it
2766                    // arrives strictly AFTER the last chunk on the
2767                    // wire (the pump has already drained, but the
2768                    // emit itself is still async and we must await
2769                    // it before the spawned task ends).
2770                    emit(from_node, caller_origin, call_id, terminal).await;
2771                });
2772            }
2773            DISPATCH_RPC_CANCEL => {
2774                if let Some(token) = self.in_flight.lock().remove(&key) {
2775                    token.cancel();
2776                }
2777                // Also drop the flow-control entry — the spawned
2778                // task's terminal cleanup will run too, but doing
2779                // it here makes the CANCEL path immediately stop
2780                // refilling the pump (the pending `acquire().await`
2781                // will resolve once the semaphore is dropped or
2782                // when the task exits).
2783                self.flow_control.lock().remove(&key);
2784            }
2785            DISPATCH_RPC_STREAM_GRANT => {
2786                // Add credit to the per-call semaphore. Silently
2787                // drop GRANT events for unknown / non-flow-
2788                // controlled calls — server can't tell whether
2789                // the caller is racing a terminal vs. sending a
2790                // grant for a non-flow-controlled stream, and
2791                // both are harmless to ignore.
2792                let amount = match decode_stream_grant(&frame[RPC_FRAME_BODY_OFFSET..]) {
2793                    Some(n) => n,
2794                    None => {
2795                        tracing::debug!(
2796                            caller_origin = format!("{:#x}", meta.origin_hash),
2797                            call_id = meta.seq_or_ts,
2798                            "rpc streaming server fold: malformed STREAM_GRANT payload",
2799                        );
2800                        return Ok(());
2801                    }
2802                };
2803                if amount == 0 {
2804                    return Ok(());
2805                }
2806                if let Some(sem) = self.flow_control.lock().get(&key).cloned() {
2807                    // Tokio's `Semaphore::add_permits` is bounded
2808                    // by `MAX_PERMITS = usize::MAX >> 3`. A
2809                    // misbehaving caller flooding huge grants
2810                    // would eventually saturate; cap defensively.
2811                    let safe = (amount as usize).min(usize::MAX >> 4);
2812                    sem.add_permits(safe);
2813                }
2814            }
2815            _ => {}
2816        }
2817        Ok(())
2818    }
2819}
2820
2821impl RedexFold<()> for RpcServerStreamingFold {
2822    /// Loopback / test shim: drives `apply_frame` with
2823    /// `from_node = 0` (AV-1 item 1). Production uses
2824    /// [`RpcServerStreamingFold::apply_inbound`].
2825    fn apply(&mut self, ev: &RedexEvent, _state: &mut ()) -> Result<(), RedexError> {
2826        self.apply_frame(0, &ev.payload)
2827    }
2828}
2829
2830// ============================================================================
2831// Phase B — server-side fold for client-streaming.
2832//
2833// `RpcStreamingRequestFold` mirrors `RpcServerStreamingFold` but
2834// flipped on the data-direction axis: the SERVER consumes a
2835// stream of REQUEST_CHUNK events and the handler produces ONE
2836// terminal RESPONSE (vs. the response-side fold where one REQUEST
2837// drives many RESPONSE chunks).
2838//
2839// Wire shape it handles:
2840//   DISPATCH_RPC_REQUEST       (FLAG_RPC_CLIENT_STREAMING_REQUEST)
2841//   DISPATCH_RPC_REQUEST_CHUNK (zero or more)
2842//   DISPATCH_RPC_REQUEST_CHUNK (FLAG_RPC_REQUEST_END)
2843//   DISPATCH_RPC_CANCEL        (any time; flips token + closes stream)
2844//
2845// Wire shape it EMITS (via callbacks):
2846//   DISPATCH_RPC_RESPONSE        (one terminal frame; via RpcResponseEmitter)
2847//   DISPATCH_RPC_REQUEST_GRANT   (one per consumed chunk when flow
2848//                                 control is opted in; via
2849//                                 RpcRequestGrantEmitter)
2850//
2851// Each service binds to exactly one fold shape (unary, server-
2852// streaming, or client-streaming) at `serve_rpc*` registration.
2853// A REQUEST without FLAG_RPC_CLIENT_STREAMING_REQUEST that lands
2854// on the client-streaming fold is a caller bug — the fold emits a
2855// terminal `Internal` and drops the call.
2856// ============================================================================
2857
2858/// Per-call request-direction sender map type. Keyed on
2859/// `(from_node, caller_origin_hash, call_id)` (AV-1 item 1): the
2860/// AEAD-authenticated last-hop session peer is part of the key so a
2861/// peer cannot push a REQUEST_CHUNK into another peer's upload stream
2862/// by copying its origin + call_id. Value is the bounded mpsc sender
2863/// the fold's `apply_frame()` pushes REQUEST_CHUNK bodies into. The
2864/// matching receiver lives inside the handler's [`RequestStream`];
2865/// dropping the sender (on REQUEST_END or CANCEL) closes the stream.
2866type RequestChunkSenders =
2867    Arc<Mutex<HashMap<(u64, u64, u64), tokio::sync::mpsc::Sender<bytes::Bytes>>>>;
2868
2869/// Shared REQUEST_CHUNK handling used by both
2870/// [`RpcStreamingRequestFold`] and [`RpcDuplexFold`]. Decodes the
2871/// payload, validates the call_id agreement, looks up the per-call
2872/// sender, pushes the body (skipping the empty-body FLAG_END
2873/// terminator), and removes the sender on FLAG_END so the
2874/// handler's stream observes EOF.
2875///
2876/// `diag_tag` selects the log prefix ("client-streaming" or
2877/// "duplex") so the two call sites surface identically-shaped
2878/// diagnostics with the correct fold name. The behavior is
2879/// otherwise identical — both folds carry the same wire format
2880/// and the same per-call mpsc + sender-map contract.
2881fn apply_request_chunk_to_senders(
2882    from_node: u64,
2883    payload_bytes: Bytes,
2884    meta: &EventMeta,
2885    senders: &RequestChunkSenders,
2886    diag_tag: &'static str,
2887) {
2888    let payload = match RpcRequestChunkPayload::decode(payload_bytes) {
2889        Ok(p) => p,
2890        Err(e) => {
2891            tracing::warn!(
2892                error = %e,
2893                caller_origin = format!("{:#x}", meta.origin_hash),
2894                call_id = meta.seq_or_ts,
2895                tag = diag_tag,
2896                "rpc server fold: malformed REQUEST_CHUNK payload",
2897            );
2898            return;
2899        }
2900    };
2901    if payload.call_id != meta.seq_or_ts {
2902        tracing::warn!(
2903            caller_origin = format!("{:#x}", meta.origin_hash),
2904            meta_call_id = meta.seq_or_ts,
2905            payload_call_id = payload.call_id,
2906            tag = diag_tag,
2907            "rpc server fold: REQUEST_CHUNK payload call_id does not match EventMeta",
2908        );
2909        return;
2910    }
2911    // Scope the sender lookup to the authenticated session peer so a
2912    // forged REQUEST_CHUNK carrying another peer's origin + call_id
2913    // misses the map (AV-1 item 1).
2914    let key = (from_node, meta.origin_hash, meta.seq_or_ts);
2915    let is_end = payload.flags & FLAG_RPC_REQUEST_END != 0;
2916    let sender = senders.lock().get(&key).cloned();
2917    let Some(sender) = sender else {
2918        // Unknown call — either the initial REQUEST hasn't
2919        // arrived yet (out-of-order delivery is possible on the
2920        // bus) or the handler already completed and the entry is
2921        // gone. Drop silently.
2922        tracing::debug!(
2923            caller_origin = format!("{:#x}", meta.origin_hash),
2924            call_id = meta.seq_or_ts,
2925            tag = diag_tag,
2926            "rpc server fold: REQUEST_CHUNK for unknown call_id; dropping",
2927        );
2928        return;
2929    };
2930    let is_pure_terminator = is_end && payload.body.is_empty();
2931    if !is_pure_terminator && sender.try_send(payload.body).is_err() {
2932        tracing::debug!(
2933            caller_origin = format!("{:#x}", meta.origin_hash),
2934            call_id = meta.seq_or_ts,
2935            tag = diag_tag,
2936            "rpc server fold: request-chunk mpsc full or closed; dropping",
2937        );
2938    }
2939    if is_end {
2940        // Drop the sender from the map → its clone here goes out
2941        // of scope at end of function → the receiver in the
2942        // handler's RequestStream sees EOF on the next poll.
2943        senders.lock().remove(&key);
2944    }
2945}
2946
2947/// Server-side fold for client-streaming RPC. Parallel to
2948/// [`RpcServerStreamingFold`] but consumes REQUEST_CHUNK on the
2949/// input side and produces one terminal RESPONSE on the output
2950/// side (vs. one REQUEST in / many RESPONSE chunks out).
2951///
2952/// State `()` — like the other folds, application state lives in
2953/// the handler's captured `Arc<Mutex<S>>`. The fold's own state
2954/// (in-flight cancellation tokens + per-call request-chunk
2955/// senders) lives on `&mut self` via `Arc<Mutex<...>>` so spawned
2956/// handler tasks can self-clean on completion.
2957///
2958/// Bidi streaming plan (Phase B).
2959pub struct RpcStreamingRequestFold {
2960    handler: Arc<dyn RpcClientStreamingHandler>,
2961    emit: RpcResponseEmitter,
2962    /// Optional request-direction grant emitter. `Some(...)`
2963    /// when the surrounding mesh glue is wired to publish
2964    /// REQUEST_GRANT events; `None` in unit tests / contexts
2965    /// without a real publish path. When `None`, the auto-grant
2966    /// path on every `RequestStream::poll_next` becomes a no-op
2967    /// (callers that opted into flow control will see no
2968    /// refill and stall once their initial window is exhausted —
2969    /// honest behavior for a fold not wired up for grants).
2970    grant_emit: Option<RpcRequestGrantEmitter>,
2971    /// (from_node, caller_origin, call_id) → cancellation token —
2972    /// authenticated-peer-scoped (AV-1 item 1).
2973    in_flight: InFlightCalls,
2974    senders: RequestChunkSenders,
2975    /// Optional per-service metrics handle. Same shape as the
2976    /// other folds. Reuses the response-side counters where they
2977    /// apply (handler_invocations / handler_panics / etc.) and
2978    /// would gain request-side counters (e.g.
2979    /// `streaming_request_chunks_dropped_total`) in a follow-up.
2980    metrics: Option<Arc<crate::adapter::net::mesh_rpc_metrics::ServiceMetricsAtomic>>,
2981}
2982
2983impl RpcStreamingRequestFold {
2984    /// Construct a client-streaming server fold. `emit` publishes
2985    /// the terminal RESPONSE on the caller's reply channel.
2986    ///
2987    /// Use the sync [`RpcResponseEmitter`] here — there's only
2988    /// one RESPONSE per call (the terminal frame), so the
2989    /// per-call serialization the async emitter buys for the
2990    /// response-side fold is not needed here.
2991    pub fn new(handler: Arc<dyn RpcClientStreamingHandler>, emit: RpcResponseEmitter) -> Self {
2992        Self {
2993            handler,
2994            emit,
2995            grant_emit: None,
2996            in_flight: Arc::new(Mutex::new(HashMap::new())),
2997            senders: Arc::new(Mutex::new(HashMap::new())),
2998            metrics: None,
2999        }
3000    }
3001
3002    /// Attach the request-direction grant emitter. Hands every
3003    /// `RequestStream::poll_next` a hook to fire one REQUEST_GRANT
3004    /// back to the caller after a chunk is consumed. Optional —
3005    /// folds constructed without it still work, callers that
3006    /// opted into flow control just won't be refilled.
3007    pub fn with_grant_emitter(mut self, grant_emit: RpcRequestGrantEmitter) -> Self {
3008        self.grant_emit = Some(grant_emit);
3009        self
3010    }
3011
3012    /// Attach a per-service metrics handle. Hooks the spawned
3013    /// handler task to bump `handler_invocations_total` /
3014    /// `handler_in_flight` / `handler_panics_total` /
3015    /// `handler_duration_*`. Symmetric with `RpcServerFold` and
3016    /// `RpcServerStreamingFold`.
3017    pub fn with_metrics(
3018        mut self,
3019        metrics: Arc<crate::adapter::net::mesh_rpc_metrics::ServiceMetricsAtomic>,
3020    ) -> Self {
3021        self.metrics = Some(metrics);
3022        self
3023    }
3024
3025    /// Test-only: snapshot of the in-flight call set.
3026    #[cfg(test)]
3027    pub fn in_flight_keys(&self) -> Vec<(u64, u64, u64)> {
3028        self.in_flight.lock().keys().copied().collect()
3029    }
3030
3031    /// Test-only: snapshot of the in-flight per-call senders.
3032    /// Useful for tests that need to assert a call's sender has
3033    /// been dropped after REQUEST_END / CANCEL.
3034    #[cfg(test)]
3035    pub fn sender_keys(&self) -> Vec<(u64, u64, u64)> {
3036        self.senders.lock().keys().copied().collect()
3037    }
3038}
3039
3040impl RpcStreamingRequestFold {
3041    /// Production-path entry point. Keys per-call state (in-flight
3042    /// token + request-chunk sender) by `(from_node, claimed_origin,
3043    /// call_id)` so a forged REQUEST_CHUNK / CANCEL from another peer
3044    /// misses the map and no-ops (AV-1 item 1).
3045    pub fn apply_inbound(&mut self, ev: &RpcInboundEvent) -> Result<(), RedexError> {
3046        self.apply_frame(ev.from_node, &ev.payload)
3047    }
3048
3049    /// Core frame application shared by [`Self::apply_inbound`] (real
3050    /// `from_node`) and the [`RedexFold`] loopback shim (`0`).
3051    fn apply_frame(&mut self, from_node: u64, frame: &Bytes) -> Result<(), RedexError> {
3052        let Some(meta) = (if frame.len() >= EVENT_META_SIZE {
3053            EventMeta::from_bytes(&frame[..EVENT_META_SIZE])
3054        } else {
3055            None
3056        }) else {
3057            tracing::warn!(
3058                payload_len = frame.len(),
3059                "rpc client-streaming server fold: event payload too short for EventMeta",
3060            );
3061            return Ok(());
3062        };
3063        let key = (from_node, meta.origin_hash, meta.seq_or_ts);
3064        match meta.dispatch {
3065            DISPATCH_RPC_REQUEST => {
3066                let payload = match RpcRequestPayload::decode(frame.slice(RPC_FRAME_BODY_OFFSET..))
3067                {
3068                    Ok(p) => p,
3069                    Err(e) => {
3070                        tracing::warn!(
3071                            error = %e,
3072                            caller_origin = format!("{:#x}", meta.origin_hash),
3073                            call_id = meta.seq_or_ts,
3074                            "rpc client-streaming server fold: malformed request payload",
3075                        );
3076                        let resp = RpcResponsePayload {
3077                            status: RpcStatus::UnknownVersion,
3078                            headers: vec![],
3079                            body: Bytes::from(format!("malformed request: {e}")),
3080                        };
3081                        (self.emit)(from_node, meta.origin_hash, meta.seq_or_ts, resp);
3082                        return Ok(());
3083                    }
3084                };
3085                // A REQUEST without the client-streaming flag on
3086                // this fold is a caller bug — the service was
3087                // registered as client-streaming. Refuse cleanly.
3088                if payload.flags & FLAG_RPC_CLIENT_STREAMING_REQUEST == 0 {
3089                    tracing::warn!(
3090                        caller_origin = format!("{:#x}", meta.origin_hash),
3091                        call_id = meta.seq_or_ts,
3092                        flags = format!("{:#06x}", payload.flags),
3093                        "rpc client-streaming server fold: REQUEST missing FLAG_RPC_CLIENT_STREAMING_REQUEST",
3094                    );
3095                    let resp = RpcResponsePayload {
3096                        status: RpcStatus::Internal,
3097                        headers: vec![],
3098                        body: Bytes::from_static(
3099                            b"REQUEST on a client-streaming service must set FLAG_RPC_CLIENT_STREAMING_REQUEST",
3100                        ),
3101                    };
3102                    (self.emit)(from_node, meta.origin_hash, meta.seq_or_ts, resp);
3103                    return Ok(());
3104                }
3105                // Refuse a duplicate REQUEST with the same
3106                // `(origin_hash, call_id)` — same rationale as
3107                // the response-side fold: a retry that arrives
3108                // while the first attempt is still in-flight
3109                // would overwrite the prior sender and orphan the
3110                // existing handler.
3111                {
3112                    let in_flight = self.in_flight.lock();
3113                    if in_flight.contains_key(&key) {
3114                        drop(in_flight);
3115                        tracing::warn!(
3116                            caller_origin = format!("{:#x}", meta.origin_hash),
3117                            call_id = meta.seq_or_ts,
3118                            "rpc client-streaming server fold: duplicate REQUEST for in-flight call_id; refusing",
3119                        );
3120                        let resp = RpcResponsePayload {
3121                            status: RpcStatus::Internal,
3122                            headers: vec![],
3123                            body: Bytes::from_static(
3124                                b"duplicate REQUEST for already-in-flight call_id",
3125                            ),
3126                        };
3127                        (self.emit)(from_node, meta.origin_hash, meta.seq_or_ts, resp);
3128                        return Ok(());
3129                    }
3130                }
3131                let cancellation = RpcCancellationToken::new();
3132                self.in_flight.lock().insert(key, cancellation.clone());
3133                // Build the per-call request-chunk mpsc. Bounded
3134                // capacity — overflow on the sender side drops the
3135                // chunk (caller can re-send or, if flow-control is
3136                // wired, will naturally not push past the credit
3137                // window).
3138                let (tx, rx) =
3139                    tokio::sync::mpsc::channel::<bytes::Bytes>(STREAMING_REQUEST_PUMP_CAPACITY);
3140                // Terminator-semantics rule: an empty body
3141                // combined with FLAG_REQUEST_END is a pure
3142                // terminator — the caller's `finish()` emits it
3143                // to close the stream without yielding a phantom
3144                // empty item to the handler. A non-empty body on
3145                // a FLAG_END frame IS a final item (used by the
3146                // "single-item degenerate path": initial REQUEST
3147                // with FLAG_END + a real body sends one item +
3148                // closes in a single frame).
3149                let end_on_initial = payload.flags & FLAG_RPC_REQUEST_END != 0;
3150                let is_pure_terminator = end_on_initial && payload.body.is_empty();
3151                if !is_pure_terminator {
3152                    // Fresh `mpsc::channel(STREAMING_REQUEST_PUMP_CAPACITY)`
3153                    // with a live receiver — try_send cannot fail.
3154                    // debug_assert surfaces the invariant break in
3155                    // tests; release logs at error level rather than
3156                    // silently swallowing the first request body.
3157                    if tx.try_send(payload.body).is_err() {
3158                        debug_assert!(
3159                            false,
3160                            "fresh client-streaming request mpsc rejected initial body"
3161                        );
3162                        tracing::error!(
3163                            caller_origin = format!("{:#x}", meta.origin_hash),
3164                            call_id = meta.seq_or_ts,
3165                            "rpc client-streaming server fold: fresh mpsc rejected initial REQUEST body (invariant break)",
3166                        );
3167                    }
3168                }
3169                // If the initial REQUEST also set FLAG_REQUEST_END,
3170                // close the stream immediately — degenerate case of
3171                // "one-item upload" where the caller didn't bother
3172                // with a trailing REQUEST_CHUNK. Don't even insert
3173                // the sender into the map; just drop it here.
3174                if !end_on_initial {
3175                    self.senders.lock().insert(key, tx);
3176                }
3177                // Build the handler's context + stream. Auto-grant
3178                // is opted into when the caller set the request
3179                // window header AND the fold was wired with a
3180                // grant emitter; both must be present for grants
3181                // to actually fly.
3182                let grant_emitter = if parse_request_window_initial(&payload.headers).is_some() {
3183                    self.grant_emit.clone()
3184                } else {
3185                    None
3186                };
3187                let request_stream = RequestStream::new(
3188                    rx,
3189                    grant_emitter,
3190                    from_node,
3191                    meta.origin_hash,
3192                    meta.seq_or_ts,
3193                );
3194                let trace_context = if payload.flags & FLAG_RPC_PROPAGATE_TRACE != 0 {
3195                    extract_trace_context(&payload.headers)
3196                } else {
3197                    None
3198                };
3199                let deadline_ns = payload.deadline_ns;
3200                let ctx = RpcStreamingContext {
3201                    caller_origin: meta.origin_hash,
3202                    call_id: meta.seq_or_ts,
3203                    deadline_ns,
3204                    headers: payload.headers,
3205                    cancellation: cancellation.clone(),
3206                    trace_context,
3207                };
3208                let handler = self.handler.clone();
3209                let emit = self.emit.clone();
3210                let in_flight = self.in_flight.clone();
3211                let senders = self.senders.clone();
3212                let caller_origin = meta.origin_hash;
3213                let call_id = meta.seq_or_ts;
3214                let cancel_probe = cancellation.clone();
3215                let cancel_for_deadline = cancellation.clone();
3216                let metrics = self.metrics.clone();
3217                tokio::spawn(async move {
3218                    if let Some(m) = metrics.as_ref() {
3219                        m.handler_invocations_total
3220                            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3221                        m.handler_in_flight
3222                            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3223                    }
3224                    let handler_started = std::time::Instant::now();
3225                    // Deadline guard: if the caller declared
3226                    // `deadline_ns`, force-drop the handler future
3227                    // after it elapses so an orphaned request stream
3228                    // (caller-side network partition before
3229                    // REQUEST_END arrives) can never hang the call
3230                    // indefinitely. `deadline_ns = 0` means "no
3231                    // deadline" — caller's responsibility.
3232                    let call_fut = futures::FutureExt::catch_unwind(std::panic::AssertUnwindSafe(
3233                        handler.call(ctx, request_stream),
3234                    ));
3235                    let outcome = if deadline_ns > 0 {
3236                        let now_ns = std::time::SystemTime::now()
3237                            .duration_since(std::time::UNIX_EPOCH)
3238                            .map(|d| d.as_nanos() as u64)
3239                            .unwrap_or(0);
3240                        let remaining = deadline_ns.saturating_sub(now_ns);
3241                        if remaining == 0 {
3242                            cancel_for_deadline.cancel();
3243                            Ok(Err(RpcHandlerError::Internal(
3244                                "handler deadline_ns already expired at spawn".to_string(),
3245                            )))
3246                        } else {
3247                            match tokio::time::timeout(
3248                                std::time::Duration::from_nanos(remaining),
3249                                call_fut,
3250                            )
3251                            .await
3252                            {
3253                                Ok(o) => o,
3254                                Err(_) => {
3255                                    cancel_for_deadline.cancel();
3256                                    Ok(Err(RpcHandlerError::Internal(
3257                                        "handler deadline_ns exceeded".to_string(),
3258                                    )))
3259                                }
3260                            }
3261                        }
3262                    } else {
3263                        call_fut.await
3264                    };
3265                    if let Some(m) = metrics.as_ref() {
3266                        m.handler_in_flight
3267                            .fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
3268                        m.record_handler_duration(handler_started.elapsed());
3269                        if outcome.is_err() {
3270                            m.handler_panics_total
3271                                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3272                        }
3273                    }
3274                    // CANCEL-wins ordering: if the cancellation
3275                    // token fired during execution, override the
3276                    // handler's terminal with Cancelled.
3277                    let terminal = if cancel_probe.is_cancelled() {
3278                        RpcResponsePayload {
3279                            status: RpcStatus::Cancelled,
3280                            headers: vec![],
3281                            body: Bytes::from_static(
3282                                b"server observed CANCEL during client-streaming handler execution",
3283                            ),
3284                        }
3285                    } else {
3286                        match outcome {
3287                            Ok(Ok(resp)) => resp,
3288                            Ok(Err(RpcHandlerError::Application { code, message })) => {
3289                                RpcResponsePayload {
3290                                    status: RpcStatus::Application(code),
3291                                    headers: vec![],
3292                                    body: Bytes::from(message),
3293                                }
3294                            }
3295                            Ok(Err(RpcHandlerError::Internal(message))) => RpcResponsePayload {
3296                                status: RpcStatus::Internal,
3297                                headers: vec![],
3298                                body: Bytes::from(message),
3299                            },
3300                            Err(panic) => {
3301                                let panic_msg = panic
3302                                    .downcast_ref::<&'static str>()
3303                                    .map(|s| s.to_string())
3304                                    .or_else(|| panic.downcast_ref::<String>().cloned())
3305                                    .unwrap_or_else(|| "<non-string panic>".into());
3306                                tracing::error!(
3307                                    caller_origin = format!("{:#x}", caller_origin),
3308                                    call_id,
3309                                    panic = %panic_msg,
3310                                    "rpc client-streaming server handler panicked",
3311                                );
3312                                RpcResponsePayload {
3313                                    status: RpcStatus::Internal,
3314                                    headers: vec![],
3315                                    body: Bytes::from(format!("handler panicked: {panic_msg}")),
3316                                }
3317                            }
3318                        }
3319                    };
3320                    in_flight.lock().remove(&key);
3321                    // Drop the per-call request-chunk sender too
3322                    // (idempotent — already gone if REQUEST_END
3323                    // arrived; defensive otherwise so a handler
3324                    // that returned without consuming all chunks
3325                    // doesn't leak the entry).
3326                    senders.lock().remove(&key);
3327                    (emit)(from_node, caller_origin, call_id, terminal);
3328                });
3329            }
3330            DISPATCH_RPC_REQUEST_CHUNK => {
3331                apply_request_chunk_to_senders(
3332                    from_node,
3333                    frame.slice(RPC_FRAME_BODY_OFFSET..),
3334                    &meta,
3335                    &self.senders,
3336                    "client-streaming",
3337                );
3338            }
3339            DISPATCH_RPC_CANCEL => {
3340                if let Some(token) = self.in_flight.lock().remove(&key) {
3341                    token.cancel();
3342                }
3343                // Drop the per-call sender so the handler's
3344                // RequestStream yields None on the next poll
3345                // (handler observes cancel via the token OR via
3346                // the stream's EOF; the cancel_probe in the
3347                // spawned task ensures the terminal RESPONSE is
3348                // Cancelled regardless of which the handler
3349                // checks first).
3350                self.senders.lock().remove(&key);
3351            }
3352            _ => {}
3353        }
3354        Ok(())
3355    }
3356}
3357
3358impl RedexFold<()> for RpcStreamingRequestFold {
3359    /// Loopback / test shim: drives `apply_frame` with
3360    /// `from_node = 0` (AV-1 item 1). Production uses
3361    /// [`RpcStreamingRequestFold::apply_inbound`].
3362    fn apply(&mut self, ev: &RedexEvent, _state: &mut ()) -> Result<(), RedexError> {
3363        self.apply_frame(0, &ev.payload)
3364    }
3365}
3366
3367// ============================================================================
3368// Phase D — server-side fold for full duplex.
3369//
3370// `RpcDuplexFold` is the hybrid of `RpcStreamingRequestFold`
3371// (Phase B — request side) and `RpcServerStreamingFold` (existing
3372// — response side). The handler trait takes BOTH a `RequestStream`
3373// AND an `RpcResponseSink`; the fold spawns one handler task per
3374// REQUEST and one pump task per call_id, then emits a terminal
3375// RESPONSE on handler return.
3376//
3377// Wire shape it consumes:
3378//   DISPATCH_RPC_REQUEST       (FLAG_CLIENT_STREAMING_REQUEST + FLAG_STREAMING_RESPONSE)
3379//   DISPATCH_RPC_REQUEST_CHUNK (zero or more, with FLAG_REQUEST_END on the last)
3380//   DISPATCH_RPC_CANCEL        (flips token + closes both directions)
3381//
3382// Wire shape it produces:
3383//   DISPATCH_RPC_RESPONSE        (multi-fire; nrpc-streaming: continue / end)
3384//   DISPATCH_RPC_REQUEST_GRANT   (one per consumed request-chunk when flow
3385//                                 control is opted in)
3386//
3387// Bidi streaming plan (Phase D).
3388// ============================================================================
3389
3390/// Server-side fold for duplex RPC. Composes Phase B's request
3391/// stream + per-call request-chunk senders with the existing
3392/// response-side pump + multi-fire RESPONSE emit.
3393///
3394/// State `()` — same as the sibling folds.
3395///
3396/// Bidi streaming plan (Phase D).
3397pub struct RpcDuplexFold {
3398    handler: Arc<dyn RpcDuplexHandler>,
3399    /// Async emitter for response chunks (per-call ordering via
3400    /// awaited emits — same rationale as `RpcServerStreamingFold`).
3401    emit: RpcAsyncResponseEmitter,
3402    /// Optional request-direction grant emitter.
3403    grant_emit: Option<RpcRequestGrantEmitter>,
3404    /// (from_node, caller_origin, call_id) → cancellation token —
3405    /// authenticated-peer-scoped (AV-1 item 1).
3406    in_flight: InFlightCalls,
3407    senders: RequestChunkSenders,
3408    metrics: Option<Arc<crate::adapter::net::mesh_rpc_metrics::ServiceMetricsAtomic>>,
3409}
3410
3411impl RpcDuplexFold {
3412    /// Construct a duplex server fold. `emit` publishes individual
3413    /// response chunks AND the terminal frame on the caller's
3414    /// reply channel (uses the async emitter for per-call
3415    /// ordering, same as `RpcServerStreamingFold`).
3416    pub fn new(handler: Arc<dyn RpcDuplexHandler>, emit: RpcAsyncResponseEmitter) -> Self {
3417        Self {
3418            handler,
3419            emit,
3420            grant_emit: None,
3421            in_flight: Arc::new(Mutex::new(HashMap::new())),
3422            senders: Arc::new(Mutex::new(HashMap::new())),
3423            metrics: None,
3424        }
3425    }
3426
3427    /// Attach the request-direction grant emitter. See
3428    /// [`RpcStreamingRequestFold::with_grant_emitter`] for the
3429    /// auto-grant behavior. When unset, callers that opted into
3430    /// flow control simply won't get refilled.
3431    pub fn with_grant_emitter(mut self, grant_emit: RpcRequestGrantEmitter) -> Self {
3432        self.grant_emit = Some(grant_emit);
3433        self
3434    }
3435
3436    /// Attach a per-service metrics handle. Bumps
3437    /// handler_invocations / handler_in_flight / handler_panics /
3438    /// handler_duration_* + the response pump's
3439    /// streaming_chunks_emitted_total per emitted chunk.
3440    pub fn with_metrics(
3441        mut self,
3442        metrics: Arc<crate::adapter::net::mesh_rpc_metrics::ServiceMetricsAtomic>,
3443    ) -> Self {
3444        self.metrics = Some(metrics);
3445        self
3446    }
3447
3448    /// Test-only: snapshot of the in-flight call set.
3449    #[cfg(test)]
3450    pub fn in_flight_keys(&self) -> Vec<(u64, u64, u64)> {
3451        self.in_flight.lock().keys().copied().collect()
3452    }
3453
3454    /// Test-only: snapshot of the in-flight per-call senders.
3455    #[cfg(test)]
3456    pub fn sender_keys(&self) -> Vec<(u64, u64, u64)> {
3457        self.senders.lock().keys().copied().collect()
3458    }
3459}
3460
3461impl RpcDuplexFold {
3462    /// Production-path entry point. Keys per-call state (in-flight
3463    /// token + request-chunk sender) by `(from_node, claimed_origin,
3464    /// call_id)` so a forged REQUEST_CHUNK / CANCEL from another peer
3465    /// misses the map and no-ops (AV-1 item 1).
3466    pub fn apply_inbound(&mut self, ev: &RpcInboundEvent) -> Result<(), RedexError> {
3467        self.apply_frame(ev.from_node, &ev.payload)
3468    }
3469
3470    /// Core frame application shared by [`Self::apply_inbound`] (real
3471    /// `from_node`) and the [`RedexFold`] loopback shim (`0`).
3472    fn apply_frame(&mut self, from_node: u64, frame: &Bytes) -> Result<(), RedexError> {
3473        let Some(meta) = (if frame.len() >= EVENT_META_SIZE {
3474            EventMeta::from_bytes(&frame[..EVENT_META_SIZE])
3475        } else {
3476            None
3477        }) else {
3478            tracing::warn!(
3479                payload_len = frame.len(),
3480                "rpc duplex server fold: event payload too short for EventMeta",
3481            );
3482            return Ok(());
3483        };
3484        let key = (from_node, meta.origin_hash, meta.seq_or_ts);
3485        match meta.dispatch {
3486            DISPATCH_RPC_REQUEST => {
3487                let payload = match RpcRequestPayload::decode(frame.slice(RPC_FRAME_BODY_OFFSET..))
3488                {
3489                    Ok(p) => p,
3490                    Err(e) => {
3491                        tracing::warn!(
3492                            error = %e,
3493                            caller_origin = format!("{:#x}", meta.origin_hash),
3494                            call_id = meta.seq_or_ts,
3495                            "rpc duplex server fold: malformed request payload",
3496                        );
3497                        let resp = RpcResponsePayload {
3498                            status: RpcStatus::UnknownVersion,
3499                            headers: vec![(
3500                                HEADER_NRPC_STREAMING.to_string(),
3501                                HEADER_NRPC_STREAMING_END.to_vec(),
3502                            )],
3503                            body: Bytes::from(format!("malformed request: {e}")),
3504                        };
3505                        let emit = self.emit.clone();
3506                        let caller_origin = meta.origin_hash;
3507                        let call_id = meta.seq_or_ts;
3508                        tokio::spawn(async move {
3509                            emit(from_node, caller_origin, call_id, resp).await;
3510                        });
3511                        return Ok(());
3512                    }
3513                };
3514                // Caller-bug guard: a duplex REQUEST must set
3515                // BOTH the client-streaming flag (we'll receive
3516                // request chunks) AND the streaming-response flag
3517                // (we'll emit response chunks). Missing flags →
3518                // refuse cleanly.
3519                let required = FLAG_RPC_CLIENT_STREAMING_REQUEST | FLAG_RPC_STREAMING_RESPONSE;
3520                if payload.flags & required != required {
3521                    tracing::warn!(
3522                        caller_origin = format!("{:#x}", meta.origin_hash),
3523                        call_id = meta.seq_or_ts,
3524                        flags = format!("{:#06x}", payload.flags),
3525                        "rpc duplex server fold: REQUEST missing required flags",
3526                    );
3527                    let resp = RpcResponsePayload {
3528                        status: RpcStatus::Internal,
3529                        headers: vec![(
3530                            HEADER_NRPC_STREAMING.to_string(),
3531                            HEADER_NRPC_STREAMING_END.to_vec(),
3532                        )],
3533                        body: Bytes::from_static(
3534                            b"REQUEST on a duplex service must set FLAG_RPC_CLIENT_STREAMING_REQUEST and FLAG_RPC_STREAMING_RESPONSE",
3535                        ),
3536                    };
3537                    let emit = self.emit.clone();
3538                    let caller_origin = meta.origin_hash;
3539                    let call_id = meta.seq_or_ts;
3540                    tokio::spawn(async move {
3541                        emit(from_node, caller_origin, call_id, resp).await;
3542                    });
3543                    return Ok(());
3544                }
3545                // Duplicate-REQUEST refusal.
3546                {
3547                    let in_flight = self.in_flight.lock();
3548                    if in_flight.contains_key(&key) {
3549                        drop(in_flight);
3550                        tracing::warn!(
3551                            caller_origin = format!("{:#x}", meta.origin_hash),
3552                            call_id = meta.seq_or_ts,
3553                            "rpc duplex server fold: duplicate REQUEST for in-flight call_id; refusing",
3554                        );
3555                        let resp = RpcResponsePayload {
3556                            status: RpcStatus::Internal,
3557                            headers: vec![(
3558                                HEADER_NRPC_STREAMING.to_string(),
3559                                HEADER_NRPC_STREAMING_END.to_vec(),
3560                            )],
3561                            body: Bytes::from_static(
3562                                b"duplicate REQUEST for already-in-flight call_id",
3563                            ),
3564                        };
3565                        let emit = self.emit.clone();
3566                        let caller_origin = meta.origin_hash;
3567                        let call_id = meta.seq_or_ts;
3568                        tokio::spawn(async move {
3569                            emit(from_node, caller_origin, call_id, resp).await;
3570                        });
3571                        return Ok(());
3572                    }
3573                }
3574                let cancellation = RpcCancellationToken::new();
3575                self.in_flight.lock().insert(key, cancellation.clone());
3576
3577                // Build per-call request-side mpsc (Phase B
3578                // pattern).
3579                let (req_tx, req_rx) =
3580                    tokio::sync::mpsc::channel::<bytes::Bytes>(STREAMING_REQUEST_PUMP_CAPACITY);
3581                let end_on_initial = payload.flags & FLAG_RPC_REQUEST_END != 0;
3582                let is_pure_terminator = end_on_initial && payload.body.is_empty();
3583                if !is_pure_terminator {
3584                    // Same invariant as the client-streaming fold:
3585                    // fresh bounded mpsc with a live receiver cannot
3586                    // reject the first send.
3587                    if req_tx.try_send(payload.body).is_err() {
3588                        debug_assert!(false, "fresh duplex request mpsc rejected initial body");
3589                        tracing::error!(
3590                            caller_origin = format!("{:#x}", meta.origin_hash),
3591                            call_id = meta.seq_or_ts,
3592                            "rpc duplex server fold: fresh mpsc rejected initial REQUEST body (invariant break)",
3593                        );
3594                    }
3595                }
3596                if !end_on_initial {
3597                    self.senders.lock().insert(key, req_tx);
3598                }
3599                // Hand the handler an auto-granting RequestStream
3600                // when the caller opted into request-direction
3601                // flow control AND the fold was wired with a
3602                // grant emitter.
3603                let grant_emitter = if parse_request_window_initial(&payload.headers).is_some() {
3604                    self.grant_emit.clone()
3605                } else {
3606                    None
3607                };
3608                let request_stream = RequestStream::new(
3609                    req_rx,
3610                    grant_emitter,
3611                    from_node,
3612                    meta.origin_hash,
3613                    meta.seq_or_ts,
3614                );
3615
3616                // Build the per-call response-side mpsc (existing
3617                // server-streaming-response pattern). The handler
3618                // writes chunks to the sink; the pump task drains
3619                // the receiver and publishes RESPONSE events.
3620                let (resp_tx, mut resp_rx) =
3621                    tokio::sync::mpsc::channel::<bytes::Bytes>(STREAMING_PUMP_CAPACITY);
3622                let response_sink = RpcResponseSink {
3623                    inner: resp_tx,
3624                    metrics: self.metrics.clone(),
3625                };
3626
3627                let trace_context = if payload.flags & FLAG_RPC_PROPAGATE_TRACE != 0 {
3628                    extract_trace_context(&payload.headers)
3629                } else {
3630                    None
3631                };
3632                let deadline_ns = payload.deadline_ns;
3633                let ctx = RpcStreamingContext {
3634                    caller_origin: meta.origin_hash,
3635                    call_id: meta.seq_or_ts,
3636                    deadline_ns,
3637                    headers: payload.headers,
3638                    cancellation: cancellation.clone(),
3639                    trace_context,
3640                };
3641                let handler = self.handler.clone();
3642                let emit = self.emit.clone();
3643                let in_flight = self.in_flight.clone();
3644                let senders = self.senders.clone();
3645                let caller_origin = meta.origin_hash;
3646                let call_id = meta.seq_or_ts;
3647                let cancel_probe = cancellation.clone();
3648                let cancel_for_deadline = cancellation.clone();
3649                let metrics = self.metrics.clone();
3650
3651                // Pump: drains resp_rx, emits per-chunk RESPONSE
3652                // events with `nrpc-streaming: continue`.
3653                let pump_emit = emit.clone();
3654                let pump_metrics = metrics.clone();
3655                let pump = tokio::spawn(async move {
3656                    while let Some(chunk) = resp_rx.recv().await {
3657                        if let Some(m) = pump_metrics.as_ref() {
3658                            m.streaming_chunks_emitted_total
3659                                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3660                        }
3661                        let resp = RpcResponsePayload {
3662                            status: RpcStatus::Ok,
3663                            headers: vec![(
3664                                HEADER_NRPC_STREAMING.to_string(),
3665                                HEADER_NRPC_STREAMING_CONTINUE.to_vec(),
3666                            )],
3667                            body: chunk.clone(),
3668                        };
3669                        pump_emit(from_node, caller_origin, call_id, resp).await;
3670                    }
3671                });
3672
3673                tokio::spawn(async move {
3674                    if let Some(m) = metrics.as_ref() {
3675                        m.handler_invocations_total
3676                            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3677                        m.handler_in_flight
3678                            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3679                    }
3680                    let handler_started = std::time::Instant::now();
3681                    // Same deadline guard as the client-streaming
3682                    // fold: force-drop the handler future at
3683                    // deadline_ns so an orphaned request stream
3684                    // can't hang the call. `0` means no deadline.
3685                    let call_fut = futures::FutureExt::catch_unwind(std::panic::AssertUnwindSafe(
3686                        handler.call(ctx, request_stream, response_sink),
3687                    ));
3688                    let outcome = if deadline_ns > 0 {
3689                        let now_ns = std::time::SystemTime::now()
3690                            .duration_since(std::time::UNIX_EPOCH)
3691                            .map(|d| d.as_nanos() as u64)
3692                            .unwrap_or(0);
3693                        let remaining = deadline_ns.saturating_sub(now_ns);
3694                        if remaining == 0 {
3695                            cancel_for_deadline.cancel();
3696                            Ok(Err(RpcHandlerError::Internal(
3697                                "duplex handler deadline_ns already expired at spawn".to_string(),
3698                            )))
3699                        } else {
3700                            match tokio::time::timeout(
3701                                std::time::Duration::from_nanos(remaining),
3702                                call_fut,
3703                            )
3704                            .await
3705                            {
3706                                Ok(o) => o,
3707                                Err(_) => {
3708                                    cancel_for_deadline.cancel();
3709                                    Ok(Err(RpcHandlerError::Internal(
3710                                        "duplex handler deadline_ns exceeded".to_string(),
3711                                    )))
3712                                }
3713                            }
3714                        }
3715                    } else {
3716                        call_fut.await
3717                    };
3718                    // Handler dropped the sink — let the pump
3719                    // drain any final in-flight chunks before we
3720                    // emit the terminal frame.
3721                    let _ = pump.await;
3722                    if let Some(m) = metrics.as_ref() {
3723                        m.handler_in_flight
3724                            .fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
3725                        m.record_handler_duration(handler_started.elapsed());
3726                        if outcome.is_err() {
3727                            m.handler_panics_total
3728                                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3729                        }
3730                    }
3731                    let terminal = if cancel_probe.is_cancelled() {
3732                        RpcResponsePayload {
3733                            status: RpcStatus::Cancelled,
3734                            headers: vec![],
3735                            body: Bytes::from_static(
3736                                b"server observed CANCEL during duplex handler execution",
3737                            ),
3738                        }
3739                    } else {
3740                        match outcome {
3741                            Ok(Ok(())) => RpcResponsePayload {
3742                                status: RpcStatus::Ok,
3743                                headers: vec![(
3744                                    HEADER_NRPC_STREAMING.to_string(),
3745                                    HEADER_NRPC_STREAMING_END.to_vec(),
3746                                )],
3747                                body: Bytes::new(),
3748                            },
3749                            Ok(Err(RpcHandlerError::Application { code, message })) => {
3750                                RpcResponsePayload {
3751                                    status: RpcStatus::Application(code),
3752                                    headers: vec![],
3753                                    body: Bytes::from(message),
3754                                }
3755                            }
3756                            Ok(Err(RpcHandlerError::Internal(message))) => RpcResponsePayload {
3757                                status: RpcStatus::Internal,
3758                                headers: vec![],
3759                                body: Bytes::from(message),
3760                            },
3761                            Err(panic) => {
3762                                let panic_msg = panic
3763                                    .downcast_ref::<&'static str>()
3764                                    .map(|s| s.to_string())
3765                                    .or_else(|| panic.downcast_ref::<String>().cloned())
3766                                    .unwrap_or_else(|| "<non-string panic>".into());
3767                                tracing::error!(
3768                                    caller_origin = format!("{:#x}", caller_origin),
3769                                    call_id,
3770                                    panic = %panic_msg,
3771                                    "rpc duplex server handler panicked",
3772                                );
3773                                RpcResponsePayload {
3774                                    status: RpcStatus::Internal,
3775                                    headers: vec![],
3776                                    body: Bytes::from(format!("handler panicked: {panic_msg}")),
3777                                }
3778                            }
3779                        }
3780                    };
3781                    in_flight.lock().remove(&key);
3782                    senders.lock().remove(&key);
3783                    emit(from_node, caller_origin, call_id, terminal).await;
3784                });
3785            }
3786            DISPATCH_RPC_REQUEST_CHUNK => {
3787                apply_request_chunk_to_senders(
3788                    from_node,
3789                    frame.slice(RPC_FRAME_BODY_OFFSET..),
3790                    &meta,
3791                    &self.senders,
3792                    "duplex",
3793                );
3794            }
3795            DISPATCH_RPC_CANCEL => {
3796                if let Some(token) = self.in_flight.lock().remove(&key) {
3797                    token.cancel();
3798                }
3799                self.senders.lock().remove(&key);
3800            }
3801            _ => {}
3802        }
3803        Ok(())
3804    }
3805}
3806
3807impl RedexFold<()> for RpcDuplexFold {
3808    /// Loopback / test shim: drives `apply_frame` with
3809    /// `from_node = 0` (AV-1 item 1). Production uses
3810    /// [`RpcDuplexFold::apply_inbound`].
3811    fn apply(&mut self, ev: &RedexEvent, _state: &mut ()) -> Result<(), RedexError> {
3812        self.apply_frame(0, &ev.payload)
3813    }
3814}
3815
3816// ============================================================================
3817// Client-side fold.
3818//
3819// `RpcClientFold` is the symmetric companion of `RpcServerFold`.
3820// It sees RESPONSE events on the caller's reply channel
3821// (`<service>.replies.<self_origin>`) and routes each one to the
3822// matching call's awaiting `oneshot::Receiver` keyed on `call_id`
3823// (the `EventMeta::seq_or_ts`).
3824//
3825// The fold's mutable state (the pending-senders map) is shared
3826// with the `Mesh::call` API via a clone of the same Arc — so the
3827// publisher side can `register(call_id)` to stage a receiver
3828// before publishing the REQUEST, and the fold side can `deliver`
3829// when the matching RESPONSE arrives.
3830// ============================================================================
3831
3832/// One pending entry — unary oneshot, server-streaming mpsc, or
3833/// client-streaming (one terminal oneshot + a separate grant
3834/// mpsc). The fold dispatches to the right variant based on
3835/// what's registered for the `call_id`.
3836enum PendingEntry {
3837    /// Unary call — exactly one RESPONSE expected. Completes the
3838    /// oneshot with the decoded payload.
3839    Unary(tokio::sync::oneshot::Sender<RpcResponsePayload>),
3840    /// Server-streaming call — multiple non-terminal `Continue`
3841    /// chunks followed by one terminal frame. Each non-terminal
3842    /// chunk pushes a `StreamItem::Chunk(body)` onto the mpsc;
3843    /// the terminal frame pushes `StreamItem::End` (Ok) or
3844    /// `StreamItem::Error(payload)` (non-Ok status) and the
3845    /// pending entry is removed.
3846    Streaming(tokio::sync::mpsc::UnboundedSender<StreamItem>),
3847    /// Client-streaming or duplex call. Two sender halves:
3848    ///
3849    /// - `terminal_tx`: oneshot that completes when the server's
3850    ///   single terminal RESPONSE arrives. Response shape and
3851    ///   delivery semantics are identical to the unary variant —
3852    ///   the caller awaits one payload, success or failure status.
3853    /// - `grant_tx`: mpsc that ferries REQUEST_GRANT credit values
3854    ///   from the client fold to the caller's send sink. Each
3855    ///   `DISPATCH_RPC_REQUEST_GRANT` event for this call_id
3856    ///   pushes one `u32` credit onto the mpsc; the caller's send
3857    ///   sink consumes credits to gate `send(...).await`.
3858    ///
3859    /// Bidi streaming plan (Phase C). Used for pure client-
3860    /// streaming (one terminal RESPONSE closes the call). Duplex
3861    /// calls use the [`PendingEntry::Duplex`] variant instead,
3862    /// since they receive many response chunks rather than one
3863    /// terminal payload.
3864    ClientStreaming {
3865        terminal_tx: tokio::sync::oneshot::Sender<RpcResponsePayload>,
3866        grant_tx: tokio::sync::mpsc::UnboundedSender<u32>,
3867    },
3868    /// Duplex call — many request chunks out, many response
3869    /// chunks in. Two senders, same shape as `ClientStreaming`
3870    /// except the terminal slot is an mpsc instead of a oneshot
3871    /// because the response side is multi-chunk (terminator is
3872    /// implicit in `StreamItem::End` / `StreamItem::Error` on
3873    /// the chunks_tx mpsc, same as `PendingEntry::Streaming`).
3874    ///
3875    /// - `chunks_tx`: response-chunk mpsc — fed by `deliver`
3876    ///   when RESPONSE events arrive on the reply channel.
3877    ///   `StreamItem::Chunk` for non-terminal, `StreamItem::End`
3878    ///   / `StreamItem::Error` terminates and removes the entry.
3879    /// - `grant_tx`: request-direction credit mpsc — fed by
3880    ///   `deliver_grant` when REQUEST_GRANT events arrive.
3881    ///
3882    /// Bidi streaming plan (Phase D).
3883    Duplex {
3884        chunks_tx: tokio::sync::mpsc::UnboundedSender<StreamItem>,
3885        grant_tx: tokio::sync::mpsc::UnboundedSender<u32>,
3886    },
3887}
3888
3889/// One item delivered to a streaming caller. The caller's
3890/// `RpcStream` translates these into `Stream::Item =
3891/// Result<Bytes, RpcError>` plus stream termination.
3892#[derive(Debug, Clone)]
3893pub enum StreamItem {
3894    /// Non-terminal chunk — a body slice from the server.
3895    Chunk(bytes::Bytes),
3896    /// Terminal frame, server signaled clean stream end.
3897    End,
3898    /// Terminal frame with a non-`Ok` status. Body is the
3899    /// server's diagnostic; status is the wire `RpcStatus` value.
3900    Error(RpcResponsePayload),
3901}
3902
3903/// Shared pending-call state. Held by both the `RpcClientFold`
3904/// (writer side: completes oneshot senders / pushes streaming
3905/// chunks on RESPONSE arrival) and the `Mesh::call*` APIs (reader
3906/// side: registers entries before publishing the REQUEST).
3907/// Concurrent access is mediated by `DashMap`.
3908///
3909/// Multiplexes unary AND streaming calls in a single map keyed
3910/// on `call_id` — the entry's enum variant tells the fold how
3911/// to dispatch incoming RESPONSE events.
3912pub struct RpcClientPending {
3913    /// Map keyed on `call_id`, value carries `(expected_target,
3914    /// PendingEntry)`. `expected_target` is the `NodeId` of the
3915    /// peer the request was dispatched to; `deliver` rejects
3916    /// frames whose wire `from_node` doesn't match. A
3917    /// `expected_target == 0` entry opts out of the binding
3918    /// (loopback tests + paths with no session).
3919    senders: dashmap::DashMap<u64, (super::super::behavior::placement::NodeId, PendingEntry)>,
3920}
3921
3922impl RpcClientPending {
3923    /// Construct an empty pending-call store.
3924    pub fn new() -> Self {
3925        Self {
3926            senders: dashmap::DashMap::new(),
3927        }
3928    }
3929
3930    /// Register a oneshot for a unary `call_id`. Returns the
3931    /// receiver the caller awaits. The caller MUST publish the
3932    /// REQUEST after registration (and not before) so the
3933    /// matching RESPONSE can't arrive while the pending entry is
3934    /// missing.
3935    ///
3936    /// `target_node` is the wire-session peer the request will
3937    /// be sent to; `deliver` rejects RESPONSE frames whose
3938    /// `from_node` doesn't match. Pass `0` for loopback / no-
3939    /// session test paths to opt out of the binding gate.
3940    ///
3941    /// If a sender already exists for `call_id` (improperly reused
3942    /// id), it is replaced and the old receiver gets a
3943    /// `RecvError::Closed` — surfacing the misuse as a hard error
3944    /// at the caller rather than silently delivering the response
3945    /// to the wrong waiter.
3946    pub fn register(
3947        &self,
3948        call_id: u64,
3949        target_node: super::super::behavior::placement::NodeId,
3950    ) -> tokio::sync::oneshot::Receiver<RpcResponsePayload> {
3951        let (tx, rx) = tokio::sync::oneshot::channel();
3952        self.senders
3953            .insert(call_id, (target_node, PendingEntry::Unary(tx)));
3954        rx
3955    }
3956
3957    /// Register a streaming entry for `call_id`. Returns the
3958    /// receive end of an mpsc the fold will push chunks onto.
3959    /// Same registration ordering rules as `register` —
3960    /// publisher must call this BEFORE publishing the REQUEST.
3961    pub fn register_streaming(
3962        &self,
3963        call_id: u64,
3964        target_node: super::super::behavior::placement::NodeId,
3965    ) -> tokio::sync::mpsc::UnboundedReceiver<StreamItem> {
3966        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
3967        self.senders
3968            .insert(call_id, (target_node, PendingEntry::Streaming(tx)));
3969        rx
3970    }
3971
3972    /// Register a client-streaming (or duplex) entry for
3973    /// `call_id`. Returns BOTH the terminal-response receiver
3974    /// (the caller awaits on this for the single terminal
3975    /// RESPONSE that ends the call) AND a grant receiver (the
3976    /// caller's send sink consumes this to gate `send().await`
3977    /// when the caller opted into request-direction flow
3978    /// control).
3979    ///
3980    /// Same registration ordering rules as `register` /
3981    /// `register_streaming` — publisher must call this BEFORE
3982    /// publishing the REQUEST so a fast server's RESPONSE /
3983    /// REQUEST_GRANT can't arrive while no pending entry exists.
3984    ///
3985    /// Bidi streaming plan (Phase C).
3986    pub fn register_client_streaming(
3987        &self,
3988        call_id: u64,
3989        target_node: super::super::behavior::placement::NodeId,
3990    ) -> (
3991        tokio::sync::oneshot::Receiver<RpcResponsePayload>,
3992        tokio::sync::mpsc::UnboundedReceiver<u32>,
3993    ) {
3994        let (terminal_tx, terminal_rx) = tokio::sync::oneshot::channel();
3995        let (grant_tx, grant_rx) = tokio::sync::mpsc::unbounded_channel();
3996        self.senders.insert(
3997            call_id,
3998            (
3999                target_node,
4000                PendingEntry::ClientStreaming {
4001                    terminal_tx,
4002                    grant_tx,
4003                },
4004            ),
4005        );
4006        (terminal_rx, grant_rx)
4007    }
4008
4009    /// Register a duplex entry for `call_id`. Returns BOTH a
4010    /// response-chunk receiver (yields `StreamItem` per inbound
4011    /// RESPONSE chunk; terminator is `End` / `Error`) AND a
4012    /// grant receiver (yields `u32` credits per inbound
4013    /// REQUEST_GRANT).
4014    ///
4015    /// Same registration ordering rules as the other `register_*`
4016    /// methods: publisher must call this BEFORE publishing the
4017    /// REQUEST so the server's response chunks / grants can't
4018    /// arrive while no pending entry exists.
4019    ///
4020    /// Bidi streaming plan (Phase D).
4021    pub fn register_duplex(
4022        &self,
4023        call_id: u64,
4024        target_node: super::super::behavior::placement::NodeId,
4025    ) -> (
4026        tokio::sync::mpsc::UnboundedReceiver<StreamItem>,
4027        tokio::sync::mpsc::UnboundedReceiver<u32>,
4028    ) {
4029        let (chunks_tx, chunks_rx) = tokio::sync::mpsc::unbounded_channel();
4030        let (grant_tx, grant_rx) = tokio::sync::mpsc::unbounded_channel();
4031        self.senders.insert(
4032            call_id,
4033            (
4034                target_node,
4035                PendingEntry::Duplex {
4036                    chunks_tx,
4037                    grant_tx,
4038                },
4039            ),
4040        );
4041        (chunks_rx, grant_rx)
4042    }
4043
4044    /// Drop the pending entry for `call_id`. Called by the
4045    /// caller-side cancellation path (e.g. `Mesh::call`'s future
4046    /// being dropped, the stream being dropped, or a deadline
4047    /// timer firing). The matching RESPONSE(s) that may still
4048    /// arrive afterwards are silently discarded by `deliver`.
4049    pub fn cancel(&self, call_id: u64) {
4050        self.senders.remove(&call_id);
4051    }
4052
4053    /// Deliver `resp` to the waiter for `call_id`, if any.
4054    ///
4055    /// `from_node` is the wire-session peer of the inbound
4056    /// RESPONSE. If the pending entry's recorded `target_node`
4057    /// is non-zero and does not match `from_node`, the frame is
4058    /// dropped with a trace log and the pending entry stays
4059    /// intact — a forged response on a shared reply channel
4060    /// can't resolve a victim's call. A recorded `target_node
4061    /// == 0` opts the call out of the binding (loopback paths).
4062    ///
4063    /// For a unary entry: completes the oneshot and removes the
4064    /// entry.
4065    ///
4066    /// For a streaming entry: examines the response's headers to
4067    /// decide whether it's a non-terminal chunk (`Continue` —
4068    /// push `StreamItem::Chunk`, keep the entry) or terminal
4069    /// (`End` / non-`Ok` — push `StreamItem::End` or `Error`,
4070    /// remove the entry).
4071    ///
4072    /// Idempotent on subsequent deliveries to a removed entry.
4073    fn deliver(
4074        &self,
4075        call_id: u64,
4076        from_node: super::super::behavior::placement::NodeId,
4077        resp: RpcResponsePayload,
4078    ) {
4079        // Look up the entry — but DON'T remove it yet, because for
4080        // streaming we may want to keep it for non-terminal chunks.
4081        // The remove decision is per-variant.
4082        let entry = self.senders.get(&call_id);
4083        let Some(entry) = entry else { return };
4084        // S-4 part 2 gate. The pending registry binds each call
4085        // to the AEAD-verified `target_node` the request was
4086        // dispatched to; any other session peer publishing on the
4087        // shared reply channel with a guessed call_id is dropped
4088        // here without touching the waiter. `0` opts out — used
4089        // by loopback paths that have no session peer.
4090        let (target_node, _entry_value) = entry.value();
4091        if *target_node != 0 && *target_node != from_node {
4092            tracing::trace!(
4093                call_id,
4094                from_node,
4095                expected = *target_node,
4096                "rpc client: dropping RESPONSE from non-target session peer"
4097            );
4098            return;
4099        }
4100        match entry.value() {
4101            (_, PendingEntry::Unary(_)) => {
4102                drop(entry);
4103                if let Some((_, (_, PendingEntry::Unary(tx)))) = self.senders.remove(&call_id) {
4104                    let _ = tx.send(resp);
4105                }
4106            }
4107            (_, PendingEntry::ClientStreaming { .. }) => {
4108                // Terminal RESPONSE for a client-streaming /
4109                // duplex call. Same delivery shape as Unary —
4110                // complete the oneshot, remove the entry. The
4111                // grant_tx half drops with the entry, which is
4112                // fine (no more grants will arrive after the
4113                // terminal frame).
4114                drop(entry);
4115                if let Some((
4116                    _,
4117                    (
4118                        _,
4119                        PendingEntry::ClientStreaming {
4120                            terminal_tx,
4121                            grant_tx: _,
4122                        },
4123                    ),
4124                )) = self.senders.remove(&call_id)
4125                {
4126                    let _ = terminal_tx.send(resp);
4127                }
4128            }
4129            (_, PendingEntry::Streaming(tx)) => {
4130                let tx = tx.clone();
4131                drop(entry);
4132                self.dispatch_streaming_chunk(&tx, resp, call_id);
4133            }
4134            (_, PendingEntry::Duplex { chunks_tx, .. }) => {
4135                // Same dispatch logic as Streaming — duplex
4136                // response side IS a multi-chunk stream.
4137                let tx = chunks_tx.clone();
4138                drop(entry);
4139                self.dispatch_streaming_chunk(&tx, resp, call_id);
4140            }
4141        }
4142    }
4143
4144    /// Shared response-chunk dispatch used by both
4145    /// `PendingEntry::Streaming` and `PendingEntry::Duplex`. The
4146    /// caller has already verified the target-binding gate and
4147    /// dropped its `entry` ref; this helper does the classify-
4148    /// and-push and removes the entry from the senders map on
4149    /// terminal frames.
4150    fn dispatch_streaming_chunk(
4151        &self,
4152        tx: &tokio::sync::mpsc::UnboundedSender<StreamItem>,
4153        resp: RpcResponsePayload,
4154        call_id: u64,
4155    ) {
4156        let kind = classify_streaming_chunk(&resp);
4157        match kind {
4158            StreamingChunkKind::Continue => {
4159                let _ = tx.send(StreamItem::Chunk(resp.body));
4160            }
4161            StreamingChunkKind::Terminal => {
4162                let item = if resp.status.is_ok() {
4163                    if !resp.body.is_empty() {
4164                        let _ = tx.send(StreamItem::Chunk(resp.body));
4165                    }
4166                    StreamItem::End
4167                } else {
4168                    StreamItem::Error(resp)
4169                };
4170                let _ = tx.send(item);
4171                self.senders.remove(&call_id);
4172            }
4173            StreamingChunkKind::Unary => {
4174                tracing::warn!(
4175                    call_id,
4176                    body_len = resp.body.len(),
4177                    "rpc client: streaming / duplex consumer received unary-shaped \
4178                     response (no nrpc-streaming header); server may have bridged a \
4179                     unary path. Bridging to single-chunk + EOF.",
4180                );
4181                if !resp.body.is_empty() {
4182                    let _ = tx.send(StreamItem::Chunk(resp.body));
4183                }
4184                let _ = tx.send(StreamItem::End);
4185                self.senders.remove(&call_id);
4186            }
4187        }
4188    }
4189
4190    /// Deliver a request-direction grant credit to the waiter
4191    /// for `call_id`, if it's a client-streaming / duplex entry.
4192    /// Silently no-op for unknown call_ids, for unary entries
4193    /// (caller bug — grant for a unary call makes no sense),
4194    /// and for server-streaming entries (grants apply only to
4195    /// the upload direction).
4196    ///
4197    /// `from_node` is gated by the same target-binding check
4198    /// as `deliver`: a grant from a non-target session peer is
4199    /// dropped (a forged grant on a shared reply channel can't
4200    /// inject credit into a victim's call).
4201    ///
4202    /// Bidi streaming plan (Phase C).
4203    fn deliver_grant(
4204        &self,
4205        call_id: u64,
4206        from_node: super::super::behavior::placement::NodeId,
4207        credits: u32,
4208    ) {
4209        let entry = self.senders.get(&call_id);
4210        let Some(entry) = entry else { return };
4211        let (target_node, _entry_value) = entry.value();
4212        if *target_node != 0 && *target_node != from_node {
4213            tracing::trace!(
4214                call_id,
4215                from_node,
4216                expected = *target_node,
4217                "rpc client: dropping REQUEST_GRANT from non-target session peer"
4218            );
4219            return;
4220        }
4221        match entry.value() {
4222            (_, PendingEntry::ClientStreaming { grant_tx, .. })
4223            | (_, PendingEntry::Duplex { grant_tx, .. }) => {
4224                let _ = grant_tx.send(credits);
4225            }
4226            // Unary / Streaming entries silently ignore — see
4227            // method docs for the rationale.
4228            _ => {}
4229        }
4230    }
4231
4232    /// Test-only: how many pending calls are registered. Used by
4233    /// integration tests to confirm cleanup after happy-path / cancel.
4234    #[cfg(test)]
4235    pub fn pending_count(&self) -> usize {
4236        self.senders.len()
4237    }
4238}
4239
4240impl Default for RpcClientPending {
4241    fn default() -> Self {
4242        Self::new()
4243    }
4244}
4245
4246/// Client-side fold. Decodes RESPONSE events and routes them to
4247/// awaiting oneshots in the shared [`RpcClientPending`].
4248///
4249/// `Mesh::call` clones the same `Arc<RpcClientPending>` to register
4250/// oneshots before publishing REQUESTs.
4251pub struct RpcClientFold {
4252    pending: Arc<RpcClientPending>,
4253}
4254
4255impl RpcClientFold {
4256    /// Construct a client fold that delivers responses through
4257    /// `pending`. Typical pattern:
4258    ///
4259    /// ```ignore
4260    /// let pending = Arc::new(RpcClientPending::new());
4261    /// let fold = RpcClientFold::new(pending.clone());
4262    /// let adapter = CortexAdapter::open(..., fold, ())?;
4263    /// // `pending` is still usable for register / cancel.
4264    /// ```
4265    pub fn new(pending: Arc<RpcClientPending>) -> Self {
4266        Self { pending }
4267    }
4268
4269    /// Production-path entry point. Mesh dispatch calls this with
4270    /// the AEAD-verified session peer's `NodeId` in
4271    /// `ev.from_node`; the pending registry's S-4 binding gate
4272    /// uses it to reject responses from the wrong target.
4273    pub fn apply_inbound(&mut self, ev: &RpcInboundEvent) {
4274        let Some(meta) = (if ev.payload.len() >= EVENT_META_SIZE {
4275            EventMeta::from_bytes(&ev.payload[..EVENT_META_SIZE])
4276        } else {
4277            None
4278        }) else {
4279            tracing::warn!(
4280                payload_len = ev.payload.len(),
4281                "rpc client fold: event payload too short for EventMeta; skipping",
4282            );
4283            return;
4284        };
4285        match meta.dispatch {
4286            DISPATCH_RPC_RESPONSE => {
4287                match RpcResponsePayload::decode(ev.payload.slice(RPC_FRAME_BODY_OFFSET..)) {
4288                    Ok(resp) => self.pending.deliver(meta.seq_or_ts, ev.from_node, resp),
4289                    Err(e) => {
4290                        tracing::warn!(
4291                            error = %e,
4292                            call_id = meta.seq_or_ts,
4293                            "rpc client fold: malformed response payload",
4294                        );
4295                    }
4296                }
4297            }
4298            DISPATCH_RPC_REQUEST_GRANT => {
4299                // Server granted upload credit for a
4300                // client-streaming / duplex call. Route it to the
4301                // matching pending entry's grant mpsc; non-client-
4302                // streaming entries silently ignore (see
4303                // RpcClientPending::deliver_grant docs).
4304                match decode_request_grant(&ev.payload[RPC_FRAME_BODY_OFFSET..]) {
4305                    Some(grant) => {
4306                        // The payload's `call_id` MUST agree with
4307                        // the EventMeta's `seq_or_ts`: producer
4308                        // encodes both to the same value (see
4309                        // `RpcRequestGrantPayload::call_id` docs).
4310                        // If they disagree, the frame is malformed
4311                        // or forged — drop it. Otherwise a peer
4312                        // could publish a GRANT whose meta names
4313                        // one call but whose payload credits a
4314                        // different in-flight call_id.
4315                        if grant.call_id != meta.seq_or_ts {
4316                            tracing::debug!(
4317                                meta_call_id = meta.seq_or_ts,
4318                                payload_call_id = grant.call_id,
4319                                "rpc client fold: REQUEST_GRANT meta/payload call_id mismatch; dropping",
4320                            );
4321                            return;
4322                        }
4323                        if grant.credits == 0 {
4324                            return;
4325                        }
4326                        self.pending
4327                            .deliver_grant(grant.call_id, ev.from_node, grant.credits);
4328                    }
4329                    None => {
4330                        tracing::debug!(
4331                            call_id = meta.seq_or_ts,
4332                            "rpc client fold: malformed REQUEST_GRANT payload"
4333                        );
4334                    }
4335                }
4336            }
4337            _ => {
4338                // Unknown / unexpected dispatch on the reply
4339                // channel — ignore (a misconfigured publisher
4340                // shouldn't take down the fold).
4341            }
4342        }
4343    }
4344}
4345
4346impl RedexFold<()> for RpcClientFold {
4347    /// Legacy entry point used by loopback / test paths that
4348    /// don't have a session peer to resolve. Calls `deliver`
4349    /// with `from_node = 0`, which the pending registry treats
4350    /// as "no binding" — callers that registered with
4351    /// `target_node = 0` accept it, callers that registered
4352    /// with a real target reject it.
4353    fn apply(&mut self, ev: &RedexEvent, _state: &mut ()) -> Result<(), RedexError> {
4354        let Some(meta) = (if ev.payload.len() >= EVENT_META_SIZE {
4355            EventMeta::from_bytes(&ev.payload[..EVENT_META_SIZE])
4356        } else {
4357            None
4358        }) else {
4359            tracing::warn!(
4360                payload_len = ev.payload.len(),
4361                "rpc client fold: event payload too short for EventMeta; skipping",
4362            );
4363            return Ok(());
4364        };
4365        // Route RESPONSE and REQUEST_GRANT events; ignore other
4366        // dispatches a misconfigured publisher might send. The
4367        // loopback path uses `from_node = 0` which the pending
4368        // registry treats as "no binding" — see the apply_inbound
4369        // production-path counterpart above for the AEAD-verified
4370        // peer routing.
4371        match meta.dispatch {
4372            DISPATCH_RPC_RESPONSE => {
4373                match RpcResponsePayload::decode(ev.payload.slice(RPC_FRAME_BODY_OFFSET..)) {
4374                    Ok(resp) => self.pending.deliver(meta.seq_or_ts, 0, resp),
4375                    Err(e) => {
4376                        // Malformed RESPONSE on the reply channel.
4377                        // We can't fabricate a synthetic response
4378                        // (the call_id might be valid; we just
4379                        // can't tell what it was supposed to
4380                        // mean). Log and leave the pending entry
4381                        // intact — the caller's deadline /
4382                        // cancellation path will eventually clean
4383                        // it up.
4384                        tracing::warn!(
4385                            error = %e,
4386                            call_id = meta.seq_or_ts,
4387                            "rpc client fold: malformed response payload",
4388                        );
4389                    }
4390                }
4391            }
4392            DISPATCH_RPC_REQUEST_GRANT => {
4393                match decode_request_grant(&ev.payload[RPC_FRAME_BODY_OFFSET..]) {
4394                    Some(grant) => {
4395                        // See `apply_inbound` REQUEST_GRANT arm for
4396                        // the meta/payload call_id invariant.
4397                        if grant.call_id != meta.seq_or_ts {
4398                            tracing::debug!(
4399                                meta_call_id = meta.seq_or_ts,
4400                                payload_call_id = grant.call_id,
4401                                "rpc client fold: REQUEST_GRANT meta/payload call_id mismatch; dropping",
4402                            );
4403                            return Ok(());
4404                        }
4405                        if grant.credits == 0 {
4406                            return Ok(());
4407                        }
4408                        self.pending.deliver_grant(grant.call_id, 0, grant.credits);
4409                    }
4410                    None => {
4411                        tracing::debug!(
4412                            call_id = meta.seq_or_ts,
4413                            "rpc client fold: malformed REQUEST_GRANT payload"
4414                        );
4415                    }
4416                }
4417            }
4418            _ => {}
4419        }
4420        Ok(())
4421    }
4422}
4423
4424#[cfg(test)]
4425mod tests {
4426    use super::*;
4427
4428    fn header(name: &str, value: &[u8]) -> RpcHeader {
4429        (name.to_string(), value.to_vec())
4430    }
4431
4432    // --------------------------------------------------------------------
4433    // OA2-E0.2 P0 — `peek_request_service` boundary contract.
4434    // --------------------------------------------------------------------
4435
4436    /// Build a REQUEST frame `EventMeta ‖ RpcRouteV1(0) ‖
4437    /// RpcRequestPayload{service, ..}` for the peek unit tests.
4438    fn request_frame_for_peek(service: &str) -> Vec<u8> {
4439        let meta = EventMeta::new(DISPATCH_RPC_REQUEST, 0, 0, 1, 0);
4440        let req = RpcRequestPayload {
4441            service: service.to_string(),
4442            deadline_ns: 0,
4443            flags: 0,
4444            headers: vec![],
4445            body: Bytes::from_static(b"body"),
4446        };
4447        let mut buf = Vec::new();
4448        buf.extend_from_slice(&meta.to_bytes());
4449        encode_rpc_route(&mut buf, 0);
4450        req.encode_into(&mut buf);
4451        buf
4452    }
4453
4454    /// The peek reads back exactly the `service` the full encoder
4455    /// wrote — the invariant the serve-bridge equality check relies
4456    /// on (peek and full decode agree on the service).
4457    #[test]
4458    fn peek_request_service_matches_full_decode() {
4459        for name in ["admin", "echo.v1", "x"] {
4460            let frame = request_frame_for_peek(name);
4461            assert_eq!(peek_request_service(&frame), Some(name));
4462            // And it agrees with the authoritative decoder.
4463            let decoded =
4464                RpcRequestPayload::decode(Bytes::from(frame[RPC_FRAME_BODY_OFFSET..].to_vec()))
4465                    .expect("decode");
4466            assert_eq!(decoded.service, name);
4467        }
4468    }
4469
4470    /// Unreadable service fields return `None`, mirroring the `Err`
4471    /// arms of `RpcRequestPayload::decode` — the bridge then lets the
4472    /// fold's full decode reject the frame (`UnknownVersion`) rather
4473    /// than silently dropping it.
4474    #[test]
4475    fn peek_request_service_none_on_malformed() {
4476        // Frame with no room for the route/body at all.
4477        let short = EventMeta::new(DISPATCH_RPC_REQUEST, 0, 0, 1, 0).to_bytes();
4478        assert_eq!(peek_request_service(&short), None);
4479
4480        // Route present but zero-length service (decode rejects empty).
4481        let meta = EventMeta::new(DISPATCH_RPC_REQUEST, 0, 0, 1, 0);
4482        let mut buf = Vec::new();
4483        buf.extend_from_slice(&meta.to_bytes());
4484        encode_rpc_route(&mut buf, 0);
4485        buf.push(0u8); // svc_len = 0
4486        assert_eq!(peek_request_service(&buf), None);
4487
4488        // Length byte claims more bytes than remain.
4489        let mut buf = Vec::new();
4490        buf.extend_from_slice(&meta.to_bytes());
4491        encode_rpc_route(&mut buf, 0);
4492        buf.push(5u8); // svc_len = 5 but no bytes follow
4493        assert_eq!(peek_request_service(&buf), None);
4494    }
4495
4496    // --------------------------------------------------------------------
4497    // Status code numbering.
4498    // --------------------------------------------------------------------
4499
4500    /// Status codes have stable wire numbers. A regression that
4501    /// renumbered any of the canonical statuses would break
4502    /// every cross-version caller / server pair on the wire — pin
4503    /// the numbers explicitly so the test catches it before the
4504    /// bug ships.
4505    #[test]
4506    fn status_wire_numbers_are_stable() {
4507        for (status, expected) in [
4508            (RpcStatus::Ok, 0x0000u16),
4509            (RpcStatus::NotFound, 0x0001),
4510            (RpcStatus::Unauthorized, 0x0002),
4511            (RpcStatus::Timeout, 0x0003),
4512            (RpcStatus::Backpressure, 0x0004),
4513            (RpcStatus::Cancelled, 0x0005),
4514            (RpcStatus::Internal, 0x0006),
4515            (RpcStatus::UnknownVersion, 0x0007),
4516            (RpcStatus::CapabilityDenied, 0x0008),
4517            (RpcStatus::AdmissionDenied, 0x0009),
4518        ] {
4519            assert_eq!(status.to_wire(), expected, "{status:?}");
4520            assert_eq!(RpcStatus::from_wire(expected), status);
4521        }
4522    }
4523
4524    /// Reserved numeric range (`0x000A..=0x7FFF`) decodes as
4525    /// `Application(v)` for forward-compat with future canonical
4526    /// assignments. A future status numbered `0x000A` would round-
4527    /// trip via `from_wire(0x000A)` until that variant is added,
4528    /// at which point the variant takes precedence.
4529    #[test]
4530    fn reserved_status_range_decodes_as_application_for_forward_compat() {
4531        let decoded = RpcStatus::from_wire(0x000A);
4532        assert_eq!(decoded, RpcStatus::Application(0x000A));
4533        assert_eq!(decoded.to_wire(), 0x000A);
4534    }
4535
4536    /// Application range (`0x8000..=0xFFFF`) encodes / decodes
4537    /// transparently as `Application(v)`.
4538    #[test]
4539    fn application_status_range_roundtrips() {
4540        for v in [0x8000u16, 0x8001, 0xCAFE, 0xFFFF] {
4541            let s = RpcStatus::from_wire(v);
4542            assert_eq!(s, RpcStatus::Application(v));
4543            assert_eq!(s.to_wire(), v);
4544        }
4545    }
4546
4547    // --------------------------------------------------------------------
4548    // Dispatch byte assignments.
4549    // --------------------------------------------------------------------
4550
4551    /// Pin the `dispatch` byte assignments so a renumber surfaces
4552    /// here before it ships on the wire. These also live in the
4553    /// design doc; this test is the source-of-truth check.
4554    #[test]
4555    fn dispatch_byte_assignments_are_stable() {
4556        assert_eq!(DISPATCH_RPC_REQUEST, 0x10);
4557        assert_eq!(DISPATCH_RPC_RESPONSE, 0x11);
4558        assert_eq!(DISPATCH_RPC_CANCEL, 0x12);
4559        assert_eq!(DISPATCH_RPC_DEADLINE_EXCEEDED, 0x13);
4560        assert_eq!(DISPATCH_RPC_STREAM_GRANT, 0x14);
4561        assert_eq!(DISPATCH_RPC_REQUEST_CHUNK, 0x15);
4562        assert_eq!(DISPATCH_RPC_REQUEST_GRANT, 0x16);
4563    }
4564
4565    /// Regression: encoder bounds. Encoding a service name longer
4566    /// than `MAX_RPC_SERVICE_NAME_LEN` panics in debug, catching
4567    /// the programmer error in tests rather than silently writing
4568    /// a truncated `as u8` length that the receiver decodes as
4569    /// garbage. The matching debug_asserts guard body length,
4570    /// header count, header name length, and header value length.
4571    #[cfg(debug_assertions)]
4572    #[test]
4573    #[should_panic(expected = "service name")]
4574    fn request_encode_panics_on_oversize_service_name() {
4575        let p = RpcRequestPayload {
4576            service: "x".repeat(MAX_RPC_SERVICE_NAME_LEN + 1),
4577            deadline_ns: 0,
4578            flags: 0,
4579            headers: vec![],
4580            body: Bytes::new(),
4581        };
4582        let _ = p.encode();
4583    }
4584
4585    #[cfg(debug_assertions)]
4586    #[test]
4587    #[should_panic(expected = "body length")]
4588    fn request_encode_panics_on_oversize_body() {
4589        let p = RpcRequestPayload {
4590            service: "x".to_string(),
4591            deadline_ns: 0,
4592            flags: 0,
4593            headers: vec![],
4594            body: Bytes::from(vec![0; MAX_RPC_BODY_LEN + 1]),
4595        };
4596        let _ = p.encode();
4597    }
4598
4599    #[cfg(debug_assertions)]
4600    #[test]
4601    #[should_panic(expected = "header name")]
4602    fn request_encode_panics_on_oversize_header_name() {
4603        let p = RpcRequestPayload {
4604            service: "x".to_string(),
4605            deadline_ns: 0,
4606            flags: 0,
4607            headers: vec![("a".repeat(MAX_RPC_HEADER_NAME_LEN + 1), vec![])],
4608            body: Bytes::new(),
4609        };
4610        let _ = p.encode();
4611    }
4612
4613    /// `encoded_len()` must agree with `encode().len()` for every
4614    /// payload shape — pin this so a future codec change can't
4615    /// silently desynchronize the size-budgeting helper from the
4616    /// actual wire size.
4617    #[test]
4618    fn encoded_len_matches_encode_len_for_request_and_response() {
4619        let req = RpcRequestPayload {
4620            service: "echo.v1".to_string(),
4621            deadline_ns: 1_700_000_000_000_000_000,
4622            flags: FLAG_RPC_PROPAGATE_TRACE,
4623            headers: vec![
4624                header("traceparent", b"00-aabb"),
4625                header("idempotency-key", &7u64.to_le_bytes()),
4626            ],
4627            body: Bytes::from_static(b"{\"hello\":\"world\"}"),
4628        };
4629        assert_eq!(req.encoded_len(), req.encode().len());
4630
4631        let resp = RpcResponsePayload {
4632            status: RpcStatus::Application(0x8001),
4633            headers: vec![header("content-type", b"application/json")],
4634            body: Bytes::from_static(b"ok"),
4635        };
4636        assert_eq!(resp.encoded_len(), resp.encode().len());
4637
4638        // Empty edge cases.
4639        let empty_req = RpcRequestPayload {
4640            service: "x".to_string(),
4641            deadline_ns: 0,
4642            flags: 0,
4643            headers: vec![],
4644            body: Bytes::new(),
4645        };
4646        assert_eq!(empty_req.encoded_len(), empty_req.encode().len());
4647        let empty_resp = RpcResponsePayload {
4648            status: RpcStatus::Ok,
4649            headers: vec![],
4650            body: Bytes::new(),
4651        };
4652        assert_eq!(empty_resp.encoded_len(), empty_resp.encode().len());
4653    }
4654
4655    /// Bit 0 of `RpcRequestPayload::flags` is reserved (was the
4656    /// removed `FLAG_RPC_IDEMPOTENT`). Pin: live flag constants
4657    /// must NOT collide with bit 0, so a future re-add can safely
4658    /// reuse it without breaking existing senders.
4659    #[test]
4660    fn flag_bit_assignments_leave_idempotent_slot_reserved() {
4661        // Bit 0 (1 << 0) and bit 3 (1 << 3) are reserved; live flags
4662        // occupy other bits. Pinning the exact assignments here so
4663        // a renumber that collides with bit 0 (future `IDEMPOTENT`
4664        // re-add) or bit 3 (held in reserve for a future protocol
4665        // flag) surfaces in the test suite before it ships.
4666        assert_eq!(FLAG_RPC_STREAMING_RESPONSE, 1 << 1);
4667        assert_eq!(FLAG_RPC_PROPAGATE_TRACE, 1 << 2);
4668        assert_eq!(FLAG_RPC_CLIENT_STREAMING_REQUEST, 1 << 4);
4669        assert_eq!(FLAG_RPC_REQUEST_END, 1 << 5);
4670        for flag in [
4671            FLAG_RPC_STREAMING_RESPONSE,
4672            FLAG_RPC_PROPAGATE_TRACE,
4673            FLAG_RPC_CLIENT_STREAMING_REQUEST,
4674            FLAG_RPC_REQUEST_END,
4675        ] {
4676            assert_eq!(
4677                flag & (1 << 0),
4678                0,
4679                "flag {flag:#06x} collides with reserved bit 0"
4680            );
4681            assert_eq!(
4682                flag & (1 << 3),
4683                0,
4684                "flag {flag:#06x} collides with reserved bit 3"
4685            );
4686        }
4687    }
4688
4689    // --------------------------------------------------------------------
4690    // Bidi streaming (Phase A) — RpcRequestChunkPayload and
4691    // RpcRequestGrantPayload wire-stability tests.
4692    // --------------------------------------------------------------------
4693
4694    /// 1/5 — RequestChunk round-trip with realistic header set and
4695    /// 1 KiB body. Pins the encode/decode loop on the full shape.
4696    #[test]
4697    fn request_chunk_roundtrip_with_headers_and_body() {
4698        let mut headers = Vec::new();
4699        for i in 0..10u8 {
4700            headers.push(header(&format!("x-chunk-meta-{i}"), &[0xAA, 0xBB, i, !i]));
4701        }
4702        let body: Vec<u8> = (0..1024u32).map(|n| (n & 0xFF) as u8).collect();
4703        let p = RpcRequestChunkPayload {
4704            call_id: 0xCAFE_F00D_DEAD_BEEF,
4705            flags: FLAG_RPC_REQUEST_END | FLAG_RPC_PROPAGATE_TRACE,
4706            headers,
4707            body: Bytes::from(body),
4708        };
4709        let bytes = p.encode();
4710        assert_eq!(
4711            p.encoded_len(),
4712            bytes.len(),
4713            "encoded_len must agree with encode().len()"
4714        );
4715        let decoded = RpcRequestChunkPayload::decode(Bytes::from(bytes)).expect("decode");
4716        assert_eq!(decoded, p);
4717    }
4718
4719    /// 2/5 — truncation rejection at every field boundary. The
4720    /// codec must error rather than panic / allocate-unbounded on
4721    /// any short slice.
4722    #[test]
4723    fn request_chunk_decode_rejects_truncation_at_every_boundary() {
4724        let p = RpcRequestChunkPayload {
4725            call_id: 0x1234,
4726            flags: 0,
4727            headers: vec![header("x", b"y")],
4728            body: Bytes::from_static(b"hello"),
4729        };
4730        let full = p.encode();
4731        // Walk every prefix shorter than the full encoding; every
4732        // one must produce a Truncated / TooLarge / InvalidUtf8
4733        // error, not panic.
4734        for n in 0..full.len() {
4735            let prefix = &full[..n];
4736            let result = RpcRequestChunkPayload::decode(Bytes::copy_from_slice(prefix));
4737            assert!(result.is_err(), "n={n}: expected Err, got Ok({:?})", result);
4738        }
4739        // Full length must decode cleanly.
4740        assert!(RpcRequestChunkPayload::decode(Bytes::from(full)).is_ok());
4741    }
4742
4743    /// 3/5 — body length cap rejection. A wire-claimed body length
4744    /// over `MAX_RPC_BODY_LEN` must error rather than try to
4745    /// allocate 4+ MiB of garbage.
4746    #[test]
4747    fn request_chunk_decode_rejects_oversized_body_length() {
4748        // Build a synthetic encoding by hand: small valid prefix
4749        // up to body_len, then claim body_len = MAX_RPC_BODY_LEN + 1.
4750        let mut buf = Vec::new();
4751        buf.put_u64_le(0x42); // call_id
4752        buf.put_u16_le(0); // flags
4753        buf.put_u8(0); // headers count = 0
4754        buf.put_u32_le((MAX_RPC_BODY_LEN + 1) as u32);
4755        // (no body bytes follow — we want the decoder to reject at
4756        // the length check before it even tries to read body bytes)
4757        let err = RpcRequestChunkPayload::decode(Bytes::from(buf))
4758            .expect_err("oversized body length must reject");
4759        match err {
4760            RpcCodecError::TooLarge {
4761                field,
4762                actual,
4763                limit,
4764            } => {
4765                assert_eq!(field, "body");
4766                assert_eq!(actual, MAX_RPC_BODY_LEN + 1);
4767                assert_eq!(limit, MAX_RPC_BODY_LEN);
4768            }
4769            other => panic!("expected TooLarge {{ field=body }}, got {other:?}"),
4770        }
4771    }
4772
4773    /// 4/5 — header count cap rejection. A header count over
4774    /// `MAX_RPC_HEADERS` must error before the per-header decode
4775    /// loop even starts.
4776    #[test]
4777    fn request_chunk_decode_rejects_oversized_header_count() {
4778        let mut buf = Vec::new();
4779        buf.put_u64_le(0x42); // call_id
4780        buf.put_u16_le(0); // flags
4781        buf.put_u8((MAX_RPC_HEADERS + 1) as u8); // over the cap
4782        let err = RpcRequestChunkPayload::decode(Bytes::from(buf))
4783            .expect_err("oversized header count must reject");
4784        match err {
4785            RpcCodecError::TooLarge {
4786                field,
4787                actual,
4788                limit,
4789            } => {
4790                // The shared `decode_headers` helper reports this
4791                // field as "headers".
4792                assert_eq!(field, "headers");
4793                assert_eq!(actual, MAX_RPC_HEADERS + 1);
4794                assert_eq!(limit, MAX_RPC_HEADERS);
4795            }
4796            other => panic!("expected TooLarge {{ field=headers }}, got {other:?}"),
4797        }
4798    }
4799
4800    /// R3-1: a `RequestStream` auto-grant carries the call's
4801    /// AEAD-authenticated `from_node`, so the upload grant is
4802    /// session-scoped — the fold binds each call to its own session, and
4803    /// two calls sharing one entity/origin + caller-chosen call_id
4804    /// produce grants with DISTINCT identities that cannot collapse and
4805    /// refill each other's request semaphore.
4806    ///
4807    /// Red-witness: the pre-R3-1 emit dropped `from_node` (fired
4808    /// `(origin, call_id, 1)`), so both sessions' grants collapsed to the
4809    /// same `(origin, call_id)` — reproduced here by hardcoding
4810    /// `from_node = 0` in the emit, which makes the two captured grants
4811    /// identical and fails the distinctness assertion.
4812    #[tokio::test]
4813    async fn request_stream_auto_grant_carries_the_authenticated_from_node() {
4814        use futures::StreamExt;
4815
4816        const ORIGIN: u64 = 0xBEEF;
4817        const CALL: u64 = 7;
4818        const NODE_A: u64 = 0xAAAA;
4819        const NODE_B: u64 = 0xBBBB;
4820
4821        // A capturing grant emitter (production `RpcRequestGrantEmitter`
4822        // signature) records every `(from_node, origin, call_id, credits)`.
4823        type Grants = Arc<Mutex<Vec<(u64, u64, u64, u32)>>>;
4824        let captured: Grants = Arc::new(Mutex::new(Vec::new()));
4825        let mk_emitter = |sink: Grants| -> RpcRequestGrantEmitter {
4826            Arc::new(move |from_node, origin, call_id, credits| {
4827                sink.lock().push((from_node, origin, call_id, credits));
4828            })
4829        };
4830
4831        // Drive one poll of a RequestStream bound to `node`, over the same
4832        // ORIGIN + CALL, and return the grant it fired.
4833        async fn one_grant(node: u64, emit: RpcRequestGrantEmitter) {
4834            let (tx, rx) = tokio::sync::mpsc::channel::<Bytes>(4);
4835            tx.send(Bytes::from_static(b"chunk"))
4836                .await
4837                .expect("queue chunk");
4838            drop(tx);
4839            let mut stream = RequestStream::new(rx, Some(emit), node, ORIGIN, CALL);
4840            assert_eq!(
4841                stream.next().await.as_deref(),
4842                Some(&b"chunk"[..]),
4843                "the stream must yield the queued chunk",
4844            );
4845        }
4846
4847        one_grant(NODE_A, mk_emitter(captured.clone())).await;
4848        one_grant(NODE_B, mk_emitter(captured.clone())).await;
4849
4850        let grants = captured.lock().clone();
4851        assert_eq!(
4852            grants,
4853            vec![(NODE_A, ORIGIN, CALL, 1), (NODE_B, ORIGIN, CALL, 1)],
4854            "each poll fires ONE grant carrying that call's from_node",
4855        );
4856        assert_ne!(
4857            grants[0], grants[1],
4858            "two sessions over the same origin+call_id must produce DISTINCT grant identities",
4859        );
4860    }
4861
4862    /// 5/5 — RequestGrant round-trip + truncation rejection. The
4863    /// payload is fixed-size (12 bytes), so the test surface is
4864    /// "exactly 12 bytes decodes" + "any other length errors".
4865    #[test]
4866    fn request_grant_roundtrip_and_truncation_rejection() {
4867        // Round-trip across the full u32 range corners + an
4868        // arbitrary mid-value.
4869        for (call_id, credits) in [
4870            (0u64, 0u32),
4871            (1, 1),
4872            (0xFFFF_FFFF_FFFF_FFFF, 0xFFFF_FFFF),
4873            (0xCAFE_F00D, 0x10203040),
4874        ] {
4875            let bytes = encode_request_grant(call_id, credits);
4876            assert_eq!(bytes.len(), 12, "request grant is always 12 bytes");
4877            let decoded = decode_request_grant(&bytes).expect("decode");
4878            assert_eq!(decoded.call_id, call_id);
4879            assert_eq!(decoded.credits, credits);
4880        }
4881        // Wrong-length payloads must reject (return None), not
4882        // panic. Empty, short, long, off-by-one each get covered.
4883        assert!(decode_request_grant(&[]).is_none());
4884        assert!(decode_request_grant(&[0u8; 11]).is_none());
4885        assert!(decode_request_grant(&[0u8; 13]).is_none());
4886    }
4887
4888    /// Bonus pin: `parse_request_window_initial` extracts a valid
4889    /// u32 ASCII-decimal header and rejects everything else.
4890    /// Same coverage shape as `parse_stream_window_initial`'s
4891    /// implicit contract, made explicit here so the request-side
4892    /// helper doesn't drift away from the response-side one.
4893    #[test]
4894    fn parse_request_window_initial_matches_response_side_semantics() {
4895        // Happy path.
4896        let headers = vec![header(HEADER_NRPC_REQUEST_WINDOW_INITIAL, b"32")];
4897        assert_eq!(parse_request_window_initial(&headers), Some(32));
4898        // Case-insensitive on header name.
4899        let headers = vec![header("Nrpc-Request-Window-Initial", b"7")];
4900        assert_eq!(parse_request_window_initial(&headers), Some(7));
4901        // Absent.
4902        assert_eq!(parse_request_window_initial(&[]), None);
4903        // Malformed value (non-numeric).
4904        let headers = vec![header(HEADER_NRPC_REQUEST_WINDOW_INITIAL, b"twelve")];
4905        assert_eq!(parse_request_window_initial(&headers), None);
4906        // Malformed value (non-utf8 bytes).
4907        let headers = vec![header(HEADER_NRPC_REQUEST_WINDOW_INITIAL, &[0xFF, 0xFE])];
4908        assert_eq!(parse_request_window_initial(&headers), None);
4909        // Empty value.
4910        let headers = vec![header(HEADER_NRPC_REQUEST_WINDOW_INITIAL, b"")];
4911        assert_eq!(parse_request_window_initial(&headers), None);
4912    }
4913
4914    // --------------------------------------------------------------------
4915    // RpcRequestPayload codec.
4916    // --------------------------------------------------------------------
4917
4918    #[test]
4919    fn request_roundtrip_minimal() {
4920        let p = RpcRequestPayload {
4921            service: "hello".to_string(),
4922            deadline_ns: 0,
4923            flags: 0,
4924            headers: vec![],
4925            body: Bytes::new(),
4926        };
4927        let bytes = p.encode();
4928        let decoded = RpcRequestPayload::decode(Bytes::from(bytes)).unwrap();
4929        assert_eq!(decoded, p);
4930    }
4931
4932    #[test]
4933    fn request_roundtrip_full() {
4934        let p = RpcRequestPayload {
4935            service: "echo.v1".to_string(),
4936            deadline_ns: 1_700_000_000_000_000_000,
4937            flags: FLAG_RPC_PROPAGATE_TRACE,
4938            headers: vec![
4939                header("traceparent", b"00-aabb..."),
4940                header("idempotency-key", &7u64.to_le_bytes()),
4941                header("content-type", b"application/json"),
4942            ],
4943            body: Bytes::from_static(b"{\"hello\":\"world\"}"),
4944        };
4945        let bytes = p.encode();
4946        let decoded = RpcRequestPayload::decode(Bytes::from(bytes)).unwrap();
4947        assert_eq!(decoded, p);
4948    }
4949
4950    #[test]
4951    fn request_decode_rejects_empty_service() {
4952        let bytes = vec![0x00];
4953        let err = RpcRequestPayload::decode(Bytes::from(bytes)).unwrap_err();
4954        assert!(matches!(err, RpcCodecError::Truncated(_)));
4955    }
4956
4957    #[test]
4958    fn request_decode_rejects_oversize_body_length() {
4959        // Forge: service "x", deadline 0, flags 0, no headers,
4960        // body length = MAX_RPC_BODY_LEN + 1 (no body bytes).
4961        let mut bytes = vec![1u8, b'x'];
4962        bytes.extend_from_slice(&0u64.to_le_bytes()); // deadline
4963        bytes.extend_from_slice(&0u16.to_le_bytes()); // flags
4964        bytes.push(0); // 0 headers
4965        bytes.extend_from_slice(&((MAX_RPC_BODY_LEN as u32) + 1).to_le_bytes());
4966        let err = RpcRequestPayload::decode(Bytes::from(bytes)).unwrap_err();
4967        assert!(
4968            matches!(err, RpcCodecError::TooLarge { field, .. } if field == "body"),
4969            "got {err:?}",
4970        );
4971    }
4972
4973    #[test]
4974    fn request_decode_rejects_oversize_headers_count() {
4975        // Forge: service "x", deadline 0, flags 0, headers count =
4976        // MAX_RPC_HEADERS + 1 (no header bytes).
4977        let mut bytes = vec![1u8, b'x'];
4978        bytes.extend_from_slice(&0u64.to_le_bytes());
4979        bytes.extend_from_slice(&0u16.to_le_bytes());
4980        bytes.push((MAX_RPC_HEADERS as u8).wrapping_add(1));
4981        let err = RpcRequestPayload::decode(Bytes::from(bytes)).unwrap_err();
4982        assert!(
4983            matches!(err, RpcCodecError::TooLarge { field, .. } if field == "headers"),
4984            "got {err:?}",
4985        );
4986    }
4987
4988    #[test]
4989    fn request_decode_rejects_truncated_at_each_field() {
4990        // Build a valid payload then truncate at each field
4991        // boundary; every truncation must error rather than silently
4992        // accept partial state.
4993        let p = RpcRequestPayload {
4994            service: "svc".to_string(),
4995            deadline_ns: 1,
4996            flags: 0,
4997            headers: vec![header("h", b"v")],
4998            body: Bytes::from_static(b"body"),
4999        };
5000        let bytes = p.encode();
5001        // Try each prefix length up to but not including the full
5002        // length — every one must be a decode error.
5003        for trim_to in 0..bytes.len() {
5004            let truncated = &bytes[..trim_to];
5005            let result = RpcRequestPayload::decode(Bytes::copy_from_slice(truncated));
5006            assert!(
5007                result.is_err(),
5008                "trim_to={trim_to} of {} must error, got {:?}",
5009                bytes.len(),
5010                result,
5011            );
5012        }
5013        // Full length must succeed.
5014        assert!(RpcRequestPayload::decode(Bytes::from(bytes)).is_ok());
5015    }
5016
5017    // --------------------------------------------------------------------
5018    // RpcResponsePayload codec.
5019    // --------------------------------------------------------------------
5020
5021    #[test]
5022    fn response_roundtrip_ok_with_body() {
5023        let p = RpcResponsePayload {
5024            status: RpcStatus::Ok,
5025            headers: vec![header("content-type", b"application/json")],
5026            body: Bytes::from_static(b"{\"answer\":42}"),
5027        };
5028        let bytes = p.encode();
5029        let decoded = RpcResponsePayload::decode(Bytes::from(bytes)).unwrap();
5030        assert_eq!(decoded, p);
5031    }
5032
5033    #[test]
5034    fn response_roundtrip_application_status() {
5035        let p = RpcResponsePayload {
5036            status: RpcStatus::Application(0xBEEF),
5037            headers: vec![],
5038            body: Bytes::from_static(b"app-specific diagnostic"),
5039        };
5040        let bytes = p.encode();
5041        let decoded = RpcResponsePayload::decode(Bytes::from(bytes)).unwrap();
5042        assert_eq!(decoded.status, RpcStatus::Application(0xBEEF));
5043        assert_eq!(decoded.body, p.body);
5044    }
5045
5046    #[test]
5047    fn response_decode_rejects_empty_buffer() {
5048        let err = RpcResponsePayload::decode(Bytes::new()).unwrap_err();
5049        assert!(matches!(err, RpcCodecError::Truncated(_)));
5050    }
5051
5052    // --------------------------------------------------------------------
5053    // Invariant: encoded sizes are reasonable.
5054    // --------------------------------------------------------------------
5055
5056    /// Wire-size budget regression: a tiny request encodes in a
5057    /// small constant number of bytes plus body. Pre-fix the headers
5058    /// or service-length encoding could have grown unbounded; pin
5059    /// the small-case so a regression in either inflates the
5060    /// minimum.
5061    #[test]
5062    fn request_minimum_wire_size_is_bounded() {
5063        let p = RpcRequestPayload {
5064            service: "x".to_string(),
5065            deadline_ns: 0,
5066            flags: 0,
5067            headers: vec![],
5068            body: Bytes::new(),
5069        };
5070        let size = p.encode().len();
5071        // 1 (svc len) + 1 (svc bytes) + 8 (deadline) + 2 (flags) + 1 (headers count) + 4 (body len) = 17
5072        assert_eq!(size, 17, "minimum request encodes in 17 bytes");
5073        assert_eq!(request_wire_size(&p), RPC_FRAME_BODY_OFFSET + 17);
5074    }
5075
5076    #[test]
5077    fn response_minimum_wire_size_is_bounded() {
5078        let p = RpcResponsePayload {
5079            status: RpcStatus::Ok,
5080            headers: vec![],
5081            body: Bytes::new(),
5082        };
5083        let size = p.encode().len();
5084        // 2 (status) + 1 (headers count) + 4 (body len) = 7
5085        assert_eq!(size, 7, "minimum response encodes in 7 bytes");
5086        assert_eq!(response_wire_size(&p), RPC_FRAME_BODY_OFFSET + 7);
5087    }
5088
5089    // ====================================================================
5090    // RpcServerFold — server-side dispatch behavior.
5091    //
5092    // These tests drive the fold directly with synthetic events
5093    // and observe the emitter callback. The end-to-end story
5094    // (Mesh::serve_rpc + bus + cortex adapter) is integration-
5095    // tested separately once the glue layer lands.
5096    // ====================================================================
5097
5098    use super::super::super::redex::{RedexEntry, RedexEvent};
5099    use std::sync::atomic::AtomicUsize;
5100    use std::time::Duration;
5101
5102    /// Captured-response store. Test-local typedef so the
5103    /// `capturing_emitter` signature stays under the `clippy::
5104    /// type_complexity` lint.
5105    type CapturedResponses = Arc<Mutex<Vec<(u64, u64, RpcResponsePayload)>>>;
5106
5107    /// Build a synthetic RedexEvent carrying an RPC request payload.
5108    /// Tests use this to drive the fold without going through the
5109    /// real ingest/cortex pipeline.
5110    fn rpc_request_event(
5111        caller_origin: u64,
5112        call_id: u64,
5113        payload: RpcRequestPayload,
5114    ) -> RedexEvent {
5115        let meta = EventMeta::new(DISPATCH_RPC_REQUEST, 0, caller_origin, call_id, 0);
5116        let mut buf = Vec::new();
5117        buf.extend_from_slice(&meta.to_bytes());
5118        // OA2-E0.2: RpcRouteV1 route placeholder — these test frames
5119        // feed the folds directly (no ingress select), and the folds
5120        // skip the route to reach the payload at RPC_FRAME_BODY_OFFSET.
5121        encode_rpc_route(&mut buf, 0);
5122        buf.extend_from_slice(&payload.encode());
5123        RedexEvent {
5124            entry: RedexEntry::new_heap(0, 0, buf.len() as u32, 0, 0),
5125            payload: bytes::Bytes::from(buf),
5126        }
5127    }
5128
5129    fn rpc_cancel_event(caller_origin: u64, call_id: u64) -> RedexEvent {
5130        let meta = EventMeta::new(DISPATCH_RPC_CANCEL, 0, caller_origin, call_id, 0);
5131        let mut buf = meta.to_bytes().to_vec();
5132        // OA2-E0.2: RpcRouteV1 route placeholder (folds skip it).
5133        encode_rpc_route(&mut buf, 0);
5134        RedexEvent {
5135            entry: RedexEntry::new_heap(0, 0, buf.len() as u32, 0, 0),
5136            payload: bytes::Bytes::from(buf),
5137        }
5138    }
5139
5140    /// AV-1 item 1: wrap a synthetic frame as an inbound event from
5141    /// the AEAD-authenticated session peer `from_node`, so a test can
5142    /// drive the production `apply_inbound` seam directly. The folds
5143    /// read the caller origin + call_id from the frame's `EventMeta`;
5144    /// `channel_hash` / `origin_hash` are ignored, so only `from_node`
5145    /// and `payload` are load-bearing for the call-identity key.
5146    fn inbound(from_node: u64, frame: bytes::Bytes) -> RpcInboundEvent {
5147        RpcInboundEvent {
5148            channel_hash: 0,
5149            origin_hash: 0,
5150            from_node,
5151            payload: frame,
5152        }
5153    }
5154
5155    /// Captures responses emitted by the fold for assertion in tests.
5156    fn capturing_emitter() -> (RpcResponseEmitter, CapturedResponses) {
5157        let captured: CapturedResponses = Arc::new(Mutex::new(Vec::new()));
5158        let captured_clone = captured.clone();
5159        let emit: RpcResponseEmitter = Arc::new(move |_from_node, origin, call_id, resp| {
5160            captured_clone.lock().push((origin, call_id, resp));
5161        });
5162        (emit, captured)
5163    }
5164
5165    /// A handler that just echoes the request body back as the
5166    /// response body, with `RpcStatus::Ok`.
5167    struct EchoHandler;
5168    #[async_trait::async_trait]
5169    impl RpcHandler for EchoHandler {
5170        async fn call(&self, ctx: RpcContext) -> Result<RpcResponsePayload, RpcHandlerError> {
5171            Ok(RpcResponsePayload {
5172                status: RpcStatus::Ok,
5173                headers: vec![],
5174                body: ctx.payload.body,
5175            })
5176        }
5177    }
5178
5179    /// Wait until `pred` is true, polling at 10ms intervals up to
5180    /// `timeout`. Used to await spawned-handler completion in tests
5181    /// without a sleep-and-pray.
5182    async fn wait_until<F: Fn() -> bool>(pred: F, timeout: Duration) -> bool {
5183        let start = std::time::Instant::now();
5184        while start.elapsed() < timeout {
5185            if pred() {
5186                return true;
5187            }
5188            tokio::time::sleep(Duration::from_millis(10)).await;
5189        }
5190        pred()
5191    }
5192
5193    /// Happy path: a REQUEST event triggers the handler; the fold
5194    /// emits a RESPONSE with the handler's payload.
5195    #[tokio::test]
5196    async fn server_fold_request_invokes_handler_and_emits_response() {
5197        let (emit, captured) = capturing_emitter();
5198        let mut fold = RpcServerFold::new(Arc::new(EchoHandler), emit);
5199        let req = RpcRequestPayload {
5200            service: "echo".to_string(),
5201            deadline_ns: 0,
5202            flags: 0,
5203            headers: vec![],
5204            body: Bytes::from_static(b"hello"),
5205        };
5206        let ev = rpc_request_event(0xCAFE, 7, req);
5207        fold.apply(&ev, &mut ()).unwrap();
5208
5209        // Handler runs in tokio::spawn; wait for the emit.
5210        assert!(
5211            wait_until(|| !captured.lock().is_empty(), Duration::from_secs(2)).await,
5212            "expected one emitted response"
5213        );
5214        let captured = captured.lock();
5215        assert_eq!(captured.len(), 1);
5216        let (origin, call_id, resp) = &captured[0];
5217        assert_eq!(*origin, 0xCAFE);
5218        assert_eq!(*call_id, 7);
5219        assert_eq!(resp.status, RpcStatus::Ok);
5220        assert_eq!(resp.body.as_ref(), b"hello");
5221        // In-flight set is cleaned up after the handler completes.
5222        assert!(fold.in_flight_keys().is_empty());
5223    }
5224
5225    /// E1.6: `apply_inbound_admitted` delivers the four-party `Admitted`
5226    /// to the handler via `RpcContext::org_admission` AND strips every
5227    /// `net-org-admission` proof header from the payload the handler
5228    /// sees, preserving the surrounding headers in order. (The public
5229    /// `apply_inbound` path leaves `org_admission` `None` — the other
5230    /// fold tests never set it, and their handlers keep their headers.)
5231    #[tokio::test]
5232    async fn admitted_request_delivers_attribution_and_strips_proof_header() {
5233        use crate::adapter::net::behavior::org::OrgKeypair;
5234        use crate::adapter::net::behavior::org_admission::Admitted;
5235        use crate::adapter::net::behavior::org_call::ORG_ADMISSION_HEADER;
5236        use crate::adapter::net::behavior::org_grant::CapabilityAuthorityId;
5237        use crate::adapter::net::identity::EntityId;
5238
5239        type Seen = Arc<Mutex<Option<(Option<Admitted>, Vec<RpcHeader>)>>>;
5240        struct SpyHandler(Seen);
5241        #[async_trait::async_trait]
5242        impl RpcHandler for SpyHandler {
5243            async fn call(&self, ctx: RpcContext) -> Result<RpcResponsePayload, RpcHandlerError> {
5244                *self.0.lock() = Some((ctx.org_admission.clone(), ctx.payload.headers.clone()));
5245                Ok(RpcResponsePayload {
5246                    status: RpcStatus::Ok,
5247                    headers: vec![],
5248                    body: Bytes::new(),
5249                })
5250            }
5251        }
5252
5253        let seen: Seen = Arc::new(Mutex::new(None));
5254        let (emit, _captured) = capturing_emitter();
5255        let mut fold = RpcServerFold::new(Arc::new(SpyHandler(seen.clone())), emit);
5256
5257        let admitted = Admitted {
5258            caller: EntityId::from_bytes([0x24u8; 32]),
5259            acting_org: OrgKeypair::from_bytes([0x77u8; 32]).org_id(),
5260            provider_org: OrgKeypair::from_bytes([0x42u8; 32]).org_id(),
5261            provider: EntityId::from_bytes([0x99u8; 32]),
5262            capability: CapabilityAuthorityId::for_tag("nrpc:oa2-echo"),
5263        };
5264        let req = RpcRequestPayload {
5265            service: "oa2-echo".to_string(),
5266            deadline_ns: 0,
5267            flags: 0,
5268            headers: vec![
5269                ("x-keep".to_string(), b"1".to_vec()),
5270                (ORG_ADMISSION_HEADER.to_string(), b"opaque-proof".to_vec()),
5271                ("y-keep".to_string(), b"2".to_vec()),
5272            ],
5273            body: Bytes::from_static(b"hi"),
5274        };
5275        let frame = rpc_request_event(0xCAFE, 7, req).payload;
5276        fold.apply_inbound_admitted(&inbound(0x61, frame), admitted.clone())
5277            .unwrap();
5278
5279        assert!(
5280            wait_until(|| seen.lock().is_some(), Duration::from_secs(2)).await,
5281            "handler must run for an admitted request",
5282        );
5283        let (got_admission, got_headers) = seen.lock().clone().unwrap();
5284        assert_eq!(
5285            got_admission,
5286            Some(admitted),
5287            "the four-party Admitted must reach the handler",
5288        );
5289        assert_eq!(
5290            got_headers,
5291            vec![
5292                ("x-keep".to_string(), b"1".to_vec()),
5293                ("y-keep".to_string(), b"2".to_vec()),
5294            ],
5295            "the proof header is stripped; surrounding headers stay, in order",
5296        );
5297    }
5298
5299    /// Application error: handler returns
5300    /// `RpcHandlerError::Application` → fold emits a response with
5301    /// `RpcStatus::Application(code)` and the message as body.
5302    #[tokio::test]
5303    async fn server_fold_application_error_maps_to_application_status() {
5304        struct AppErrHandler;
5305        #[async_trait::async_trait]
5306        impl RpcHandler for AppErrHandler {
5307            async fn call(&self, _ctx: RpcContext) -> Result<RpcResponsePayload, RpcHandlerError> {
5308                Err(RpcHandlerError::Application {
5309                    code: 0xBEEF,
5310                    message: "bad input".to_string(),
5311                })
5312            }
5313        }
5314        let (emit, captured) = capturing_emitter();
5315        let mut fold = RpcServerFold::new(Arc::new(AppErrHandler), emit);
5316        let req = RpcRequestPayload {
5317            service: "x".to_string(),
5318            deadline_ns: 0,
5319            flags: 0,
5320            headers: vec![],
5321            body: Bytes::new(),
5322        };
5323        fold.apply(&rpc_request_event(1, 1, req), &mut ()).unwrap();
5324        assert!(wait_until(|| !captured.lock().is_empty(), Duration::from_secs(2)).await);
5325        let captured = captured.lock();
5326        let (_, _, resp) = &captured[0];
5327        assert_eq!(resp.status, RpcStatus::Application(0xBEEF));
5328        assert_eq!(resp.body.as_ref(), b"bad input");
5329    }
5330
5331    /// Internal error: handler returns `RpcHandlerError::Internal`
5332    /// → fold emits `RpcStatus::Internal` with the message body.
5333    #[tokio::test]
5334    async fn server_fold_internal_error_maps_to_internal_status() {
5335        struct IntErrHandler;
5336        #[async_trait::async_trait]
5337        impl RpcHandler for IntErrHandler {
5338            async fn call(&self, _ctx: RpcContext) -> Result<RpcResponsePayload, RpcHandlerError> {
5339                Err(RpcHandlerError::Internal("db timeout".to_string()))
5340            }
5341        }
5342        let (emit, captured) = capturing_emitter();
5343        let mut fold = RpcServerFold::new(Arc::new(IntErrHandler), emit);
5344        let req = RpcRequestPayload {
5345            service: "x".to_string(),
5346            deadline_ns: 0,
5347            flags: 0,
5348            headers: vec![],
5349            body: Bytes::new(),
5350        };
5351        fold.apply(&rpc_request_event(1, 1, req), &mut ()).unwrap();
5352        assert!(wait_until(|| !captured.lock().is_empty(), Duration::from_secs(2)).await);
5353        let captured = captured.lock();
5354        let (_, _, resp) = &captured[0];
5355        assert_eq!(resp.status, RpcStatus::Internal);
5356        assert_eq!(resp.body.as_ref(), b"db timeout");
5357    }
5358
5359    /// Handler panic: caught by the fold's `catch_unwind`; surfaces
5360    /// as `RpcStatus::Internal` to the caller. Pre-fix the panic
5361    /// would propagate up the spawned task, log a tokio
5362    /// uncaught-panic message, and silently leave the caller
5363    /// waiting forever.
5364    #[tokio::test]
5365    async fn server_fold_handler_panic_surfaces_as_internal_status() {
5366        struct PanicHandler;
5367        #[async_trait::async_trait]
5368        impl RpcHandler for PanicHandler {
5369            async fn call(&self, _ctx: RpcContext) -> Result<RpcResponsePayload, RpcHandlerError> {
5370                panic!("kaboom");
5371            }
5372        }
5373        let (emit, captured) = capturing_emitter();
5374        let mut fold = RpcServerFold::new(Arc::new(PanicHandler), emit);
5375        let req = RpcRequestPayload {
5376            service: "x".to_string(),
5377            deadline_ns: 0,
5378            flags: 0,
5379            headers: vec![],
5380            body: Bytes::new(),
5381        };
5382        fold.apply(&rpc_request_event(1, 1, req), &mut ()).unwrap();
5383        assert!(wait_until(|| !captured.lock().is_empty(), Duration::from_secs(2)).await);
5384        let captured = captured.lock();
5385        let (_, _, resp) = &captured[0];
5386        assert_eq!(resp.status, RpcStatus::Internal);
5387        assert!(
5388            String::from_utf8_lossy(&resp.body).contains("kaboom"),
5389            "panic message must surface in body, got {}",
5390            String::from_utf8_lossy(&resp.body),
5391        );
5392    }
5393
5394    /// Deadline already passed: server short-circuits with
5395    /// `Timeout` without invoking the handler. Pinned via the
5396    /// `with_test_now_ns` clock override so the test doesn't race
5397    /// wall time.
5398    #[tokio::test]
5399    async fn server_fold_deadline_already_passed_short_circuits_to_timeout() {
5400        let invoked = Arc::new(AtomicBool::new(false));
5401        struct CountingHandler {
5402            invoked: Arc<AtomicBool>,
5403        }
5404        #[async_trait::async_trait]
5405        impl RpcHandler for CountingHandler {
5406            async fn call(&self, _ctx: RpcContext) -> Result<RpcResponsePayload, RpcHandlerError> {
5407                self.invoked.store(true, Ordering::Release);
5408                Ok(RpcResponsePayload {
5409                    status: RpcStatus::Ok,
5410                    headers: vec![],
5411                    body: Bytes::new(),
5412                })
5413            }
5414        }
5415        let (emit, captured) = capturing_emitter();
5416        let mut fold = RpcServerFold::new(
5417            Arc::new(CountingHandler {
5418                invoked: invoked.clone(),
5419            }),
5420            emit,
5421        )
5422        // Use a clock value > DEADLINE_SKEW_TOLERANCE_NS + 1
5423        // (10s + 1ns) so the deadline-passed check fires past the
5424        // skew tolerance window. With now=20s and deadline=1ns,
5425        // (now - 10s) > 1ns.
5426        .with_test_now_ns(20_000_000_000);
5427        let req = RpcRequestPayload {
5428            service: "x".to_string(),
5429            // Deadline well in the past — past the skew tolerance.
5430            deadline_ns: 1_000,
5431            flags: 0,
5432            headers: vec![],
5433            body: Bytes::new(),
5434        };
5435        fold.apply(&rpc_request_event(1, 1, req), &mut ()).unwrap();
5436        // Emit happens synchronously in the deadline-passed branch
5437        // (no handler spawn).
5438        let captured = captured.lock();
5439        assert_eq!(captured.len(), 1);
5440        let (_, _, resp) = &captured[0];
5441        assert_eq!(resp.status, RpcStatus::Timeout);
5442        assert!(
5443            !invoked.load(Ordering::Acquire),
5444            "handler must NOT be invoked when deadline already passed",
5445        );
5446    }
5447
5448    /// Regression: a deadline that has elapsed by less than
5449    /// `DEADLINE_SKEW_TOLERANCE_NS` does NOT short-circuit. A
5450    /// peer with a slightly-fast clock would otherwise be
5451    /// prematurely timed out before the handler ever ran.
5452    #[tokio::test]
5453    async fn server_fold_deadline_within_skew_tolerance_invokes_handler() {
5454        let invoked = Arc::new(AtomicBool::new(false));
5455        struct CountingHandler {
5456            invoked: Arc<AtomicBool>,
5457        }
5458        #[async_trait::async_trait]
5459        impl RpcHandler for CountingHandler {
5460            async fn call(&self, _ctx: RpcContext) -> Result<RpcResponsePayload, RpcHandlerError> {
5461                self.invoked.store(true, Ordering::Release);
5462                Ok(RpcResponsePayload {
5463                    status: RpcStatus::Ok,
5464                    headers: vec![],
5465                    body: Bytes::new(),
5466                })
5467            }
5468        }
5469        let (emit, captured) = capturing_emitter();
5470        let mut fold = RpcServerFold::new(
5471            Arc::new(CountingHandler {
5472                invoked: invoked.clone(),
5473            }),
5474            emit,
5475        )
5476        // now = 100s, deadline = 95s → elapsed = 5s, within the
5477        // 10s skew tolerance.
5478        .with_test_now_ns(100_000_000_000);
5479        let req = RpcRequestPayload {
5480            service: "x".to_string(),
5481            deadline_ns: 95_000_000_000,
5482            flags: 0,
5483            headers: vec![],
5484            body: Bytes::new(),
5485        };
5486        fold.apply(&rpc_request_event(1, 1, req), &mut ()).unwrap();
5487        assert!(
5488            wait_until(|| invoked.load(Ordering::Acquire), Duration::from_secs(1)).await,
5489            "handler must run when deadline is within skew tolerance",
5490        );
5491        let captured = captured.lock();
5492        assert_eq!(captured.len(), 1);
5493        assert_eq!(captured[0].2.status, RpcStatus::Ok);
5494    }
5495
5496    /// CANCEL flips the matching in-flight token. The handler that
5497    /// `select!`s on the cancellation observes the signal and can
5498    /// short-circuit. The fold removes the in-flight entry on
5499    /// CANCEL.
5500    #[tokio::test]
5501    async fn server_fold_cancel_flips_token_and_clears_in_flight() {
5502        let resumed_after_cancel = Arc::new(AtomicBool::new(false));
5503        struct CancelObservingHandler {
5504            resumed: Arc<AtomicBool>,
5505        }
5506        #[async_trait::async_trait]
5507        impl RpcHandler for CancelObservingHandler {
5508            async fn call(&self, ctx: RpcContext) -> Result<RpcResponsePayload, RpcHandlerError> {
5509                tokio::select! {
5510                    _ = ctx.cancellation.cancelled() => {
5511                        self.resumed.store(true, Ordering::Release);
5512                        Err(RpcHandlerError::Internal("cancelled by caller".to_string()))
5513                    }
5514                    _ = tokio::time::sleep(Duration::from_secs(5)) => {
5515                        Ok(RpcResponsePayload {
5516                            status: RpcStatus::Ok,
5517                            headers: vec![],
5518                            body: Bytes::from_static(b"slept the full window"),
5519                        })
5520                    }
5521                }
5522            }
5523        }
5524        let (emit, captured) = capturing_emitter();
5525        let mut fold = RpcServerFold::new(
5526            Arc::new(CancelObservingHandler {
5527                resumed: resumed_after_cancel.clone(),
5528            }),
5529            emit,
5530        );
5531        let req = RpcRequestPayload {
5532            service: "x".to_string(),
5533            deadline_ns: 0,
5534            flags: 0,
5535            headers: vec![],
5536            body: Bytes::new(),
5537        };
5538        fold.apply(&rpc_request_event(1, 42, req), &mut ()).unwrap();
5539        // Wait until the handler's `select!` is parked; then send
5540        // CANCEL.
5541        assert!(
5542            wait_until(
5543                || fold.in_flight_keys().contains(&(0, 1, 42)),
5544                Duration::from_secs(1)
5545            )
5546            .await
5547        );
5548        fold.apply(&rpc_cancel_event(1, 42), &mut ()).unwrap();
5549        // The cancellation is observed by the handler. Even though
5550        // the handler returns `Internal("cancelled by caller")`,
5551        // the fold's CANCEL-wins ordering overrides the response
5552        // with `RpcStatus::Cancelled` so the caller sees the
5553        // documented status code rather than the handler's
5554        // accidental Internal payload.
5555        assert!(
5556            wait_until(|| !captured.lock().is_empty(), Duration::from_secs(2)).await,
5557            "handler should observe cancellation and emit response"
5558        );
5559        assert!(
5560            resumed_after_cancel.load(Ordering::Acquire),
5561            "handler must observe cancellation"
5562        );
5563        let captured = captured.lock();
5564        assert_eq!(captured.len(), 1);
5565        let (_, _, resp) = &captured[0];
5566        assert_eq!(
5567            resp.status,
5568            RpcStatus::Cancelled,
5569            "CANCEL must override handler outcome with RpcStatus::Cancelled"
5570        );
5571        // CANCEL also removes the in-flight entry directly.
5572        // Handler completion removes it again (idempotent).
5573        assert!(fold.in_flight_keys().is_empty());
5574    }
5575
5576    // ====================================================================
5577    // AV-1 item 1 — server-fold call/control identity is bound to the
5578    // AEAD-authenticated session peer `from_node`. Each witness drives
5579    // the production `apply_inbound` seam with a victim frame on one
5580    // session and an adversarial control frame that copies the victim's
5581    // origin + call_id but arrives on a DIFFERENT session. The forged
5582    // frame must miss the `(from_node, origin, call_id)` key and leave
5583    // the victim's call/control state untouched.
5584    // ====================================================================
5585
5586    /// CANCEL hijack (unary). An attacker that copies the victim's
5587    /// origin + call_id onto a CANCEL, but sends it on its own session,
5588    /// must not cancel the victim's in-flight call. Only a CANCEL from
5589    /// the victim's own session cancels it.
5590    #[tokio::test]
5591    async fn unary_fold_foreign_session_cancel_cannot_hijack_a_call() {
5592        const VICTIM: u64 = 0xA;
5593        const ATTACKER: u64 = 0xB;
5594        const ORIGIN: u64 = 0x1111;
5595        const CALL_ID: u64 = 42;
5596        let resumed = Arc::new(AtomicBool::new(false));
5597        struct H {
5598            resumed: Arc<AtomicBool>,
5599        }
5600        #[async_trait::async_trait]
5601        impl RpcHandler for H {
5602            async fn call(&self, ctx: RpcContext) -> Result<RpcResponsePayload, RpcHandlerError> {
5603                tokio::select! {
5604                    _ = ctx.cancellation.cancelled() => {
5605                        self.resumed.store(true, Ordering::Release);
5606                        Err(RpcHandlerError::Internal("cancelled".to_string()))
5607                    }
5608                    _ = tokio::time::sleep(Duration::from_secs(5)) => Ok(RpcResponsePayload {
5609                        status: RpcStatus::Ok,
5610                        headers: vec![],
5611                        body: Bytes::from_static(b"slept"),
5612                    }),
5613                }
5614            }
5615        }
5616        let (emit, captured) = capturing_emitter();
5617        let mut fold = RpcServerFold::new(
5618            Arc::new(H {
5619                resumed: resumed.clone(),
5620            }),
5621            emit,
5622        );
5623        let req = RpcRequestPayload {
5624            service: "x".to_string(),
5625            deadline_ns: 0,
5626            flags: 0,
5627            headers: vec![],
5628            body: Bytes::new(),
5629        };
5630        // Victim's REQUEST on the victim's session.
5631        fold.apply_inbound(&inbound(
5632            VICTIM,
5633            rpc_request_event(ORIGIN, CALL_ID, req).payload,
5634        ))
5635        .unwrap();
5636        assert!(
5637            wait_until(
5638                || fold.in_flight_keys().contains(&(VICTIM, ORIGIN, CALL_ID)),
5639                Duration::from_secs(1)
5640            )
5641            .await
5642        );
5643        // Attacker's forged CANCEL on a DIFFERENT session — must miss.
5644        fold.apply_inbound(&inbound(
5645            ATTACKER,
5646            rpc_cancel_event(ORIGIN, CALL_ID).payload,
5647        ))
5648        .unwrap();
5649        tokio::time::sleep(Duration::from_millis(100)).await;
5650        assert!(
5651            fold.in_flight_keys().contains(&(VICTIM, ORIGIN, CALL_ID)),
5652            "forged CANCEL from a foreign session must not remove the victim's entry",
5653        );
5654        assert!(
5655            !resumed.load(Ordering::Acquire),
5656            "victim handler must not observe the forged CANCEL",
5657        );
5658        assert!(
5659            captured.lock().is_empty(),
5660            "a hijacked CANCEL must not produce a terminal response",
5661        );
5662        // The victim's OWN CANCEL cancels the call.
5663        fold.apply_inbound(&inbound(VICTIM, rpc_cancel_event(ORIGIN, CALL_ID).payload))
5664            .unwrap();
5665        assert!(
5666            wait_until(|| !captured.lock().is_empty(), Duration::from_secs(2)).await,
5667            "the victim's own CANCEL must cancel the call",
5668        );
5669        assert!(resumed.load(Ordering::Acquire));
5670        assert_eq!(captured.lock()[0].2.status, RpcStatus::Cancelled);
5671    }
5672
5673    /// STREAM_GRANT + CANCEL hijack (server-streaming). A forged
5674    /// STREAM_GRANT from a foreign session must not refill the victim's
5675    /// flow-control window, and a forged CANCEL must not tear the call
5676    /// down.
5677    #[tokio::test]
5678    async fn streaming_fold_foreign_session_cannot_refill_or_cancel() {
5679        const VICTIM: u64 = 0xA;
5680        const ATTACKER: u64 = 0xB;
5681        const ORIGIN: u64 = 0x2222;
5682        const CALL_ID: u64 = 7;
5683        let release = Arc::new(Notify::new());
5684        struct Blocking {
5685            release: Arc<Notify>,
5686        }
5687        #[async_trait::async_trait]
5688        impl RpcStreamingHandler for Blocking {
5689            async fn call(
5690                &self,
5691                _ctx: RpcContext,
5692                _sink: RpcResponseSink,
5693            ) -> Result<(), RpcHandlerError> {
5694                // Never emit a chunk (so the pump consumes no permits);
5695                // hold the call open until released.
5696                self.release.notified().await;
5697                Ok(())
5698            }
5699        }
5700        let (emit, _captured) = capturing_async_emitter();
5701        let mut fold = RpcServerStreamingFold::new(
5702            Arc::new(Blocking {
5703                release: release.clone(),
5704            }),
5705            emit,
5706        );
5707        // Victim REQUEST opting into flow control with an initial
5708        // window of 2.
5709        let req = RpcRequestPayload {
5710            service: "s".to_string(),
5711            deadline_ns: 0,
5712            flags: 0,
5713            headers: vec![(HEADER_NRPC_STREAM_WINDOW_INITIAL.to_string(), b"2".to_vec())],
5714            body: Bytes::new(),
5715        };
5716        fold.apply_inbound(&inbound(
5717            VICTIM,
5718            rpc_request_event(ORIGIN, CALL_ID, req).payload,
5719        ))
5720        .unwrap();
5721        assert!(
5722            wait_until(
5723                || fold.in_flight_keys().contains(&(VICTIM, ORIGIN, CALL_ID)),
5724                Duration::from_secs(1)
5725            )
5726            .await
5727        );
5728        assert_eq!(
5729            fold.flow_control_permits((VICTIM, ORIGIN, CALL_ID)),
5730            Some(2),
5731            "victim's initial window",
5732        );
5733        // Attacker STREAM_GRANT(5) on its own session — must miss.
5734        fold.apply_inbound(&inbound(
5735            ATTACKER,
5736            rpc_stream_grant_event(ORIGIN, CALL_ID, 5).payload,
5737        ))
5738        .unwrap();
5739        assert_eq!(
5740            fold.flow_control_permits((VICTIM, ORIGIN, CALL_ID)),
5741            Some(2),
5742            "a forged STREAM_GRANT from a foreign session must not refill the victim's window",
5743        );
5744        // Attacker CANCEL on its own session — must miss.
5745        fold.apply_inbound(&inbound(
5746            ATTACKER,
5747            rpc_cancel_event(ORIGIN, CALL_ID).payload,
5748        ))
5749        .unwrap();
5750        tokio::time::sleep(Duration::from_millis(100)).await;
5751        assert!(
5752            fold.in_flight_keys().contains(&(VICTIM, ORIGIN, CALL_ID)),
5753            "a forged CANCEL from a foreign session must not tear down the victim's stream",
5754        );
5755        // The victim's own STREAM_GRANT(5) DOES refill.
5756        fold.apply_inbound(&inbound(
5757            VICTIM,
5758            rpc_stream_grant_event(ORIGIN, CALL_ID, 5).payload,
5759        ))
5760        .unwrap();
5761        assert_eq!(
5762            fold.flow_control_permits((VICTIM, ORIGIN, CALL_ID)),
5763            Some(7),
5764            "the victim's own STREAM_GRANT must refill its window",
5765        );
5766        release.notify_one();
5767    }
5768
5769    /// REQUEST_CHUNK + CANCEL hijack (client-streaming). A forged
5770    /// REQUEST_CHUNK from a foreign session must not enter the victim's
5771    /// upload stream, and a forged CANCEL must not close it.
5772    #[tokio::test]
5773    async fn client_streaming_fold_foreign_session_cannot_feed_or_cancel() {
5774        const VICTIM: u64 = 0xA;
5775        const ATTACKER: u64 = 0xB;
5776        const ORIGIN: u64 = 0xCAFE;
5777        const CALL_ID: u64 = 7;
5778        let seen = Arc::new(Mutex::new(Vec::new()));
5779        let observed_cancel = Arc::new(AtomicBool::new(false));
5780        let (emit, captured) = capturing_emitter();
5781        let mut fold = RpcStreamingRequestFold::new(
5782            Arc::new(CollectingClientStreamHandler {
5783                seen: seen.clone(),
5784                observed_cancel: observed_cancel.clone(),
5785            }),
5786            emit,
5787        );
5788        // Victim REQUEST (client-streaming flag), initial body "a".
5789        let req = RpcRequestPayload {
5790            service: "agg".to_string(),
5791            deadline_ns: 0,
5792            flags: FLAG_RPC_CLIENT_STREAMING_REQUEST,
5793            headers: vec![],
5794            body: Bytes::from_static(b"a"),
5795        };
5796        fold.apply_inbound(&inbound(
5797            VICTIM,
5798            rpc_request_event(ORIGIN, CALL_ID, req).payload,
5799        ))
5800        .unwrap();
5801        assert!(
5802            wait_until(
5803                || fold.sender_keys().contains(&(VICTIM, ORIGIN, CALL_ID)),
5804                Duration::from_secs(1)
5805            )
5806            .await
5807        );
5808        // Attacker forges a REQUEST_CHUNK and a CANCEL with the victim's
5809        // origin + call_id, both on its OWN session — both must miss.
5810        fold.apply_inbound(&inbound(
5811            ATTACKER,
5812            rpc_request_chunk_event(ORIGIN, CALL_ID, 0, b"ATTACK".to_vec()).payload,
5813        ))
5814        .unwrap();
5815        fold.apply_inbound(&inbound(
5816            ATTACKER,
5817            rpc_cancel_event(ORIGIN, CALL_ID).payload,
5818        ))
5819        .unwrap();
5820        tokio::time::sleep(Duration::from_millis(100)).await;
5821        assert!(
5822            fold.sender_keys().contains(&(VICTIM, ORIGIN, CALL_ID)),
5823            "forged frames must not close the victim's upload stream",
5824        );
5825        // Victim feeds a legit chunk and ends its own stream.
5826        fold.apply_inbound(&inbound(
5827            VICTIM,
5828            rpc_request_chunk_event(ORIGIN, CALL_ID, 0, b"b".to_vec()).payload,
5829        ))
5830        .unwrap();
5831        fold.apply_inbound(&inbound(
5832            VICTIM,
5833            rpc_request_chunk_event(ORIGIN, CALL_ID, FLAG_RPC_REQUEST_END, b"c".to_vec()).payload,
5834        ))
5835        .unwrap();
5836        assert!(
5837            wait_until(|| !captured.lock().is_empty(), Duration::from_secs(2)).await,
5838            "expected terminal RESPONSE",
5839        );
5840        let bodies: Vec<Vec<u8>> = seen.lock().iter().map(|b| b.to_vec()).collect();
5841        assert_eq!(
5842            bodies,
5843            vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()],
5844            "the attacker's forged chunk must never enter the victim's stream",
5845        );
5846        assert!(
5847            !observed_cancel.load(Ordering::SeqCst),
5848            "the attacker's forged CANCEL must not cancel the victim's stream",
5849        );
5850    }
5851
5852    /// REQUEST_CHUNK + CANCEL hijack (duplex). Same contract as the
5853    /// client-streaming witness, on the duplex fold.
5854    #[tokio::test]
5855    async fn duplex_fold_foreign_session_cannot_feed_or_cancel() {
5856        const VICTIM: u64 = 0xA;
5857        const ATTACKER: u64 = 0xB;
5858        const ORIGIN: u64 = 0xBEEF;
5859        const CALL_ID: u64 = 9;
5860        let seen = Arc::new(Mutex::new(Vec::new()));
5861        let observed_cancel = Arc::new(AtomicBool::new(false));
5862        struct DuplexCollect {
5863            seen: Arc<Mutex<Vec<bytes::Bytes>>>,
5864            observed_cancel: Arc<AtomicBool>,
5865        }
5866        #[async_trait::async_trait]
5867        impl RpcDuplexHandler for DuplexCollect {
5868            async fn call(
5869                &self,
5870                ctx: RpcStreamingContext,
5871                mut requests: RequestStream,
5872                _responses: RpcResponseSink,
5873            ) -> Result<(), RpcHandlerError> {
5874                use futures::StreamExt;
5875                while let Some(chunk) = requests.next().await {
5876                    self.seen.lock().push(chunk);
5877                }
5878                if ctx.cancellation.is_cancelled() {
5879                    self.observed_cancel.store(true, Ordering::SeqCst);
5880                }
5881                Ok(())
5882            }
5883        }
5884        let (emit, _captured) = capturing_async_emitter();
5885        let mut fold = RpcDuplexFold::new(
5886            Arc::new(DuplexCollect {
5887                seen: seen.clone(),
5888                observed_cancel: observed_cancel.clone(),
5889            }),
5890            emit,
5891        );
5892        let req = RpcRequestPayload {
5893            service: "dx".to_string(),
5894            deadline_ns: 0,
5895            flags: FLAG_RPC_CLIENT_STREAMING_REQUEST | FLAG_RPC_STREAMING_RESPONSE,
5896            headers: vec![],
5897            body: Bytes::from_static(b"a"),
5898        };
5899        fold.apply_inbound(&inbound(
5900            VICTIM,
5901            rpc_request_event(ORIGIN, CALL_ID, req).payload,
5902        ))
5903        .unwrap();
5904        assert!(
5905            wait_until(
5906                || fold.sender_keys().contains(&(VICTIM, ORIGIN, CALL_ID)),
5907                Duration::from_secs(1)
5908            )
5909            .await
5910        );
5911        // Attacker forges a chunk + cancel on its own session — miss.
5912        fold.apply_inbound(&inbound(
5913            ATTACKER,
5914            rpc_request_chunk_event(ORIGIN, CALL_ID, 0, b"ATTACK".to_vec()).payload,
5915        ))
5916        .unwrap();
5917        fold.apply_inbound(&inbound(
5918            ATTACKER,
5919            rpc_cancel_event(ORIGIN, CALL_ID).payload,
5920        ))
5921        .unwrap();
5922        tokio::time::sleep(Duration::from_millis(100)).await;
5923        assert!(
5924            fold.sender_keys().contains(&(VICTIM, ORIGIN, CALL_ID)),
5925            "forged frames must not close the victim's duplex upload stream",
5926        );
5927        fold.apply_inbound(&inbound(
5928            VICTIM,
5929            rpc_request_chunk_event(ORIGIN, CALL_ID, 0, b"b".to_vec()).payload,
5930        ))
5931        .unwrap();
5932        fold.apply_inbound(&inbound(
5933            VICTIM,
5934            rpc_request_chunk_event(ORIGIN, CALL_ID, FLAG_RPC_REQUEST_END, b"c".to_vec()).payload,
5935        ))
5936        .unwrap();
5937        assert!(
5938            wait_until(
5939                || !seen.lock().is_empty() && seen.lock().len() >= 3,
5940                Duration::from_secs(2)
5941            )
5942            .await,
5943            "victim's chunks must reach the handler",
5944        );
5945        let bodies: Vec<Vec<u8>> = seen.lock().iter().map(|b| b.to_vec()).collect();
5946        assert_eq!(
5947            bodies,
5948            vec![b"a".to_vec(), b"b".to_vec(), b"c".to_vec()],
5949            "the attacker's forged chunk must never enter the victim's duplex stream",
5950        );
5951        assert!(
5952            !observed_cancel.load(Ordering::SeqCst),
5953            "the attacker's forged CANCEL must not cancel the victim's duplex stream",
5954        );
5955    }
5956
5957    /// Regression: a duplicate REQUEST for an already-in-flight
5958    /// `(origin_hash, call_id)` must be refused with a synthetic
5959    /// `Internal` response and must NOT spawn a second handler.
5960    /// Without the refusal, two handlers race under the same key
5961    /// and CANCEL handling is broken (CANCEL removes the entry
5962    /// the first handler reinserts, etc.).
5963    #[tokio::test]
5964    async fn server_fold_duplicate_request_refuses_without_double_dispatch() {
5965        let invocations = Arc::new(AtomicUsize::new(0));
5966        struct CountingHandler {
5967            invocations: Arc<AtomicUsize>,
5968        }
5969        #[async_trait::async_trait]
5970        impl RpcHandler for CountingHandler {
5971            async fn call(&self, _ctx: RpcContext) -> Result<RpcResponsePayload, RpcHandlerError> {
5972                self.invocations.fetch_add(1, Ordering::SeqCst);
5973                tokio::time::sleep(Duration::from_millis(80)).await;
5974                Ok(RpcResponsePayload {
5975                    status: RpcStatus::Ok,
5976                    headers: vec![],
5977                    body: Bytes::from_static(b"done"),
5978                })
5979            }
5980        }
5981        let (emit, captured) = capturing_emitter();
5982        let mut fold = RpcServerFold::new(
5983            Arc::new(CountingHandler {
5984                invocations: invocations.clone(),
5985            }),
5986            emit,
5987        );
5988        let req = RpcRequestPayload {
5989            service: "x".to_string(),
5990            deadline_ns: 0,
5991            flags: 0,
5992            headers: vec![],
5993            body: Bytes::new(),
5994        };
5995        // First REQUEST — handler spawns and parks in sleep.
5996        fold.apply(&rpc_request_event(1, 99, req.clone()), &mut ())
5997            .unwrap();
5998        assert!(
5999            wait_until(
6000                || fold.in_flight_keys().contains(&(0, 1, 99)),
6001                Duration::from_secs(1)
6002            )
6003            .await
6004        );
6005        // Second REQUEST with same key — must be refused
6006        // synchronously with a synthetic Internal response.
6007        fold.apply(&rpc_request_event(1, 99, req), &mut ()).unwrap();
6008        // The refusal emit happens synchronously in the fold's
6009        // sync emitter path.
6010        let after_dup = captured.lock().clone();
6011        assert_eq!(
6012            after_dup.len(),
6013            1,
6014            "duplicate REQUEST must emit exactly one synthetic refusal",
6015        );
6016        assert_eq!(after_dup[0].2.status, RpcStatus::Internal);
6017        assert!(String::from_utf8_lossy(&after_dup[0].2.body).contains("duplicate"));
6018        // Wait for the first handler to complete.
6019        assert!(
6020            wait_until(|| captured.lock().len() == 2, Duration::from_secs(2)).await,
6021            "first handler should still complete normally"
6022        );
6023        let captured = captured.lock();
6024        assert_eq!(captured.len(), 2);
6025        // The first handler's response is the second emit (Ok).
6026        assert_eq!(captured[1].2.status, RpcStatus::Ok);
6027        assert_eq!(
6028            invocations.load(Ordering::SeqCst),
6029            1,
6030            "duplicate REQUEST must NOT spawn a second handler",
6031        );
6032    }
6033
6034    /// Regression: a CANCEL that fires while the handler is mid-
6035    /// flight must override the handler's outcome with
6036    /// `RpcStatus::Cancelled` even when the handler ignores
6037    /// cancellation and returns `Ok(...)`. Without this, a caller
6038    /// who cancelled would see the handler's accidental success
6039    /// payload and could not tell whether their CANCEL won.
6040    #[tokio::test]
6041    async fn server_fold_cancel_overrides_handler_ok_with_cancelled_status() {
6042        struct IgnoresCancellation;
6043        #[async_trait::async_trait]
6044        impl RpcHandler for IgnoresCancellation {
6045            async fn call(&self, _ctx: RpcContext) -> Result<RpcResponsePayload, RpcHandlerError> {
6046                // Sleep long enough for the test to send CANCEL,
6047                // then return Ok regardless. This models a handler
6048                // that doesn't `select!` on `ctx.cancellation`.
6049                tokio::time::sleep(Duration::from_millis(80)).await;
6050                Ok(RpcResponsePayload {
6051                    status: RpcStatus::Ok,
6052                    headers: vec![],
6053                    body: Bytes::from_static(b"finished despite cancellation"),
6054                })
6055            }
6056        }
6057        let (emit, captured) = capturing_emitter();
6058        let mut fold = RpcServerFold::new(Arc::new(IgnoresCancellation), emit);
6059        let req = RpcRequestPayload {
6060            service: "x".to_string(),
6061            deadline_ns: 0,
6062            flags: 0,
6063            headers: vec![],
6064            body: Bytes::new(),
6065        };
6066        fold.apply(&rpc_request_event(7, 11, req), &mut ()).unwrap();
6067        // Wait until the handler is parked, then send CANCEL well
6068        // before the handler's sleep elapses.
6069        assert!(
6070            wait_until(
6071                || fold.in_flight_keys().contains(&(0, 7, 11)),
6072                Duration::from_secs(1)
6073            )
6074            .await
6075        );
6076        fold.apply(&rpc_cancel_event(7, 11), &mut ()).unwrap();
6077        assert!(
6078            wait_until(|| !captured.lock().is_empty(), Duration::from_secs(2)).await,
6079            "handler should complete and emit response"
6080        );
6081        let captured = captured.lock();
6082        assert_eq!(captured.len(), 1);
6083        let (_, _, resp) = &captured[0];
6084        assert_eq!(
6085            resp.status,
6086            RpcStatus::Cancelled,
6087            "handler that returned Ok despite CANCEL must surface as Cancelled"
6088        );
6089        assert!(fold.in_flight_keys().is_empty());
6090    }
6091
6092    /// CANCEL for an unknown call_id is a no-op (no panic, no
6093    /// stray emission). This is the case where a CANCEL races a
6094    /// handler completion or a duplicate CANCEL arrives.
6095    #[tokio::test]
6096    async fn server_fold_cancel_for_unknown_call_id_is_no_op() {
6097        let (emit, captured) = capturing_emitter();
6098        let mut fold = RpcServerFold::new(Arc::new(EchoHandler), emit);
6099        // CANCEL with no matching REQUEST.
6100        fold.apply(&rpc_cancel_event(1, 999), &mut ()).unwrap();
6101        assert!(captured.lock().is_empty());
6102        assert!(fold.in_flight_keys().is_empty());
6103    }
6104
6105    /// Malformed request payload: fold emits a
6106    /// `RpcStatus::UnknownVersion` response and continues. A
6107    /// regression that returned `Err` here would kill the cortex
6108    /// adapter's tail-and-fold task on the first malformed event,
6109    /// which is the wrong behavior for an RPC server that needs
6110    /// to keep serving past garbage.
6111    #[tokio::test]
6112    async fn server_fold_malformed_payload_emits_unknown_version_and_keeps_going() {
6113        let (emit, captured) = capturing_emitter();
6114        let mut fold = RpcServerFold::new(Arc::new(EchoHandler), emit);
6115        // Build an event with valid meta but a garbage tail (just
6116        // a single 0x00 byte, which fails the service-len check).
6117        let meta = EventMeta::new(DISPATCH_RPC_REQUEST, 0, 7, 1, 0);
6118        let mut buf = Vec::new();
6119        buf.extend_from_slice(&meta.to_bytes());
6120        // OA2-E0.2: RpcRouteV1 route placeholder — these test frames
6121        // feed the folds directly (no ingress select), and the folds
6122        // skip the route to reach the payload at RPC_FRAME_BODY_OFFSET.
6123        encode_rpc_route(&mut buf, 0);
6124        buf.push(0x00); // svc_len = 0 → empty service → Truncated
6125        let ev = RedexEvent {
6126            entry: RedexEntry::new_heap(0, 0, buf.len() as u32, 0, 0),
6127            payload: bytes::Bytes::from(buf),
6128        };
6129        let result = fold.apply(&ev, &mut ());
6130        assert!(
6131            result.is_ok(),
6132            "fold must NOT return Err on malformed payload (would kill the adapter); got {result:?}"
6133        );
6134        let captured = captured.lock();
6135        assert_eq!(captured.len(), 1);
6136        let (_, _, resp) = &captured[0];
6137        assert_eq!(resp.status, RpcStatus::UnknownVersion);
6138    }
6139
6140    /// Cancellation token roundtrip: `cancel()` sets `is_cancelled`
6141    /// and wakes a parked `cancelled().await`.
6142    #[tokio::test]
6143    async fn cancellation_token_signals_waiters() {
6144        let token = RpcCancellationToken::new();
6145        assert!(!token.is_cancelled());
6146        let token2 = token.clone();
6147        let waiter = tokio::spawn(async move {
6148            token2.cancelled().await;
6149        });
6150        // Give the waiter a chance to park.
6151        tokio::time::sleep(Duration::from_millis(10)).await;
6152        token.cancel();
6153        // Waiter wakes.
6154        tokio::time::timeout(Duration::from_secs(1), waiter)
6155            .await
6156            .expect("waiter must wake within 1s")
6157            .expect("waiter task must not panic");
6158        assert!(token.is_cancelled());
6159    }
6160
6161    // ====================================================================
6162    // W3C Trace Context propagation.
6163    // ====================================================================
6164
6165    /// `build_trace_headers` + `extract_trace_context` round-trip
6166    /// a typical W3C trace context through the request headers.
6167    #[test]
6168    fn trace_context_round_trips_through_headers() {
6169        let tc = TraceContext {
6170            traceparent: "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01".to_string(),
6171            tracestate: "vendor1=opaque-value,vendor2=other".to_string(),
6172        };
6173        let headers = build_trace_headers(&tc);
6174        assert_eq!(headers.len(), 2, "non-empty tracestate emits both headers");
6175        let extracted = extract_trace_context(&headers).expect("must extract");
6176        assert_eq!(extracted, tc);
6177    }
6178
6179    /// Regression for M21: `extract_trace_context` does
6180    /// case-INsensitive matching on the header names, matching the
6181    /// W3C and HTTP conventions. A peer that emits capitalized
6182    /// `Traceparent` or `TRACESTATE` must still be picked up — the
6183    /// previous implementation used `name.as_str() == "traceparent"`
6184    /// and silently dropped any non-lowercase variant.
6185    #[test]
6186    fn extract_trace_context_is_case_insensitive_on_header_names() {
6187        // Capital-T traceparent + uppercase TRACESTATE — both must
6188        // be picked up by the extractor.
6189        let headers = vec![
6190            (
6191                "Traceparent".to_string(),
6192                b"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01".to_vec(),
6193            ),
6194            ("TRACESTATE".to_string(), b"vendor=value".to_vec()),
6195        ];
6196        let extracted =
6197            extract_trace_context(&headers).expect("capital-T traceparent must be recognized");
6198        assert_eq!(
6199            extracted.traceparent,
6200            "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
6201        );
6202        assert_eq!(extracted.tracestate, "vendor=value");
6203
6204        // Mixed-case still works.
6205        let headers = vec![
6206            ("traceParent".to_string(), b"00-aa-bb-01".to_vec()),
6207            ("TraceState".to_string(), b"v=1".to_vec()),
6208        ];
6209        let extracted =
6210            extract_trace_context(&headers).expect("mixed-case traceparent must be recognized");
6211        assert_eq!(extracted.traceparent, "00-aa-bb-01");
6212        assert_eq!(extracted.tracestate, "v=1");
6213    }
6214
6215    /// Empty `tracestate` is omitted on the wire (W3C convention)
6216    /// but extracted as empty on the receive side.
6217    #[test]
6218    fn trace_context_empty_tracestate_omitted_from_wire() {
6219        let tc = TraceContext {
6220            traceparent: "00-aa-bb-01".to_string(),
6221            tracestate: String::new(),
6222        };
6223        let headers = build_trace_headers(&tc);
6224        assert_eq!(
6225            headers.len(),
6226            1,
6227            "empty tracestate must NOT be emitted on the wire",
6228        );
6229        assert_eq!(headers[0].0, "traceparent");
6230        let extracted = extract_trace_context(&headers).expect("must extract");
6231        assert_eq!(extracted.traceparent, "00-aa-bb-01");
6232        assert_eq!(extracted.tracestate, "");
6233    }
6234
6235    /// Headers without `traceparent` decode as `None`. Useful for
6236    /// the FLAG_RPC_PROPAGATE_TRACE-set-but-no-headers misuse
6237    /// case — the server gets `None` rather than a bogus context.
6238    #[test]
6239    fn trace_context_missing_traceparent_returns_none() {
6240        let headers = vec![
6241            ("content-type".to_string(), b"application/json".to_vec()),
6242            ("idempotency-key".to_string(), b"abc".to_vec()),
6243        ];
6244        assert!(extract_trace_context(&headers).is_none());
6245    }
6246
6247    /// Server fold populates `RpcContext::trace_context` only when
6248    /// the caller signals `FLAG_RPC_PROPAGATE_TRACE`. End-to-end
6249    /// through the fold's apply path.
6250    #[tokio::test]
6251    async fn server_fold_propagates_trace_context_via_flag() {
6252        struct CapturingHandler {
6253            captured: Arc<Mutex<Option<Option<TraceContext>>>>,
6254        }
6255        #[async_trait::async_trait]
6256        impl RpcHandler for CapturingHandler {
6257            async fn call(&self, ctx: RpcContext) -> Result<RpcResponsePayload, RpcHandlerError> {
6258                *self.captured.lock() = Some(ctx.trace_context.clone());
6259                Ok(RpcResponsePayload {
6260                    status: RpcStatus::Ok,
6261                    headers: vec![],
6262                    body: Bytes::new(),
6263                })
6264            }
6265        }
6266
6267        // Helper: run one request through a fresh fold and return
6268        // what the handler captured for trace_context.
6269        async fn run(req: RpcRequestPayload) -> Option<TraceContext> {
6270            let captured: Arc<Mutex<Option<Option<TraceContext>>>> = Arc::new(Mutex::new(None));
6271            let (emit, _captured_responses) = capturing_emitter();
6272            let handler = Arc::new(CapturingHandler {
6273                captured: captured.clone(),
6274            });
6275            let mut fold = RpcServerFold::new(handler, emit);
6276            fold.apply(&rpc_request_event(1, 1, req), &mut ()).unwrap();
6277            // Wait for the spawned handler to finish.
6278            assert!(
6279                wait_until(|| captured.lock().is_some(), Duration::from_secs(2)).await,
6280                "handler must run"
6281            );
6282            let observed = captured.lock().take().unwrap();
6283            observed
6284        }
6285
6286        // Case 1: FLAG_RPC_PROPAGATE_TRACE NOT set → trace_context is None.
6287        let req_no_flag = RpcRequestPayload {
6288            service: "x".to_string(),
6289            deadline_ns: 0,
6290            flags: 0,
6291            headers: vec![("traceparent".to_string(), b"00-aa-bb-01".to_vec())],
6292            body: Bytes::new(),
6293        };
6294        assert!(
6295            run(req_no_flag).await.is_none(),
6296            "without the flag, server must NOT extract trace_context"
6297        );
6298
6299        // Case 2: FLAG set + headers present → server gets the context.
6300        let tc = TraceContext {
6301            traceparent: "00-trace-span-01".to_string(),
6302            tracestate: "vendor=value".to_string(),
6303        };
6304        let req_with_flag = RpcRequestPayload {
6305            service: "x".to_string(),
6306            deadline_ns: 0,
6307            flags: FLAG_RPC_PROPAGATE_TRACE,
6308            headers: build_trace_headers(&tc),
6309            body: Bytes::new(),
6310        };
6311        let observed = run(req_with_flag).await.expect("flag set → should be Some");
6312        assert_eq!(observed, tc);
6313
6314        // Case 3: FLAG set but headers missing → None (defensive).
6315        let req_flag_no_headers = RpcRequestPayload {
6316            service: "x".to_string(),
6317            deadline_ns: 0,
6318            flags: FLAG_RPC_PROPAGATE_TRACE,
6319            headers: vec![],
6320            body: Bytes::new(),
6321        };
6322        assert!(
6323            run(req_flag_no_headers).await.is_none(),
6324            "flag set but no headers → server gets None (no synthesis)"
6325        );
6326    }
6327
6328    /// Race: cancel fires AFTER the future is registered but
6329    /// BEFORE the await actually parks. The token's
6330    /// `notified()`-then-check ordering must catch this case
6331    /// without sleeping past the cancellation.
6332    #[tokio::test]
6333    async fn cancellation_token_does_not_miss_cancel_racing_register() {
6334        for _ in 0..50 {
6335            let token = RpcCancellationToken::new();
6336            let token2 = token.clone();
6337            let waiter = tokio::spawn(async move {
6338                token2.cancelled().await;
6339            });
6340            // No sleep — fire cancel as fast as possible against
6341            // the just-spawned waiter. In the worst case the
6342            // waiter has not yet reached `notified()`; it will see
6343            // `is_cancelled() == true` on its first check and
6344            // return immediately. In the other case it parks and
6345            // gets woken by `notify_waiters`.
6346            token.cancel();
6347            tokio::time::timeout(Duration::from_secs(1), waiter)
6348                .await
6349                .expect("waiter must complete within 1s")
6350                .expect("waiter task must not panic");
6351        }
6352    }
6353
6354    // ====================================================================
6355    // RpcClientFold — caller-side response routing.
6356    // ====================================================================
6357
6358    fn rpc_response_event(
6359        caller_origin: u64,
6360        call_id: u64,
6361        payload: RpcResponsePayload,
6362    ) -> RedexEvent {
6363        let meta = EventMeta::new(DISPATCH_RPC_RESPONSE, 0, caller_origin, call_id, 0);
6364        let mut buf = Vec::new();
6365        buf.extend_from_slice(&meta.to_bytes());
6366        // OA2-E0.2: RpcRouteV1 route placeholder — these test frames
6367        // feed the folds directly (no ingress select), and the folds
6368        // skip the route to reach the payload at RPC_FRAME_BODY_OFFSET.
6369        encode_rpc_route(&mut buf, 0);
6370        buf.extend_from_slice(&payload.encode());
6371        RedexEvent {
6372            entry: RedexEntry::new_heap(0, 0, buf.len() as u32, 0, 0),
6373            payload: bytes::Bytes::from(buf),
6374        }
6375    }
6376
6377    /// Happy path: register a call, drive the matching RESPONSE
6378    /// through the fold, the awaiting receiver gets the payload.
6379    #[tokio::test]
6380    async fn client_fold_routes_response_to_registered_waiter() {
6381        let pending = Arc::new(RpcClientPending::new());
6382        let mut fold = RpcClientFold::new(pending.clone());
6383        let rx = pending.register(42, 0);
6384        assert_eq!(pending.pending_count(), 1);
6385
6386        let resp = RpcResponsePayload {
6387            status: RpcStatus::Ok,
6388            headers: vec![],
6389            body: Bytes::from_static(b"hello back"),
6390        };
6391        fold.apply(&rpc_response_event(0xCAFE, 42, resp.clone()), &mut ())
6392            .unwrap();
6393
6394        // Receiver is completed.
6395        let got = tokio::time::timeout(Duration::from_secs(1), rx)
6396            .await
6397            .expect("receiver must complete within 1s")
6398            .expect("sender must not be dropped");
6399        assert_eq!(got, resp);
6400        // Pending entry cleared after delivery.
6401        assert_eq!(pending.pending_count(), 0);
6402    }
6403
6404    /// RESPONSE for an unknown call_id is a no-op (no panic, no
6405    /// stray side effect). This is the case where a stale RESPONSE
6406    /// arrives after the caller has cancelled or timed out.
6407    #[tokio::test]
6408    async fn client_fold_response_for_unknown_call_id_is_no_op() {
6409        let pending = Arc::new(RpcClientPending::new());
6410        let mut fold = RpcClientFold::new(pending.clone());
6411        let resp = RpcResponsePayload {
6412            status: RpcStatus::Ok,
6413            headers: vec![],
6414            body: Bytes::new(),
6415        };
6416        fold.apply(&rpc_response_event(1, 999, resp), &mut ())
6417            .unwrap();
6418        assert_eq!(pending.pending_count(), 0);
6419    }
6420
6421    /// REQUEST / CANCEL events on the reply channel are ignored
6422    /// rather than producing a stray decode-error or affecting
6423    /// pending state. The reply channel shouldn't carry these in
6424    /// practice (they belong on `<service>.requests`), but a
6425    /// misconfigured publisher must not break the fold.
6426    #[tokio::test]
6427    async fn client_fold_ignores_non_response_dispatches() {
6428        let pending = Arc::new(RpcClientPending::new());
6429        let mut fold = RpcClientFold::new(pending.clone());
6430        let _rx = pending.register(7, 0);
6431
6432        // REQUEST event landing on the caller's reply channel is
6433        // ignored.
6434        let req = RpcRequestPayload {
6435            service: "stray".to_string(),
6436            deadline_ns: 0,
6437            flags: 0,
6438            headers: vec![],
6439            body: Bytes::new(),
6440        };
6441        fold.apply(&rpc_request_event(1, 7, req), &mut ()).unwrap();
6442        // Pending entry untouched.
6443        assert_eq!(pending.pending_count(), 1);
6444
6445        // CANCEL on the reply channel: also ignored.
6446        fold.apply(&rpc_cancel_event(1, 7), &mut ()).unwrap();
6447        assert_eq!(pending.pending_count(), 1);
6448    }
6449
6450    /// `cancel(call_id)` removes the pending entry; a subsequent
6451    /// RESPONSE for that call_id is dropped silently.
6452    #[tokio::test]
6453    async fn client_pending_cancel_drops_subsequent_response() {
6454        let pending = Arc::new(RpcClientPending::new());
6455        let mut fold = RpcClientFold::new(pending.clone());
6456        let rx = pending.register(5, 0);
6457        pending.cancel(5);
6458        assert_eq!(pending.pending_count(), 0);
6459
6460        let resp = RpcResponsePayload {
6461            status: RpcStatus::Ok,
6462            headers: vec![],
6463            body: Bytes::new(),
6464        };
6465        fold.apply(&rpc_response_event(1, 5, resp), &mut ())
6466            .unwrap();
6467
6468        // Receiver was dropped along with the cancel. The previously-
6469        // returned `rx` errors with `Closed`.
6470        let result = tokio::time::timeout(Duration::from_secs(1), rx).await;
6471        let inner = result.expect("must complete within 1s");
6472        assert!(
6473            inner.is_err(),
6474            "receiver after cancel must error (sender dropped)",
6475        );
6476    }
6477
6478    /// Malformed RESPONSE payload: fold returns Ok (does not kill
6479    /// the cortex adapter) and leaves the pending entry intact for
6480    /// the caller's deadline / cancellation path to clean up. Pre-
6481    /// fix a bad payload could either kill the fold or fabricate a
6482    /// synthetic response — both wrong.
6483    #[tokio::test]
6484    async fn client_fold_malformed_response_is_logged_not_fatal() {
6485        let pending = Arc::new(RpcClientPending::new());
6486        let mut fold = RpcClientFold::new(pending.clone());
6487        let rx = pending.register(11, 0);
6488
6489        // Build a malformed RESPONSE: valid meta, garbage tail
6490        // (just `[0xFF]`, which is shorter than the required 2-byte
6491        // status + 1-byte headers count + 4-byte body length).
6492        let meta = EventMeta::new(DISPATCH_RPC_RESPONSE, 0, 1, 11, 0);
6493        let mut buf = Vec::new();
6494        buf.extend_from_slice(&meta.to_bytes());
6495        // OA2-E0.2: RpcRouteV1 route placeholder — these test frames
6496        // feed the folds directly (no ingress select), and the folds
6497        // skip the route to reach the payload at RPC_FRAME_BODY_OFFSET.
6498        encode_rpc_route(&mut buf, 0);
6499        buf.push(0xFF);
6500        let ev = RedexEvent {
6501            entry: RedexEntry::new_heap(0, 0, buf.len() as u32, 0, 0),
6502            payload: bytes::Bytes::from(buf),
6503        };
6504        let result = fold.apply(&ev, &mut ());
6505        assert!(
6506            result.is_ok(),
6507            "fold must not return Err on malformed response"
6508        );
6509        // Pending entry NOT cleared — the caller's cancellation
6510        // path will eventually clean it up via `cancel(call_id)`.
6511        assert_eq!(pending.pending_count(), 1);
6512        // Receiver is still pending (not delivered, not closed).
6513        assert!(
6514            tokio::time::timeout(Duration::from_millis(50), rx)
6515                .await
6516                .is_err(),
6517            "receiver should still be parked (no delivery, no drop)",
6518        );
6519    }
6520
6521    /// Re-registering the same call_id replaces the prior sender;
6522    /// the prior `Receiver` errors with `RecvError::Closed`. This
6523    /// is the misuse-detection path — call_ids should be unique
6524    /// per (caller, target) for the lifetime of the call, and a
6525    /// clash surfaces as a hard error rather than silently
6526    /// delivering the response to the wrong waiter.
6527    #[tokio::test]
6528    async fn client_pending_re_register_closes_prior_receiver() {
6529        let pending = Arc::new(RpcClientPending::new());
6530        let rx_a = pending.register(99, 0);
6531        let _rx_b = pending.register(99, 0);
6532        // The first receiver is now closed (sender dropped on
6533        // re-insert).
6534        let result = tokio::time::timeout(Duration::from_secs(1), rx_a).await;
6535        let inner = result.expect("must complete within 1s");
6536        assert!(inner.is_err(), "re-register must close prior receiver");
6537        assert_eq!(pending.pending_count(), 1);
6538    }
6539
6540    /// S-4 part 2 regression: a RESPONSE whose wire `from_node`
6541    /// doesn't match the recorded `target_node` must not resolve
6542    /// the call. Without the gate, any peer with publish access
6543    /// to the caller's reply channel could ship a spoofed
6544    /// response (random call_ids from S-4 part 1 narrow the
6545    /// attack surface, but this gate closes the residual case
6546    /// of an attacker who has observed the victim's call_id via
6547    /// some side channel).
6548    #[tokio::test]
6549    async fn client_pending_drops_response_from_wrong_target() {
6550        let pending = Arc::new(RpcClientPending::new());
6551        let rx = pending.register(0xDEAD_BEEF, 0x42);
6552        let resp = RpcResponsePayload {
6553            status: RpcStatus::Ok,
6554            headers: Vec::new(),
6555            body: Bytes::from_static(b"forged"),
6556        };
6557        // Forged from a different session peer — must drop.
6558        pending.deliver(0xDEAD_BEEF, 0x99, resp.clone());
6559        // Receiver is still parked; pending entry is intact.
6560        let parked = tokio::time::timeout(Duration::from_millis(50), rx).await;
6561        assert!(
6562            parked.is_err(),
6563            "forged RESPONSE from wrong target must not resolve the call"
6564        );
6565        assert_eq!(pending.pending_count(), 1);
6566
6567        // Legitimate RESPONSE from the recorded target resolves.
6568        let rx2 = pending.register(0xCAFE, 0x42);
6569        let ok_resp = RpcResponsePayload {
6570            status: RpcStatus::Ok,
6571            headers: Vec::new(),
6572            body: Bytes::from_static(b"ok"),
6573        };
6574        pending.deliver(0xCAFE, 0x42, ok_resp);
6575        let delivered = tokio::time::timeout(Duration::from_millis(50), rx2)
6576            .await
6577            .expect("must complete")
6578            .expect("must receive");
6579        assert_eq!(delivered.body.as_ref(), b"ok");
6580    }
6581
6582    // ====================================================================
6583    // Phase C — RpcClientPending + RpcClientFold for client-streaming.
6584    // ====================================================================
6585
6586    /// Build a REQUEST_GRANT event for tests. Mirror of
6587    /// `rpc_stream_grant_event` for the request direction.
6588    fn rpc_request_grant_event(caller_origin: u64, call_id: u64, credits: u32) -> RedexEvent {
6589        let meta = EventMeta::new(DISPATCH_RPC_REQUEST_GRANT, 0, caller_origin, call_id, 0);
6590        let mut buf = Vec::with_capacity(EVENT_META_SIZE + 12);
6591        buf.extend_from_slice(&meta.to_bytes());
6592        // OA2-E0.2: RpcRouteV1 route placeholder — these test frames
6593        // feed the folds directly (no ingress select), and the folds
6594        // skip the route to reach the payload at RPC_FRAME_BODY_OFFSET.
6595        encode_rpc_route(&mut buf, 0);
6596        buf.extend_from_slice(&encode_request_grant(call_id, credits));
6597        RedexEvent {
6598            entry: RedexEntry::new_heap(0, 0, buf.len() as u32, 0, 0),
6599            payload: bytes::Bytes::from(buf),
6600        }
6601    }
6602
6603    /// `register_client_streaming` returns two halves: a terminal
6604    /// oneshot and a grant mpsc. A terminal RESPONSE resolves the
6605    /// oneshot (same shape as unary delivery); a REQUEST_GRANT
6606    /// for the same call_id pushes its credit onto the mpsc.
6607    #[tokio::test]
6608    async fn client_pending_client_streaming_routes_terminal_and_grants() {
6609        let pending = Arc::new(RpcClientPending::new());
6610        let (terminal_rx, mut grant_rx) = pending.register_client_streaming(0xCAFE_F00D, 0);
6611        // Push two grants — both should land on the mpsc.
6612        pending.deliver_grant(0xCAFE_F00D, 0, 3);
6613        pending.deliver_grant(0xCAFE_F00D, 0, 7);
6614        assert_eq!(grant_rx.recv().await, Some(3));
6615        assert_eq!(grant_rx.recv().await, Some(7));
6616        // Terminal RESPONSE resolves the oneshot and removes the
6617        // entry. Grant mpsc closes too (its sender drops with
6618        // the entry).
6619        let resp = RpcResponsePayload {
6620            status: RpcStatus::Ok,
6621            headers: vec![],
6622            body: Bytes::from_static(b"done"),
6623        };
6624        pending.deliver(0xCAFE_F00D, 0, resp.clone());
6625        let delivered = tokio::time::timeout(Duration::from_millis(50), terminal_rx)
6626            .await
6627            .expect("terminal must complete")
6628            .expect("terminal must receive");
6629        assert_eq!(delivered.body.as_ref(), b"done");
6630        // Grant mpsc now closed.
6631        assert_eq!(grant_rx.recv().await, None);
6632        assert_eq!(pending.pending_count(), 0);
6633    }
6634
6635    /// REQUEST_GRANT from a non-target session peer is dropped
6636    /// without injecting credit. Same S-4-style binding gate as
6637    /// the RESPONSE delivery path — a forged grant on a shared
6638    /// reply channel can't inflate a victim's credit budget.
6639    #[tokio::test]
6640    async fn client_pending_grant_from_wrong_target_is_dropped() {
6641        let pending = Arc::new(RpcClientPending::new());
6642        let (_terminal_rx, mut grant_rx) = pending.register_client_streaming(0xCAFE_F00D, 0x42);
6643        // Forged grant from a different session peer — must drop.
6644        pending.deliver_grant(0xCAFE_F00D, 0x99, 100);
6645        let parked = tokio::time::timeout(Duration::from_millis(50), grant_rx.recv()).await;
6646        assert!(
6647            parked.is_err(),
6648            "forged REQUEST_GRANT from wrong target must not inject credit"
6649        );
6650        // Legitimate grant from the recorded target lands.
6651        pending.deliver_grant(0xCAFE_F00D, 0x42, 5);
6652        let delivered = tokio::time::timeout(Duration::from_millis(50), grant_rx.recv())
6653            .await
6654            .expect("must complete")
6655            .expect("must receive");
6656        assert_eq!(delivered, 5);
6657    }
6658
6659    /// `deliver_grant` for an unknown call_id is a silent no-op.
6660    /// Same harmless-drop semantics as a STREAM_GRANT for an
6661    /// unknown / non-flow-controlled call (CANCEL/GRANT race is
6662    /// always possible).
6663    #[tokio::test]
6664    async fn client_pending_grant_for_unknown_call_id_is_no_op() {
6665        let pending = Arc::new(RpcClientPending::new());
6666        // No entry registered for this call_id.
6667        pending.deliver_grant(0xDEAD, 0, 42);
6668        // No panics, no entries created.
6669        assert_eq!(pending.pending_count(), 0);
6670    }
6671
6672    /// `deliver_grant` for a unary entry is silently dropped
6673    /// (grants only apply to client-streaming / duplex calls).
6674    #[tokio::test]
6675    async fn client_pending_grant_for_unary_entry_is_no_op() {
6676        let pending = Arc::new(RpcClientPending::new());
6677        let _rx = pending.register(0xDEAD, 0);
6678        pending.deliver_grant(0xDEAD, 0, 42);
6679        // No state changes — entry still pending, no leak.
6680        assert_eq!(pending.pending_count(), 1);
6681    }
6682
6683    /// `RpcClientFold::apply` (legacy / loopback path) routes
6684    /// DISPATCH_RPC_REQUEST_GRANT events through to the matching
6685    /// ClientStreaming entry's grant mpsc. Pins the second
6686    /// dispatch arm the fold gained for Phase C.
6687    #[tokio::test]
6688    async fn client_fold_routes_request_grant_to_registered_waiter() {
6689        let pending = Arc::new(RpcClientPending::new());
6690        let mut fold = RpcClientFold::new(pending.clone());
6691        let (_terminal_rx, mut grant_rx) = pending.register_client_streaming(0xC0DE, 0);
6692        let ev = rpc_request_grant_event(0xCAFE, 0xC0DE, 9);
6693        fold.apply(&ev, &mut ()).unwrap();
6694        let delivered = tokio::time::timeout(Duration::from_millis(50), grant_rx.recv())
6695            .await
6696            .expect("must complete")
6697            .expect("must receive");
6698        assert_eq!(delivered, 9);
6699    }
6700
6701    /// `RpcClientFold::apply` ignores REQUEST_GRANT events whose
6702    /// payload is malformed (wrong length): no panic, no entry
6703    /// state change, fold returns Ok and keeps going. Mirror of
6704    /// the response-side malformed-payload regression.
6705    #[tokio::test]
6706    async fn client_fold_malformed_request_grant_is_logged_not_fatal() {
6707        let pending = Arc::new(RpcClientPending::new());
6708        let mut fold = RpcClientFold::new(pending.clone());
6709        let (_terminal_rx, mut grant_rx) = pending.register_client_streaming(0xC0DE, 0);
6710        // Build a GRANT event whose payload is only 4 bytes
6711        // (truncated — codec needs 12).
6712        let meta = EventMeta::new(DISPATCH_RPC_REQUEST_GRANT, 0, 0xCAFE, 0xC0DE, 0);
6713        let mut buf = Vec::new();
6714        buf.extend_from_slice(&meta.to_bytes());
6715        // OA2-E0.2: RpcRouteV1 route placeholder — these test frames
6716        // feed the folds directly (no ingress select), and the folds
6717        // skip the route to reach the payload at RPC_FRAME_BODY_OFFSET.
6718        encode_rpc_route(&mut buf, 0);
6719        buf.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD]);
6720        let ev = RedexEvent {
6721            entry: RedexEntry::new_heap(0, 0, buf.len() as u32, 0, 0),
6722            payload: bytes::Bytes::from(buf),
6723        };
6724        let result = fold.apply(&ev, &mut ());
6725        assert!(
6726            result.is_ok(),
6727            "malformed REQUEST_GRANT must NOT kill the fold"
6728        );
6729        // No credit landed on the mpsc.
6730        let parked = tokio::time::timeout(Duration::from_millis(30), grant_rx.recv()).await;
6731        assert!(
6732            parked.is_err(),
6733            "malformed REQUEST_GRANT must not inject credit"
6734        );
6735    }
6736
6737    /// REQUEST_GRANT frames where the payload `call_id` does NOT
6738    /// agree with `EventMeta::seq_or_ts` must be dropped: the
6739    /// producer is contracted to encode both fields to the same
6740    /// value (see `RpcRequestGrantPayload::call_id` doc), so a
6741    /// mismatch is either a malformed frame or an attempted
6742    /// cross-call credit-injection. Without this check, a peer
6743    /// could publish a GRANT whose meta names one call but whose
6744    /// payload credits a different in-flight call_id.
6745    ///
6746    /// Regression: cubic-dev-ai bot P2 review comment on the
6747    /// `nrpc-streaming` branch.
6748    #[tokio::test]
6749    async fn client_fold_drops_request_grant_with_mismatched_call_ids() {
6750        let pending = Arc::new(RpcClientPending::new());
6751        let mut fold = RpcClientFold::new(pending.clone());
6752        let (_terminal_rx_victim, mut grant_rx_victim) =
6753            pending.register_client_streaming(0xC0DE, 0);
6754        let (_terminal_rx_other, mut grant_rx_other) = pending.register_client_streaming(0xBEEF, 0);
6755
6756        // Build a hand-rolled frame: meta names call 0xC0DE,
6757        // payload encodes credit for call 0xBEEF. Either the
6758        // producer is broken or this is a forged frame; the
6759        // consumer must drop, not deliver.
6760        let meta = EventMeta::new(DISPATCH_RPC_REQUEST_GRANT, 0, 0xCAFE, 0xC0DE, 0);
6761        let mut buf = Vec::with_capacity(EVENT_META_SIZE + 12);
6762        buf.extend_from_slice(&meta.to_bytes());
6763        // OA2-E0.2: RpcRouteV1 route placeholder — these test frames
6764        // feed the folds directly (no ingress select), and the folds
6765        // skip the route to reach the payload at RPC_FRAME_BODY_OFFSET.
6766        encode_rpc_route(&mut buf, 0);
6767        buf.extend_from_slice(&encode_request_grant(0xBEEF, 5));
6768        let ev = RedexEvent {
6769            entry: RedexEntry::new_heap(0, 0, buf.len() as u32, 0, 0),
6770            payload: bytes::Bytes::from(buf),
6771        };
6772        fold.apply(&ev, &mut ()).unwrap();
6773
6774        let parked_victim =
6775            tokio::time::timeout(Duration::from_millis(30), grant_rx_victim.recv()).await;
6776        assert!(
6777            parked_victim.is_err(),
6778            "mismatched REQUEST_GRANT must not credit the call named in meta",
6779        );
6780        let parked_other =
6781            tokio::time::timeout(Duration::from_millis(30), grant_rx_other.recv()).await;
6782        assert!(
6783            parked_other.is_err(),
6784            "mismatched REQUEST_GRANT must not credit the call named in payload either",
6785        );
6786    }
6787
6788    // ====================================================================
6789    // RpcServerStreamingFold — coverage for the multi-fire emit path.
6790    //
6791    // The streaming fold is the most complex code in this file:
6792    //   - Per-call cancellation token (same as unary)
6793    //   - Pump task that drains an mpsc and awaits each emit to
6794    //     enforce per-call ordering
6795    //   - Optional flow-control semaphore (caller-set window +
6796    //     STREAM_GRANT credit refills)
6797    //   - Terminal-frame emission with CANCEL-wins override
6798    //
6799    // These tests pin each branch: ordered chunks + clean EOF;
6800    // application error after partial stream; panic surfacing as
6801    // Internal; CANCEL flipping the cancellation token AND being
6802    // surfaced as the terminal status; STREAM_GRANT permits;
6803    // duplicate-REQUEST refusal.
6804    // ====================================================================
6805
6806    /// Build an async `RpcAsyncResponseEmitter` that captures every
6807    /// emit into a shared Vec. Streaming fold tests use this to
6808    /// inspect the multi-frame emit pattern.
6809    fn capturing_async_emitter() -> (RpcAsyncResponseEmitter, CapturedResponses) {
6810        let captured: CapturedResponses = Arc::new(Mutex::new(Vec::new()));
6811        let captured_clone = captured.clone();
6812        let emit: RpcAsyncResponseEmitter = Arc::new(move |_from_node, origin, call_id, resp| {
6813            let captured_clone = captured_clone.clone();
6814            Box::pin(async move {
6815                captured_clone.lock().push((origin, call_id, resp));
6816            })
6817        });
6818        (emit, captured)
6819    }
6820
6821    /// Synthesize a STREAM_GRANT event for a `(caller_origin, call_id)`
6822    /// asking for `n` additional credits.
6823    fn rpc_stream_grant_event(caller_origin: u64, call_id: u64, n: u32) -> RedexEvent {
6824        let meta = EventMeta::new(DISPATCH_RPC_STREAM_GRANT, 0, caller_origin, call_id, 0);
6825        let mut buf = Vec::with_capacity(EVENT_META_SIZE + 4);
6826        buf.extend_from_slice(&meta.to_bytes());
6827        // OA2-E0.2: RpcRouteV1 route placeholder — these test frames
6828        // feed the folds directly (no ingress select), and the folds
6829        // skip the route to reach the payload at RPC_FRAME_BODY_OFFSET.
6830        encode_rpc_route(&mut buf, 0);
6831        buf.extend_from_slice(&encode_stream_grant(n));
6832        RedexEvent {
6833            entry: RedexEntry::new_heap(0, 0, buf.len() as u32, 0, 0),
6834            payload: bytes::Bytes::from(buf),
6835        }
6836    }
6837
6838    /// Streaming handler that emits N chunks and returns Ok. The
6839    /// caller-side test asserts (a) all N chunks arrive in order
6840    /// with the `nrpc-streaming: continue` header, (b) a final
6841    /// terminal frame with `nrpc-streaming: end` follows.
6842    #[tokio::test]
6843    async fn streaming_fold_emits_chunks_in_order_and_clean_terminal() {
6844        struct CountingHandler {
6845            n: usize,
6846        }
6847        #[async_trait::async_trait]
6848        impl RpcStreamingHandler for CountingHandler {
6849            async fn call(
6850                &self,
6851                _ctx: RpcContext,
6852                sink: RpcResponseSink,
6853            ) -> Result<(), RpcHandlerError> {
6854                for i in 0..self.n {
6855                    sink.send(format!("chunk-{i}").into_bytes());
6856                }
6857                Ok(())
6858            }
6859        }
6860        let (emit, captured) = capturing_async_emitter();
6861        let mut fold = RpcServerStreamingFold::new(Arc::new(CountingHandler { n: 5 }), emit);
6862        let req = RpcRequestPayload {
6863            service: "stream".to_string(),
6864            deadline_ns: 0,
6865            flags: FLAG_RPC_STREAMING_RESPONSE,
6866            headers: vec![],
6867            body: Bytes::new(),
6868        };
6869        fold.apply(&rpc_request_event(11, 22, req), &mut ())
6870            .unwrap();
6871        // 5 continue chunks + 1 terminal end frame.
6872        assert!(
6873            wait_until(|| captured.lock().len() == 6, Duration::from_secs(2)).await,
6874            "expected 6 frames (5 chunks + terminal end), got {}",
6875            captured.lock().len(),
6876        );
6877        let captured = captured.lock();
6878        for (i, (_, _, resp)) in captured.iter().take(5).enumerate() {
6879            assert_eq!(resp.status, RpcStatus::Ok);
6880            // continue header on every non-terminal chunk
6881            let hdr = resp
6882                .headers
6883                .iter()
6884                .find(|(n, _)| n == HEADER_NRPC_STREAMING)
6885                .expect("streaming header present");
6886            assert_eq!(hdr.1.as_slice(), HEADER_NRPC_STREAMING_CONTINUE);
6887            assert_eq!(resp.body, format!("chunk-{i}").into_bytes());
6888        }
6889        // Terminal frame
6890        let (_, _, term) = captured.last().unwrap();
6891        assert_eq!(term.status, RpcStatus::Ok);
6892        let hdr = term
6893            .headers
6894            .iter()
6895            .find(|(n, _)| n == HEADER_NRPC_STREAMING)
6896            .expect("terminal must have streaming header");
6897        assert_eq!(hdr.1.as_slice(), HEADER_NRPC_STREAMING_END);
6898        assert!(term.body.is_empty());
6899    }
6900
6901    /// Handler returns `Err(Internal)` after sending 2 chunks. Caller
6902    /// must see (a) both chunks with the continue header, (b) a
6903    /// terminal frame carrying `RpcStatus::Internal` (NOT the end
6904    /// marker — the terminal-error path drops the header).
6905    #[tokio::test]
6906    async fn streaming_fold_terminal_error_after_partial_stream() {
6907        struct PartialErrHandler;
6908        #[async_trait::async_trait]
6909        impl RpcStreamingHandler for PartialErrHandler {
6910            async fn call(
6911                &self,
6912                _ctx: RpcContext,
6913                sink: RpcResponseSink,
6914            ) -> Result<(), RpcHandlerError> {
6915                sink.send(b"first".to_vec());
6916                sink.send(b"second".to_vec());
6917                Err(RpcHandlerError::Internal("ran out of fuel".into()))
6918            }
6919        }
6920        let (emit, captured) = capturing_async_emitter();
6921        let mut fold = RpcServerStreamingFold::new(Arc::new(PartialErrHandler), emit);
6922        let req = RpcRequestPayload {
6923            service: "x".to_string(),
6924            deadline_ns: 0,
6925            flags: FLAG_RPC_STREAMING_RESPONSE,
6926            headers: vec![],
6927            body: Bytes::new(),
6928        };
6929        fold.apply(&rpc_request_event(1, 1, req), &mut ()).unwrap();
6930        assert!(
6931            wait_until(|| captured.lock().len() == 3, Duration::from_secs(2)).await,
6932            "expected 2 chunks + 1 terminal error",
6933        );
6934        let captured = captured.lock();
6935        assert_eq!(captured[0].2.body.as_ref(), b"first");
6936        assert_eq!(captured[1].2.body.as_ref(), b"second");
6937        let (_, _, term) = &captured[2];
6938        assert_eq!(term.status, RpcStatus::Internal);
6939        assert!(
6940            String::from_utf8_lossy(&term.body).contains("ran out of fuel"),
6941            "diagnostic must round-trip, got {:?}",
6942            String::from_utf8_lossy(&term.body),
6943        );
6944    }
6945
6946    /// Handler panics. The fold's `catch_unwind` surfaces it as a
6947    /// terminal `RpcStatus::Internal` rather than killing the
6948    /// runtime.
6949    #[tokio::test]
6950    async fn streaming_fold_handler_panic_surfaces_as_internal_terminal() {
6951        struct PanicHandler;
6952        #[async_trait::async_trait]
6953        impl RpcStreamingHandler for PanicHandler {
6954            async fn call(
6955                &self,
6956                _ctx: RpcContext,
6957                _sink: RpcResponseSink,
6958            ) -> Result<(), RpcHandlerError> {
6959                panic!("kaboom in streaming handler");
6960            }
6961        }
6962        let (emit, captured) = capturing_async_emitter();
6963        let mut fold = RpcServerStreamingFold::new(Arc::new(PanicHandler), emit);
6964        let req = RpcRequestPayload {
6965            service: "x".to_string(),
6966            deadline_ns: 0,
6967            flags: FLAG_RPC_STREAMING_RESPONSE,
6968            headers: vec![],
6969            body: Bytes::new(),
6970        };
6971        fold.apply(&rpc_request_event(1, 2, req), &mut ()).unwrap();
6972        assert!(
6973            wait_until(|| !captured.lock().is_empty(), Duration::from_secs(2)).await,
6974            "panic must surface as a terminal frame",
6975        );
6976        let captured = captured.lock();
6977        assert_eq!(captured.len(), 1);
6978        let (_, _, resp) = &captured[0];
6979        assert_eq!(resp.status, RpcStatus::Internal);
6980        assert!(
6981            String::from_utf8_lossy(&resp.body).contains("kaboom"),
6982            "panic message must surface, got {:?}",
6983            String::from_utf8_lossy(&resp.body),
6984        );
6985    }
6986
6987    /// CANCEL during a streaming call overrides the terminal frame
6988    /// with `RpcStatus::Cancelled` — same CANCEL-wins ordering as
6989    /// the unary fold.
6990    #[tokio::test]
6991    async fn streaming_fold_cancel_overrides_terminal_with_cancelled() {
6992        struct CooperativeHandler;
6993        #[async_trait::async_trait]
6994        impl RpcStreamingHandler for CooperativeHandler {
6995            async fn call(
6996                &self,
6997                ctx: RpcContext,
6998                sink: RpcResponseSink,
6999            ) -> Result<(), RpcHandlerError> {
7000                sink.send(b"chunk-0".to_vec());
7001                tokio::select! {
7002                    _ = ctx.cancellation.cancelled() => Ok(()),
7003                    _ = tokio::time::sleep(Duration::from_secs(5)) => Ok(()),
7004                }
7005            }
7006        }
7007        let (emit, captured) = capturing_async_emitter();
7008        let mut fold = RpcServerStreamingFold::new(Arc::new(CooperativeHandler), emit);
7009        let req = RpcRequestPayload {
7010            service: "x".to_string(),
7011            deadline_ns: 0,
7012            flags: FLAG_RPC_STREAMING_RESPONSE,
7013            headers: vec![],
7014            body: Bytes::new(),
7015        };
7016        fold.apply(&rpc_request_event(7, 13, req), &mut ()).unwrap();
7017        // Wait until at least the first chunk is captured AND the
7018        // handler is parked (in_flight key present), then CANCEL.
7019        assert!(
7020            wait_until(
7021                || !captured.lock().is_empty() && fold.in_flight_keys().contains(&(0, 7, 13)),
7022                Duration::from_secs(2)
7023            )
7024            .await
7025        );
7026        fold.apply(&rpc_cancel_event(7, 13), &mut ()).unwrap();
7027        // Wait for the terminal frame.
7028        assert!(
7029            wait_until(|| captured.lock().len() >= 2, Duration::from_secs(2)).await,
7030            "expected first chunk + terminal frame",
7031        );
7032        let captured = captured.lock();
7033        // First emit was the chunk; the LAST should be the
7034        // Cancelled terminal.
7035        assert_eq!(
7036            captured.last().unwrap().2.status,
7037            RpcStatus::Cancelled,
7038            "CANCEL must override terminal status",
7039        );
7040    }
7041
7042    /// Duplicate REQUEST with the same `(origin, call_id)` is
7043    /// refused with a synthetic Internal terminal frame and does
7044    /// NOT spawn a second handler. Mirror of the unary fold's
7045    /// regression at server_fold_duplicate_request_refuses_*.
7046    #[tokio::test]
7047    async fn streaming_fold_duplicate_request_refuses_without_double_dispatch() {
7048        let invocations = Arc::new(AtomicUsize::new(0));
7049        struct CountingHandler {
7050            invocations: Arc<AtomicUsize>,
7051        }
7052        #[async_trait::async_trait]
7053        impl RpcStreamingHandler for CountingHandler {
7054            async fn call(
7055                &self,
7056                _ctx: RpcContext,
7057                sink: RpcResponseSink,
7058            ) -> Result<(), RpcHandlerError> {
7059                self.invocations.fetch_add(1, Ordering::SeqCst);
7060                tokio::time::sleep(Duration::from_millis(80)).await;
7061                sink.send(b"chunk".to_vec());
7062                Ok(())
7063            }
7064        }
7065        let (emit, captured) = capturing_async_emitter();
7066        let mut fold = RpcServerStreamingFold::new(
7067            Arc::new(CountingHandler {
7068                invocations: invocations.clone(),
7069            }),
7070            emit,
7071        );
7072        let req = RpcRequestPayload {
7073            service: "x".to_string(),
7074            deadline_ns: 0,
7075            flags: FLAG_RPC_STREAMING_RESPONSE,
7076            headers: vec![],
7077            body: Bytes::new(),
7078        };
7079        fold.apply(&rpc_request_event(1, 99, req.clone()), &mut ())
7080            .unwrap();
7081        assert!(
7082            wait_until(
7083                || fold.in_flight_keys().contains(&(0, 1, 99)),
7084                Duration::from_secs(1)
7085            )
7086            .await
7087        );
7088        // Duplicate REQUEST — must emit a synthetic Internal
7089        // terminal and not invoke the handler a second time.
7090        fold.apply(&rpc_request_event(1, 99, req), &mut ()).unwrap();
7091        assert!(
7092            wait_until(|| !captured.lock().is_empty(), Duration::from_secs(1)).await,
7093            "synthetic refusal should be emitted",
7094        );
7095        // First emit (chronologically) is the synthetic refusal.
7096        let refusal = captured.lock()[0].clone();
7097        assert_eq!(refusal.2.status, RpcStatus::Internal);
7098        assert!(String::from_utf8_lossy(&refusal.2.body).contains("duplicate"));
7099        // Wait for the original handler to complete (chunk + terminal).
7100        assert!(
7101            wait_until(|| captured.lock().len() >= 3, Duration::from_secs(2)).await,
7102            "first handler should still complete normally",
7103        );
7104        assert_eq!(
7105            invocations.load(Ordering::SeqCst),
7106            1,
7107            "duplicate REQUEST must NOT spawn a second handler",
7108        );
7109    }
7110
7111    /// STREAM_GRANT for an unknown call_id is silently dropped
7112    /// (no panic, no tracing event escalation). Pin the
7113    /// always-safe behavior so a misbehaving caller (or a CANCEL/
7114    /// GRANT race) can't crash the fold.
7115    #[tokio::test]
7116    async fn streaming_fold_grant_for_unknown_call_id_is_no_op() {
7117        struct NoopHandler;
7118        #[async_trait::async_trait]
7119        impl RpcStreamingHandler for NoopHandler {
7120            async fn call(
7121                &self,
7122                _ctx: RpcContext,
7123                _sink: RpcResponseSink,
7124            ) -> Result<(), RpcHandlerError> {
7125                Ok(())
7126            }
7127        }
7128        let (emit, captured) = capturing_async_emitter();
7129        let mut fold = RpcServerStreamingFold::new(Arc::new(NoopHandler), emit);
7130        let result = fold.apply(&rpc_stream_grant_event(99, 42, 5), &mut ());
7131        assert!(result.is_ok(), "GRANT for unknown call_id must be Ok");
7132        assert!(captured.lock().is_empty(), "no emit for unknown GRANT");
7133    }
7134
7135    /// Regression for M20: the streaming pump's mpsc is bounded
7136    /// at `STREAMING_PUMP_CAPACITY`. A handler that produces
7137    /// chunks faster than the pump drains gets its excess
7138    /// `sink.send(...)` calls silently dropped (matching the
7139    /// "caller cancelled" semantic) — and the metric counter
7140    /// `streaming_chunks_dropped_total` increments.
7141    ///
7142    /// We construct the sink directly with a tiny bounded mpsc
7143    /// (capacity 2) and a metrics handle, then call `send` 5
7144    /// times without a receiver. The first 2 fit in the channel;
7145    /// the next 3 are dropped and counted.
7146    #[tokio::test]
7147    async fn streaming_sink_drops_on_full_and_increments_metric() {
7148        use crate::adapter::net::mesh_rpc_metrics::{RpcMetricsRegistry, ServiceMetricsAtomic};
7149        // Tiny channel to make overflow easy to observe.
7150        let (tx, _rx) = tokio::sync::mpsc::channel::<bytes::Bytes>(2);
7151        let registry = RpcMetricsRegistry::new();
7152        let metrics: Arc<ServiceMetricsAtomic> = registry.for_service("drop_test");
7153        let sink = RpcResponseSink {
7154            inner: tx,
7155            metrics: Some(metrics.clone()),
7156        };
7157        // 5 sends; first 2 buffer, next 3 drop.
7158        for i in 0..5u8 {
7159            sink.send(vec![i]);
7160        }
7161        assert_eq!(
7162            metrics
7163                .streaming_chunks_dropped_total
7164                .load(Ordering::Relaxed),
7165            3,
7166            "expected 3 dropped chunks (capacity=2, sent 5)",
7167        );
7168    }
7169
7170    /// Malformed REQUEST payload on the streaming fold: emits one
7171    /// terminal `UnknownVersion` frame and continues — same
7172    /// keep-the-adapter-alive contract as the unary fold.
7173    #[tokio::test]
7174    async fn streaming_fold_malformed_payload_emits_unknown_version_terminal() {
7175        struct NoopHandler;
7176        #[async_trait::async_trait]
7177        impl RpcStreamingHandler for NoopHandler {
7178            async fn call(
7179                &self,
7180                _ctx: RpcContext,
7181                _sink: RpcResponseSink,
7182            ) -> Result<(), RpcHandlerError> {
7183                Ok(())
7184            }
7185        }
7186        let (emit, captured) = capturing_async_emitter();
7187        let mut fold = RpcServerStreamingFold::new(Arc::new(NoopHandler), emit);
7188        // Garbage tail: valid meta + 0x00 svc_len → Truncated.
7189        let meta = EventMeta::new(DISPATCH_RPC_REQUEST, 0, 1, 1, 0);
7190        let mut buf = Vec::new();
7191        buf.extend_from_slice(&meta.to_bytes());
7192        // OA2-E0.2: RpcRouteV1 route placeholder — these test frames
7193        // feed the folds directly (no ingress select), and the folds
7194        // skip the route to reach the payload at RPC_FRAME_BODY_OFFSET.
7195        encode_rpc_route(&mut buf, 0);
7196        buf.push(0x00);
7197        let ev = RedexEvent {
7198            entry: RedexEntry::new_heap(0, 0, buf.len() as u32, 0, 0),
7199            payload: bytes::Bytes::from(buf),
7200        };
7201        let result = fold.apply(&ev, &mut ());
7202        assert!(
7203            result.is_ok(),
7204            "malformed payload must NOT kill the adapter",
7205        );
7206        assert!(
7207            wait_until(|| !captured.lock().is_empty(), Duration::from_secs(2)).await,
7208            "synthetic UnknownVersion terminal must arrive",
7209        );
7210        let captured = captured.lock();
7211        assert_eq!(captured[0].2.status, RpcStatus::UnknownVersion);
7212        let hdr = captured[0]
7213            .2
7214            .headers
7215            .iter()
7216            .find(|(n, _)| n == HEADER_NRPC_STREAMING);
7217        assert!(hdr.is_some(), "malformed terminal must include end marker");
7218    }
7219
7220    // ====================================================================
7221    // Phase B — RpcStreamingRequestFold (server-side client-streaming)
7222    // ====================================================================
7223
7224    /// Build a REQUEST_CHUNK event for tests. Mirrors
7225    /// `rpc_request_event` / `rpc_stream_grant_event` shape.
7226    fn rpc_request_chunk_event(
7227        caller_origin: u64,
7228        call_id: u64,
7229        flags: u16,
7230        body: Vec<u8>,
7231    ) -> RedexEvent {
7232        let meta = EventMeta::new(DISPATCH_RPC_REQUEST_CHUNK, 0, caller_origin, call_id, 0);
7233        let payload = RpcRequestChunkPayload {
7234            call_id,
7235            flags,
7236            headers: vec![],
7237            body: body.into(),
7238        };
7239        let mut buf = Vec::new();
7240        buf.extend_from_slice(&meta.to_bytes());
7241        // OA2-E0.2: RpcRouteV1 route placeholder — these test frames
7242        // feed the folds directly (no ingress select), and the folds
7243        // skip the route to reach the payload at RPC_FRAME_BODY_OFFSET.
7244        encode_rpc_route(&mut buf, 0);
7245        buf.extend_from_slice(&payload.encode());
7246        RedexEvent {
7247            entry: RedexEntry::new_heap(0, 0, buf.len() as u32, 0, 0),
7248            payload: bytes::Bytes::from(buf),
7249        }
7250    }
7251
7252    /// Collecting client-streaming handler: drains the stream into
7253    /// a Vec, returns an Ok response whose body is the count of
7254    /// chunks seen (8-byte LE). Captured chunk bodies are exposed
7255    /// via the `Arc<Mutex<Vec<Bytes>>>` so tests can assert
7256    /// ordering and content.
7257    struct CollectingClientStreamHandler {
7258        seen: Arc<Mutex<Vec<bytes::Bytes>>>,
7259        observed_cancel: Arc<AtomicBool>,
7260    }
7261    #[async_trait::async_trait]
7262    impl RpcClientStreamingHandler for CollectingClientStreamHandler {
7263        async fn call(
7264            &self,
7265            ctx: RpcStreamingContext,
7266            mut requests: RequestStream,
7267        ) -> Result<RpcResponsePayload, RpcHandlerError> {
7268            use futures::StreamExt;
7269            while let Some(chunk) = requests.next().await {
7270                self.seen.lock().push(chunk);
7271            }
7272            // Re-check cancellation after EOF so the test can
7273            // distinguish "clean REQUEST_END" from "CANCEL closed
7274            // the stream early".
7275            if ctx.cancellation.is_cancelled() {
7276                self.observed_cancel
7277                    .store(true, std::sync::atomic::Ordering::SeqCst);
7278            }
7279            let count = self.seen.lock().len() as u64;
7280            Ok(RpcResponsePayload {
7281                status: RpcStatus::Ok,
7282                headers: vec![],
7283                body: Bytes::copy_from_slice(&count.to_le_bytes()),
7284            })
7285        }
7286    }
7287
7288    /// 1/6 — happy path: REQUEST + 3 REQUEST_CHUNKs (last has
7289    /// FLAG_END) delivers 4 bodies to the handler in order; the
7290    /// fold emits exactly one terminal RESPONSE carrying the
7291    /// handler's reply.
7292    #[tokio::test]
7293    async fn streaming_request_fold_collects_all_chunks_and_emits_terminal_response() {
7294        let seen = Arc::new(Mutex::new(Vec::new()));
7295        let observed_cancel = Arc::new(AtomicBool::new(false));
7296        let (emit, captured) = capturing_emitter();
7297        let mut fold = RpcStreamingRequestFold::new(
7298            Arc::new(CollectingClientStreamHandler {
7299                seen: seen.clone(),
7300                observed_cancel: observed_cancel.clone(),
7301            }),
7302            emit,
7303        );
7304        // REQUEST with the client-streaming flag, body = "a".
7305        let req = RpcRequestPayload {
7306            service: "agg".to_string(),
7307            deadline_ns: 0,
7308            flags: FLAG_RPC_CLIENT_STREAMING_REQUEST,
7309            headers: vec![],
7310            body: Bytes::from_static(b"a"),
7311        };
7312        fold.apply(&rpc_request_event(0xCAFE, 7, req), &mut ())
7313            .unwrap();
7314        // Wait until the sender is registered (handler task has
7315        // picked up the request and the apply path completed).
7316        assert!(
7317            wait_until(
7318                || fold.sender_keys().contains(&(0, 0xCAFE, 7)),
7319                Duration::from_secs(1)
7320            )
7321            .await
7322        );
7323        // Three more chunks; last sets FLAG_REQUEST_END.
7324        fold.apply(
7325            &rpc_request_chunk_event(0xCAFE, 7, 0, b"b".to_vec()),
7326            &mut (),
7327        )
7328        .unwrap();
7329        fold.apply(
7330            &rpc_request_chunk_event(0xCAFE, 7, 0, b"c".to_vec()),
7331            &mut (),
7332        )
7333        .unwrap();
7334        fold.apply(
7335            &rpc_request_chunk_event(0xCAFE, 7, FLAG_RPC_REQUEST_END, b"d".to_vec()),
7336            &mut (),
7337        )
7338        .unwrap();
7339        // Handler should observe 4 bodies in order and emit one
7340        // terminal RESPONSE whose body encodes the count.
7341        assert!(
7342            wait_until(|| !captured.lock().is_empty(), Duration::from_secs(2)).await,
7343            "expected terminal RESPONSE"
7344        );
7345        let captured = captured.lock();
7346        assert_eq!(captured.len(), 1, "exactly one terminal RESPONSE");
7347        let (origin, call_id, resp) = &captured[0];
7348        assert_eq!(*origin, 0xCAFE);
7349        assert_eq!(*call_id, 7);
7350        assert_eq!(resp.status, RpcStatus::Ok);
7351        assert_eq!(resp.body.as_ref(), 4u64.to_le_bytes());
7352        // And the chunks landed in order.
7353        let seen = seen.lock();
7354        let collected: Vec<&[u8]> = seen.iter().map(|b| b.as_ref()).collect();
7355        assert_eq!(collected, vec![b"a", b"b", b"c", b"d"]);
7356        assert!(
7357            !observed_cancel.load(std::sync::atomic::Ordering::SeqCst),
7358            "clean REQUEST_END must NOT register as a cancellation"
7359        );
7360    }
7361
7362    /// 2/6 — degenerate case: initial REQUEST with both the
7363    /// client-streaming AND request-end flags set. Handler sees
7364    /// exactly one body (the REQUEST's own body) and EOF — the
7365    /// "one-item upload" fast path that saves a trailing CHUNK
7366    /// event.
7367    #[tokio::test]
7368    async fn streaming_request_fold_initial_request_with_end_flag_yields_single_item() {
7369        let seen = Arc::new(Mutex::new(Vec::new()));
7370        let observed_cancel = Arc::new(AtomicBool::new(false));
7371        let (emit, captured) = capturing_emitter();
7372        let mut fold = RpcStreamingRequestFold::new(
7373            Arc::new(CollectingClientStreamHandler {
7374                seen: seen.clone(),
7375                observed_cancel,
7376            }),
7377            emit,
7378        );
7379        let req = RpcRequestPayload {
7380            service: "agg".to_string(),
7381            deadline_ns: 0,
7382            flags: FLAG_RPC_CLIENT_STREAMING_REQUEST | FLAG_RPC_REQUEST_END,
7383            headers: vec![],
7384            body: Bytes::from_static(b"only"),
7385        };
7386        fold.apply(&rpc_request_event(1, 42, req), &mut ()).unwrap();
7387        assert!(
7388            wait_until(|| !captured.lock().is_empty(), Duration::from_secs(2)).await,
7389            "expected terminal RESPONSE"
7390        );
7391        let captured = captured.lock();
7392        assert_eq!(captured.len(), 1);
7393        assert_eq!(captured[0].2.status, RpcStatus::Ok);
7394        assert_eq!(captured[0].2.body.as_ref(), 1u64.to_le_bytes());
7395        assert_eq!(
7396            seen.lock()
7397                .iter()
7398                .map(|b| b.as_ref())
7399                .collect::<Vec<&[u8]>>(),
7400            vec![b"only" as &[u8]]
7401        );
7402        // Sender must NOT have been registered (initial-REQUEST-
7403        // with-END skips the map insert).
7404        assert!(fold.sender_keys().is_empty());
7405    }
7406
7407    /// 3/6 — CANCEL closes the request stream early, flips the
7408    /// cancellation token, and the spawned task overrides the
7409    /// handler's terminal with `RpcStatus::Cancelled`.
7410    #[tokio::test]
7411    async fn streaming_request_fold_cancel_closes_stream_and_overrides_terminal() {
7412        let seen = Arc::new(Mutex::new(Vec::new()));
7413        let observed_cancel = Arc::new(AtomicBool::new(false));
7414        let (emit, captured) = capturing_emitter();
7415        let mut fold = RpcStreamingRequestFold::new(
7416            Arc::new(CollectingClientStreamHandler {
7417                seen: seen.clone(),
7418                observed_cancel: observed_cancel.clone(),
7419            }),
7420            emit,
7421        );
7422        let req = RpcRequestPayload {
7423            service: "agg".to_string(),
7424            deadline_ns: 0,
7425            flags: FLAG_RPC_CLIENT_STREAMING_REQUEST,
7426            headers: vec![],
7427            body: Bytes::from_static(b"first"),
7428        };
7429        fold.apply(&rpc_request_event(2, 17, req), &mut ()).unwrap();
7430        // Wait for the handler to register, then one in-flight
7431        // CHUNK, then CANCEL before the handler ever finishes
7432        // draining.
7433        assert!(
7434            wait_until(
7435                || fold.sender_keys().contains(&(0, 2, 17)),
7436                Duration::from_secs(1)
7437            )
7438            .await
7439        );
7440        fold.apply(
7441            &rpc_request_chunk_event(2, 17, 0, b"second".to_vec()),
7442            &mut (),
7443        )
7444        .unwrap();
7445        fold.apply(&rpc_cancel_event(2, 17), &mut ()).unwrap();
7446        // Terminal must arrive and must be Cancelled (CANCEL-wins
7447        // ordering, same as the response-side fold).
7448        assert!(
7449            wait_until(|| !captured.lock().is_empty(), Duration::from_secs(2)).await,
7450            "expected terminal RESPONSE"
7451        );
7452        let captured = captured.lock();
7453        assert_eq!(captured.len(), 1);
7454        assert_eq!(
7455            captured[0].2.status,
7456            RpcStatus::Cancelled,
7457            "CANCEL must override terminal status"
7458        );
7459        assert!(
7460            observed_cancel.load(std::sync::atomic::Ordering::SeqCst),
7461            "handler must observe cancellation token after stream EOF"
7462        );
7463        // Both maps must be clean post-cancel.
7464        assert!(fold.in_flight_keys().is_empty());
7465        assert!(fold.sender_keys().is_empty());
7466    }
7467
7468    /// 4/6 — handler returns `Err(RpcHandlerError::Application)`
7469    /// → terminal RESPONSE carries the application status code +
7470    /// message body.
7471    #[tokio::test]
7472    async fn streaming_request_fold_application_error_round_trips() {
7473        struct AppErrHandler;
7474        #[async_trait::async_trait]
7475        impl RpcClientStreamingHandler for AppErrHandler {
7476            async fn call(
7477                &self,
7478                _ctx: RpcStreamingContext,
7479                mut requests: RequestStream,
7480            ) -> Result<RpcResponsePayload, RpcHandlerError> {
7481                use futures::StreamExt;
7482                // Drain so the stream's EOF doesn't race the
7483                // error return.
7484                while requests.next().await.is_some() {}
7485                Err(RpcHandlerError::Application {
7486                    code: 0xBEEF,
7487                    message: "bad input".to_string(),
7488                })
7489            }
7490        }
7491        let (emit, captured) = capturing_emitter();
7492        let mut fold = RpcStreamingRequestFold::new(Arc::new(AppErrHandler), emit);
7493        let req = RpcRequestPayload {
7494            service: "agg".to_string(),
7495            deadline_ns: 0,
7496            flags: FLAG_RPC_CLIENT_STREAMING_REQUEST | FLAG_RPC_REQUEST_END,
7497            headers: vec![],
7498            body: Bytes::new(),
7499        };
7500        fold.apply(&rpc_request_event(3, 100, req), &mut ())
7501            .unwrap();
7502        assert!(
7503            wait_until(|| !captured.lock().is_empty(), Duration::from_secs(2)).await,
7504            "expected terminal RESPONSE"
7505        );
7506        let captured = captured.lock();
7507        assert_eq!(captured.len(), 1);
7508        assert_eq!(captured[0].2.status, RpcStatus::Application(0xBEEF));
7509        assert_eq!(captured[0].2.body.as_ref(), b"bad input");
7510    }
7511
7512    /// 5/6 — handler panic is caught by `catch_unwind`; terminal
7513    /// surfaces as `Internal` carrying the panic message. Same
7514    /// contract as the existing folds — a misbehaving handler
7515    /// can't take down the cortex adapter.
7516    #[tokio::test]
7517    async fn streaming_request_fold_handler_panic_surfaces_as_internal() {
7518        struct PanickyHandler;
7519        #[async_trait::async_trait]
7520        impl RpcClientStreamingHandler for PanickyHandler {
7521            async fn call(
7522                &self,
7523                _ctx: RpcStreamingContext,
7524                _requests: RequestStream,
7525            ) -> Result<RpcResponsePayload, RpcHandlerError> {
7526                panic!("intentional test panic");
7527            }
7528        }
7529        let (emit, captured) = capturing_emitter();
7530        let mut fold = RpcStreamingRequestFold::new(Arc::new(PanickyHandler), emit);
7531        let req = RpcRequestPayload {
7532            service: "agg".to_string(),
7533            deadline_ns: 0,
7534            flags: FLAG_RPC_CLIENT_STREAMING_REQUEST | FLAG_RPC_REQUEST_END,
7535            headers: vec![],
7536            body: Bytes::new(),
7537        };
7538        fold.apply(&rpc_request_event(4, 200, req), &mut ())
7539            .unwrap();
7540        assert!(
7541            wait_until(|| !captured.lock().is_empty(), Duration::from_secs(2)).await,
7542            "expected terminal RESPONSE"
7543        );
7544        let captured = captured.lock();
7545        assert_eq!(captured.len(), 1);
7546        assert_eq!(captured[0].2.status, RpcStatus::Internal);
7547        assert!(
7548            String::from_utf8_lossy(&captured[0].2.body).contains("intentional test panic"),
7549            "panic body should carry the panic message"
7550        );
7551    }
7552
7553    /// 6/6 — duplicate REQUEST with the same `(origin, call_id)`
7554    /// is refused with a synthetic `Internal` terminal frame and
7555    /// does NOT spawn a second handler. Mirror of the regression
7556    /// pinned in the unary + response-streaming folds.
7557    #[tokio::test]
7558    async fn streaming_request_fold_duplicate_request_refuses_without_double_dispatch() {
7559        let invocations = Arc::new(AtomicUsize::new(0));
7560        struct CountingHandler {
7561            invocations: Arc<AtomicUsize>,
7562        }
7563        #[async_trait::async_trait]
7564        impl RpcClientStreamingHandler for CountingHandler {
7565            async fn call(
7566                &self,
7567                _ctx: RpcStreamingContext,
7568                mut requests: RequestStream,
7569            ) -> Result<RpcResponsePayload, RpcHandlerError> {
7570                use futures::StreamExt;
7571                self.invocations.fetch_add(1, Ordering::SeqCst);
7572                // Slow handler to keep the call in-flight while
7573                // the duplicate REQUEST arrives.
7574                tokio::time::sleep(Duration::from_millis(80)).await;
7575                while requests.next().await.is_some() {}
7576                Ok(RpcResponsePayload {
7577                    status: RpcStatus::Ok,
7578                    headers: vec![],
7579                    body: Bytes::new(),
7580                })
7581            }
7582        }
7583        let (emit, captured) = capturing_emitter();
7584        let mut fold = RpcStreamingRequestFold::new(
7585            Arc::new(CountingHandler {
7586                invocations: invocations.clone(),
7587            }),
7588            emit,
7589        );
7590        let req = RpcRequestPayload {
7591            service: "agg".to_string(),
7592            deadline_ns: 0,
7593            flags: FLAG_RPC_CLIENT_STREAMING_REQUEST,
7594            headers: vec![],
7595            body: Bytes::new(),
7596        };
7597        fold.apply(&rpc_request_event(5, 99, req.clone()), &mut ())
7598            .unwrap();
7599        assert!(
7600            wait_until(
7601                || fold.in_flight_keys().contains(&(0, 5, 99)),
7602                Duration::from_secs(1)
7603            )
7604            .await
7605        );
7606        // Duplicate REQUEST: synthetic Internal terminal emitted,
7607        // handler invocation count must stay at 1.
7608        fold.apply(&rpc_request_event(5, 99, req), &mut ()).unwrap();
7609        assert!(
7610            wait_until(|| !captured.lock().is_empty(), Duration::from_secs(1)).await,
7611            "synthetic refusal terminal expected"
7612        );
7613        let refusal = captured.lock()[0].clone();
7614        assert_eq!(refusal.2.status, RpcStatus::Internal);
7615        assert!(String::from_utf8_lossy(&refusal.2.body).contains("duplicate"));
7616        // Finish the first handler so its terminal lands too.
7617        fold.apply(
7618            &rpc_request_chunk_event(5, 99, FLAG_RPC_REQUEST_END, vec![]),
7619            &mut (),
7620        )
7621        .unwrap();
7622        assert!(
7623            wait_until(|| captured.lock().len() >= 2, Duration::from_secs(2)).await,
7624            "first handler should still complete normally"
7625        );
7626        assert_eq!(
7627            invocations.load(Ordering::SeqCst),
7628            1,
7629            "duplicate REQUEST must NOT spawn a second handler",
7630        );
7631    }
7632}