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