Skip to main content

laser_wire/
forward.rs

1// Frames the managed backend consumes on behalf of a client. The SDK never
2// sends these directly. They live here so the encoding has one definition and
3// the golden corpus pins them like every other frame.
4
5use serde::{Deserialize, Serialize};
6
7/// Trusted stream identity stamped by the server before forwarding a session request.
8#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
9pub struct ForwardedScope {
10    pub stream_id: u32,
11    pub stream: String,
12    pub stream_created_at_micros: u64,
13}
14
15/// A forwarded managed query. Carries the authenticated identity the SDK cannot
16/// set itself, plus the opaque request the SDK sent. CBOR-encoded, named fields.
17#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
18pub struct ForwardedQuery {
19    /// Authenticated identity. Audit today, per-user stream scoping later.
20    /// Trusted: set by the server, not the client.
21    pub user_id: u32,
22    /// Originating client id, for logging and audit only.
23    pub client_id: u128,
24    /// `gen_ai.conversation.id` echoed for the audit log, never used to route.
25    pub correlation: Option<String>,
26    /// Opaque CBOR `QueryEnvelope` from the SDK, not decoded in transit.
27    #[serde(with = "crate::encoding::bin_bytes")]
28    pub query_envelope: Vec<u8>,
29    /// The caller's effective capability grants, stamped by the server. Empty
30    /// (the default, skipped on the wire) when authorization is not in effect.
31    #[serde(default, skip_serializing_if = "Vec::is_empty")]
32    pub grants: Vec<crate::authz::Grant>,
33}
34
35/// A forwarded keyed managed command (registry browse, key-value, forks). Unlike
36/// `ForwardedQuery`, several op kinds share one path, so the frame carries
37/// `command_code` and the backend dispatches on it. `payload` is the opaque
38/// CBOR request the SDK sent.
39#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
40pub struct ForwardedCommand {
41    /// Authenticated identity, set by the server. The key-value store scopes its
42    /// rows by this `user_id`, and the SDK cannot set it.
43    pub user_id: u32,
44    /// Originating client id, for logging and audit only.
45    pub client_id: u128,
46    /// `gen_ai.conversation.id` echoed for the audit log, never used to route.
47    pub correlation: Option<String>,
48    /// Stable identity minted by the originating SDK for a logical managed
49    /// mutation. Read-only commands leave it absent.
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub operation_id: Option<u128>,
52    /// Set by the server, like `user_id`: true when the caller holds a global
53    /// read permission. Widens the read-side ops (key-value scan/namespaces,
54    /// fork list) to every user's rows, while writes stay scoped to `user_id`
55    /// regardless.
56    #[serde(default)]
57    pub read_all: bool,
58    /// Server-stamped stream identity. A session command without it fails closed.
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub scope: Option<ForwardedScope>,
61    /// The managed command code (browse, key-value, or fork block). The backend
62    /// dispatches on it.
63    pub command_code: u32,
64    /// Opaque CBOR request the SDK sent, not decoded in transit.
65    #[serde(with = "crate::encoding::bin_bytes")]
66    pub payload: Vec<u8>,
67    /// The caller's effective capability grants, stamped by the server. Empty
68    /// (the default, skipped on the wire) when authorization is not in effect.
69    #[serde(default, skip_serializing_if = "Vec::is_empty")]
70    pub grants: Vec<crate::authz::Grant>,
71}
72
73#[cfg(all(test, feature = "cbor"))]
74mod tests {
75    use super::*;
76    use crate::framing::{decode_named, encode_named};
77
78    // A contiguous byte-string run inside the encoded frame proves the opaque
79    // field rode as a CBOR byte string (major type 2), not an array of ints.
80    // The values are all >= 0x18, which an array would have to widen to two
81    // bytes each, so the byte string is also strictly smaller.
82    fn contains_byte_string(frame: &[u8], len: u8, fill: u8) -> bool {
83        let head = 0x40 | len; // byte string, length in the low 5 bits
84        let mut want = vec![head];
85        want.extend(std::iter::repeat_n(fill, len as usize));
86        frame.windows(want.len()).any(|w| w == want.as_slice())
87    }
88
89    #[test]
90    fn given_forwarded_query_when_encoded_then_envelope_rides_as_a_byte_string() {
91        let frame = encode_named(&ForwardedQuery {
92            user_id: 7,
93            client_id: 42,
94            correlation: Some("conv-1".to_owned()),
95            query_envelope: vec![0x90; 5],
96            grants: Vec::new(),
97        })
98        .expect("encodes");
99        assert!(
100            contains_byte_string(&frame, 5, 0x90),
101            "query_envelope must encode as a CBOR byte string, got {frame:02x?}"
102        );
103    }
104
105    #[test]
106    fn given_forwarded_command_when_encoded_then_payload_rides_as_a_byte_string() {
107        let frame = encode_named(&ForwardedCommand {
108            user_id: 7,
109            client_id: 42,
110            correlation: None,
111            operation_id: Some(17),
112            read_all: true,
113            scope: None,
114            command_code: 1_000_000,
115            payload: vec![0x90; 5],
116            grants: Vec::new(),
117        })
118        .expect("encodes");
119        assert!(
120            contains_byte_string(&frame, 5, 0x90),
121            "payload must encode as a CBOR byte string, got {frame:02x?}"
122        );
123    }
124
125    #[test]
126    fn given_forwarded_frames_when_round_tripped_then_should_preserve_fields() {
127        let query = ForwardedQuery {
128            user_id: 1,
129            client_id: u128::MAX,
130            correlation: Some("c".to_owned()),
131            query_envelope: vec![0xff, 0x00, 0x18, 0x7f],
132            grants: Vec::new(),
133        };
134        let back: ForwardedQuery =
135            decode_named(&encode_named(&query).expect("encodes")).expect("decodes");
136        assert_eq!(back, query);
137
138        let command = ForwardedCommand {
139            user_id: 2,
140            client_id: 9,
141            correlation: None,
142            operation_id: Some(9),
143            read_all: false,
144            scope: None,
145            command_code: 42,
146            payload: vec![0xde, 0xad, 0xbe, 0xef],
147            grants: Vec::new(),
148        };
149        let back: ForwardedCommand =
150            decode_named(&encode_named(&command).expect("encodes")).expect("decodes");
151        assert_eq!(back, command);
152    }
153}