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 /// Set by the server, like `user_id`: true when the caller holds a global
41 /// read permission. Widens the read-side ops (key-value scan/namespaces,
42 /// fork list) to every user's rows, while writes stay scoped to `user_id`
43 /// regardless. Defaults to false for a frame from an older server that
44 /// does not set it.
45 #[serde(default)]
46 pub read_all: bool,
47 /// The managed command code (browse, key-value, or fork block). The backend
48 /// dispatches on it.
49 pub command_code: u32,
50 /// Opaque CBOR request the SDK sent, not decoded in transit.
51 #[serde(with = "crate::encoding::bin_bytes")]
52 pub payload: Vec<u8>,
53 /// The caller's effective capability grants, stamped by the server. Empty
54 /// (the default, skipped on the wire) when authorization is not in effect.
55 #[serde(default, skip_serializing_if = "Vec::is_empty")]
56 pub grants: Vec<crate::authz::Grant>,
57}
58
59#[cfg(all(test, feature = "cbor"))]
60mod tests {
61 use super::*;
62 use crate::framing::{decode_named, encode_named};
63
64 // A contiguous byte-string run inside the encoded frame proves the opaque
65 // field rode as a CBOR byte string (major type 2), not an array of ints.
66 // The values are all >= 0x18, which an array would have to widen to two
67 // bytes each, so the byte string is also strictly smaller.
68 fn contains_byte_string(frame: &[u8], len: u8, fill: u8) -> bool {
69 let head = 0x40 | len; // byte string, length in the low 5 bits
70 let mut want = vec![head];
71 want.extend(std::iter::repeat_n(fill, len as usize));
72 frame.windows(want.len()).any(|w| w == want.as_slice())
73 }
74
75 #[test]
76 fn given_forwarded_query_when_encoded_then_envelope_rides_as_a_byte_string() {
77 let frame = encode_named(&ForwardedQuery {
78 user_id: 7,
79 client_id: 42,
80 correlation: Some("conv-1".to_owned()),
81 query_envelope: vec![0x90; 5],
82 grants: Vec::new(),
83 })
84 .expect("encodes");
85 assert!(
86 contains_byte_string(&frame, 5, 0x90),
87 "query_envelope must encode as a CBOR byte string, got {frame:02x?}"
88 );
89 }
90
91 #[test]
92 fn given_forwarded_command_when_encoded_then_payload_rides_as_a_byte_string() {
93 let frame = encode_named(&ForwardedCommand {
94 user_id: 7,
95 client_id: 42,
96 correlation: None,
97 read_all: true,
98 command_code: 1_000_000,
99 payload: vec![0x90; 5],
100 grants: Vec::new(),
101 })
102 .expect("encodes");
103 assert!(
104 contains_byte_string(&frame, 5, 0x90),
105 "payload must encode as a CBOR byte string, got {frame:02x?}"
106 );
107 }
108
109 #[test]
110 fn given_forwarded_frames_when_round_tripped_then_should_preserve_fields() {
111 let query = ForwardedQuery {
112 user_id: 1,
113 client_id: u128::MAX,
114 correlation: Some("c".to_owned()),
115 query_envelope: vec![0xff, 0x00, 0x18, 0x7f],
116 grants: Vec::new(),
117 };
118 let back: ForwardedQuery =
119 decode_named(&encode_named(&query).expect("encodes")).expect("decodes");
120 assert_eq!(back, query);
121
122 let command = ForwardedCommand {
123 user_id: 2,
124 client_id: 9,
125 correlation: None,
126 read_all: false,
127 command_code: 42,
128 payload: vec![0xde, 0xad, 0xbe, 0xef],
129 grants: Vec::new(),
130 };
131 let back: ForwardedCommand =
132 decode_named(&encode_named(&command).expect("encodes")).expect("decodes");
133 assert_eq!(back, command);
134 }
135}