1use crate::batch::BatchRequest;
2use crate::clients::ClientMetadata;
3use crate::codes::AGDX_BATCH_CODE;
4use crate::error::InvalidError;
5use crate::graph::{GraphEdge, GraphNode, GraphQuery, GraphUpsert, SourceRef};
6use crate::kv::{KvScan, KvSet};
7use crate::limits::{
8 MAX_BATCH_OPS, MAX_CLIENT_METADATA, MAX_FRAME_BYTES, MAX_GRAPH_NODE_LABELS,
9 MAX_GRAPH_RESULT_ELEMENTS, MAX_GRAPH_TRAVERSE_DEPTH, MAX_KEY_BYTES, MAX_MEMORY_BODY_BYTES,
10 MAX_METADATA_ENTRIES, MAX_METADATA_KEY_BYTES, MAX_PAGE_SIZE, MAX_QUERY_NAME_BYTES,
11 MAX_SCAN_LIMIT, MAX_SOURCE_REF_BYTES, MAX_TEXT_QUERY_BYTES, MAX_VALUE_BYTES,
12 MAX_VECTOR_DIMENSIONS,
13};
14use crate::memory::MemoryRecord;
15use crate::query::{TextQuery, Value, VectorQuery};
16
17pub trait Validate {
23 fn validate(&self) -> Result<(), InvalidError>;
25}
26
27pub(crate) fn validate_safelisted_name(
33 label: &str,
34 value: &str,
35 cap: usize,
36) -> Result<(), InvalidError> {
37 if value.is_empty() {
38 return Err(InvalidError::new(format!("{label} must not be empty")));
39 }
40 if value.len() > cap {
41 return Err(InvalidError::new(format!(
42 "{label} is {}B, exceeds cap {cap}B",
43 value.len()
44 )));
45 }
46 if let Some(bad) = value
47 .bytes()
48 .find(|byte| !matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' | b'.'))
49 {
50 return Err(InvalidError::new(format!(
51 "{label} has a disallowed byte {bad:#04x}: allowed are ASCII letters, digits, '-', '_', '.'"
52 )));
53 }
54 Ok(())
55}
56
57impl Validate for BatchRequest {
58 fn validate(&self) -> Result<(), InvalidError> {
59 if self.ops.len() > MAX_BATCH_OPS {
60 return Err(InvalidError::new(format!(
61 "batch has {} ops, exceeds cap {MAX_BATCH_OPS}",
62 self.ops.len()
63 )));
64 }
65 let mut total = 0usize;
66 for item in &self.ops {
67 if item.code == AGDX_BATCH_CODE {
70 return Err(InvalidError::new(
71 "a batch may not contain a batch op".to_owned(),
72 ));
73 }
74 if item.payload.len() > MAX_VALUE_BYTES {
75 return Err(InvalidError::new(format!(
76 "batch op payload is {}B, exceeds cap {MAX_VALUE_BYTES}B",
77 item.payload.len()
78 )));
79 }
80 total = total.saturating_add(item.payload.len());
81 }
82 if total > MAX_FRAME_BYTES {
83 return Err(InvalidError::new(format!(
84 "batch total payload is {total}B, exceeds cap {MAX_FRAME_BYTES}B"
85 )));
86 }
87 Ok(())
88 }
89}
90
91impl Validate for KvSet {
92 fn validate(&self) -> Result<(), InvalidError> {
93 crate::kv::validate_namespace(&self.namespace)?;
94 if self.key.is_empty() {
95 return Err(InvalidError::new("key-value key is empty".to_owned()));
96 }
97 if self.key.len() > MAX_KEY_BYTES {
98 return Err(InvalidError::new(format!(
99 "key is {}B, exceeds cap {MAX_KEY_BYTES}B",
100 self.key.len()
101 )));
102 }
103 if self.value.len() > MAX_VALUE_BYTES {
104 return Err(InvalidError::new(format!(
105 "value is {}B, exceeds cap {MAX_VALUE_BYTES}B",
106 self.value.len()
107 )));
108 }
109 Ok(())
110 }
111}
112
113impl Validate for KvScan {
114 fn validate(&self) -> Result<(), InvalidError> {
115 crate::kv::validate_namespace(&self.namespace)?;
116 if self.limit > MAX_SCAN_LIMIT {
117 return Err(InvalidError::new(format!(
118 "scan limit {} exceeds cap {MAX_SCAN_LIMIT}",
119 self.limit
120 )));
121 }
122 Ok(())
123 }
124}
125
126impl Validate for GraphQuery {
127 fn validate(&self) -> Result<(), InvalidError> {
128 crate::graph::validate_graph_name(&self.graph)?;
129 if self.traverse.len() > MAX_GRAPH_TRAVERSE_DEPTH as usize {
130 return Err(InvalidError::new(format!(
131 "graph traversal depth {} exceeds cap {MAX_GRAPH_TRAVERSE_DEPTH}",
132 self.traverse.len()
133 )));
134 }
135 if self.limit > MAX_GRAPH_RESULT_ELEMENTS {
136 return Err(InvalidError::new(format!(
137 "graph result limit {} exceeds cap {MAX_GRAPH_RESULT_ELEMENTS}",
138 self.limit
139 )));
140 }
141 Ok(())
142 }
143}
144
145impl Validate for SourceRef {
146 fn validate(&self) -> Result<(), InvalidError> {
147 let size = match self {
148 SourceRef::Message { conversation, .. } => conversation.as_ref().map_or(0, String::len),
151 SourceRef::Kv { namespace, key } => namespace.len() + key.len(),
152 SourceRef::Memory { id } => id.len(),
153 };
154 if size > MAX_SOURCE_REF_BYTES {
155 return Err(InvalidError::new(format!(
156 "source reference is {size}B, exceeds cap {MAX_SOURCE_REF_BYTES}B"
157 )));
158 }
159 Ok(())
160 }
161}
162
163fn validate_attrs(label: &str, attrs: &[(String, Value)]) -> Result<(), InvalidError> {
165 if attrs.len() > MAX_METADATA_ENTRIES {
166 return Err(InvalidError::new(format!(
167 "{label} carries {} attributes, exceeds cap {MAX_METADATA_ENTRIES}",
168 attrs.len()
169 )));
170 }
171 for (key, _) in attrs {
172 if key.len() > MAX_METADATA_KEY_BYTES {
173 return Err(InvalidError::new(format!(
174 "{label} attribute name is {}B, exceeds cap {MAX_METADATA_KEY_BYTES}B",
175 key.len()
176 )));
177 }
178 }
179 Ok(())
180}
181
182impl Validate for GraphNode {
183 fn validate(&self) -> Result<(), InvalidError> {
184 if self.labels.len() > MAX_GRAPH_NODE_LABELS {
185 return Err(InvalidError::new(format!(
186 "graph node carries {} labels, exceeds cap {MAX_GRAPH_NODE_LABELS}",
187 self.labels.len()
188 )));
189 }
190 validate_attrs("graph node", &self.attrs)?;
191 if let Some(source) = &self.source {
192 source.validate()?;
193 }
194 Ok(())
195 }
196}
197
198impl Validate for GraphEdge {
199 fn validate(&self) -> Result<(), InvalidError> {
200 validate_attrs("graph edge", &self.attrs)?;
201 if let Some(source) = &self.source {
202 source.validate()?;
203 }
204 Ok(())
205 }
206}
207
208impl Validate for GraphUpsert {
209 fn validate(&self) -> Result<(), InvalidError> {
210 crate::graph::validate_graph_name(&self.graph)?;
211 let elements = self.nodes.len() + self.edges.len();
212 if elements > MAX_GRAPH_RESULT_ELEMENTS {
213 return Err(InvalidError::new(format!(
214 "graph upsert carries {elements} elements, exceeds cap {MAX_GRAPH_RESULT_ELEMENTS}"
215 )));
216 }
217 for node in &self.nodes {
218 node.validate()?;
219 }
220 for edge in &self.edges {
221 edge.validate()?;
222 }
223 Ok(())
224 }
225}
226
227impl Validate for TextQuery {
228 fn validate(&self) -> Result<(), InvalidError> {
229 if self.query.trim().is_empty() || self.query.len() > MAX_TEXT_QUERY_BYTES {
230 return Err(InvalidError::new(format!(
231 "text query is {}B, expected 1..={MAX_TEXT_QUERY_BYTES}B",
232 self.query.len()
233 )));
234 }
235 if self.query.chars().any(char::is_control) {
236 return Err(InvalidError::new("text query contains a control character"));
237 }
238 if let Some(field) = &self.field
239 && (field.is_empty()
240 || field.len() > MAX_QUERY_NAME_BYTES
241 || field.chars().any(char::is_control))
242 {
243 return Err(InvalidError::new("text query field is invalid"));
244 }
245 Ok(())
246 }
247}
248
249impl Validate for VectorQuery {
250 fn validate(&self) -> Result<(), InvalidError> {
251 if self.field.is_empty()
252 || self.field.len() > MAX_QUERY_NAME_BYTES
253 || self.field.chars().any(char::is_control)
254 {
255 return Err(InvalidError::new("vector field is invalid"));
256 }
257 if self.embedding.is_empty() || self.embedding.len() > MAX_VECTOR_DIMENSIONS {
258 return Err(InvalidError::new(format!(
259 "vector dimensions must be in 1..={MAX_VECTOR_DIMENSIONS}"
260 )));
261 }
262 if self
263 .embedding
264 .iter()
265 .any(|value| !value.is_finite() || (*value == 0.0 && value.is_sign_negative()))
266 {
267 return Err(InvalidError::new(
268 "vector values must be finite canonical floats",
269 ));
270 }
271 if self.top_k == 0 || self.top_k > MAX_PAGE_SIZE as u32 {
272 return Err(InvalidError::new(format!(
273 "vector top_k {} is outside 1..={MAX_PAGE_SIZE}",
274 self.top_k
275 )));
276 }
277 Ok(())
278 }
279}
280
281impl Validate for MemoryRecord {
282 fn validate(&self) -> Result<(), InvalidError> {
283 if let MemoryRecord::Item { body, .. } = self
284 && body.len() > MAX_MEMORY_BODY_BYTES
285 {
286 return Err(InvalidError::new(format!(
287 "memory body is {}B, exceeds cap {MAX_MEMORY_BODY_BYTES}B",
288 body.len()
289 )));
290 }
291 Ok(())
292 }
293}
294
295impl Validate for ClientMetadata {
296 fn validate(&self) -> Result<(), InvalidError> {
297 let size = self.metadata.as_ref().map_or(0, Vec::len);
298 if size > MAX_CLIENT_METADATA {
299 return Err(InvalidError::new(format!(
300 "client metadata is {size}B, exceeds cap {MAX_CLIENT_METADATA}B"
301 )));
302 }
303 Ok(())
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use crate::batch::BatchItem;
311 use crate::codes::{AGDX_KV_GET_CODE, BATCH_OP_VERSION, KV_OP_VERSION};
312
313 #[test]
314 fn given_a_batch_over_the_op_cap_when_validated_then_should_reject() {
315 let request = BatchRequest {
316 v: BATCH_OP_VERSION,
317 ops: (0..MAX_BATCH_OPS + 1)
318 .map(|_| BatchItem {
319 code: AGDX_KV_GET_CODE,
320 payload: Vec::new(),
321 })
322 .collect(),
323 };
324 assert!(request.validate().is_err());
325 }
326
327 #[test]
328 fn given_a_batch_nesting_a_batch_when_validated_then_should_reject() {
329 let request = BatchRequest {
330 v: BATCH_OP_VERSION,
331 ops: vec![BatchItem {
332 code: AGDX_BATCH_CODE,
333 payload: Vec::new(),
334 }],
335 };
336 assert!(request.validate().is_err());
337 }
338
339 #[test]
340 fn given_namespaces_when_validated_then_should_enforce_bounds() {
341 use crate::kv::validate_namespace;
342 use crate::limits::MAX_NAMESPACE_BYTES;
343 assert!(validate_namespace("default").is_ok());
344 assert!(validate_namespace("agent-abc/session").is_ok(), "hierarchy");
345 assert!(validate_namespace("").is_err(), "empty");
346 assert!(validate_namespace("bad\nns").is_err(), "control byte");
347 assert!(validate_namespace(&"n".repeat(MAX_NAMESPACE_BYTES)).is_ok());
348 assert!(validate_namespace(&"n".repeat(MAX_NAMESPACE_BYTES + 1)).is_err());
349 }
350
351 #[test]
352 fn given_graph_names_when_validated_then_should_enforce_bounds() {
353 use crate::graph::validate_graph_name;
354 use crate::limits::MAX_GRAPH_NAME_BYTES;
355 assert!(validate_graph_name("knowledge").is_ok());
356 assert!(validate_graph_name("").is_err(), "empty");
357 assert!(validate_graph_name("bad\tname").is_err(), "control byte");
358 assert!(validate_graph_name(&"g".repeat(MAX_GRAPH_NAME_BYTES + 1)).is_err());
359 }
360
361 #[test]
362 fn given_an_oversized_key_when_validated_then_should_reject_and_a_valid_one_passes() {
363 let over = KvSet {
364 v: KV_OP_VERSION,
365 namespace: "ns".to_owned(),
366 key: vec![b'x'; MAX_KEY_BYTES + 1],
367 value: vec![1, 2, 3],
368 expires_at_micros: None,
369 };
370 assert!(over.validate().is_err());
371 let ok = KvSet {
372 v: KV_OP_VERSION,
373 namespace: "ns".to_owned(),
374 key: vec![b'x'; 8],
375 value: vec![1, 2, 3],
376 expires_at_micros: None,
377 };
378 assert!(ok.validate().is_ok());
379 }
380
381 fn node_with(labels: Vec<String>, attrs: Vec<(String, Value)>) -> GraphNode {
382 GraphNode {
383 id: crate::graph::NodeId::from_u128(1),
384 labels,
385 attrs,
386 embedding: None,
387 source: None,
388 }
389 }
390
391 #[test]
392 fn given_a_node_over_the_label_cap_when_validated_then_should_reject() {
393 let over = node_with(
394 (0..=MAX_GRAPH_NODE_LABELS)
395 .map(|index| format!("label-{index}"))
396 .collect(),
397 Vec::new(),
398 );
399 assert!(over.validate().is_err());
400 assert!(
401 node_with(vec!["Person".to_owned()], Vec::new())
402 .validate()
403 .is_ok()
404 );
405 }
406
407 #[test]
408 fn given_a_node_over_the_attribute_cap_when_validated_then_should_reject() {
409 let over = node_with(
410 Vec::new(),
411 (0..=MAX_METADATA_ENTRIES)
412 .map(|index| (format!("attr-{index}"), Value::from("x")))
413 .collect(),
414 );
415 assert!(over.validate().is_err());
416 }
417
418 #[test]
419 fn given_an_oversized_source_reference_when_validated_then_should_reject() {
420 let over = SourceRef::Kv {
421 namespace: "n".repeat(MAX_SOURCE_REF_BYTES),
422 key: "k".repeat(MAX_SOURCE_REF_BYTES),
423 };
424 assert!(over.validate().is_err());
425 assert!(
426 SourceRef::Memory {
427 id: "01KWM3K3XEP3NP5TN850J17YBP".to_owned()
428 }
429 .validate()
430 .is_ok()
431 );
432 }
433
434 #[test]
435 fn given_an_upsert_over_the_element_cap_when_validated_then_should_reject() {
436 let over = GraphUpsert {
437 v: 1,
438 graph: "knowledge".to_owned(),
439 nodes: (0..=MAX_GRAPH_RESULT_ELEMENTS)
440 .map(|_| node_with(Vec::new(), Vec::new()))
441 .collect(),
442 edges: Vec::new(),
443 };
444 assert!(over.validate().is_err());
445 let ok = GraphUpsert {
446 v: 1,
447 graph: "knowledge".to_owned(),
448 nodes: vec![node_with(vec!["Person".to_owned()], Vec::new())],
449 edges: Vec::new(),
450 };
451 assert!(ok.validate().is_ok());
452 }
453
454 #[test]
455 fn given_an_upsert_carrying_an_over_cap_node_when_validated_then_should_reject() {
456 let upsert = GraphUpsert {
457 v: 1,
458 graph: "knowledge".to_owned(),
459 nodes: vec![node_with(
460 (0..=MAX_GRAPH_NODE_LABELS)
461 .map(|index| format!("label-{index}"))
462 .collect(),
463 Vec::new(),
464 )],
465 edges: Vec::new(),
466 };
467 assert!(
468 upsert.validate().is_err(),
469 "an upsert must enforce its elements' caps, not just its own"
470 );
471 }
472
473 #[test]
474 fn given_an_oversized_text_query_when_validated_then_should_reject() {
475 let over = TextQuery {
476 field: None,
477 query: "q".repeat(MAX_TEXT_QUERY_BYTES + 1),
478 };
479 assert!(over.validate().is_err());
480 let ok = TextQuery {
481 field: None,
482 query: "checkout is slow".to_owned(),
483 };
484 assert!(ok.validate().is_ok());
485 }
486
487 #[test]
488 fn given_an_over_cap_vector_top_k_when_validated_then_should_reject() {
489 let over = VectorQuery {
490 field: "embedding".to_owned(),
491 embedding: vec![0.0; 4],
492 top_k: MAX_PAGE_SIZE as u32 + 1,
493 };
494 assert!(over.validate().is_err());
495 let ok = VectorQuery {
496 field: "embedding".to_owned(),
497 embedding: vec![0.0; 4],
498 top_k: 10,
499 };
500 assert!(ok.validate().is_ok());
501 }
502
503 #[test]
504 fn given_an_oversized_memory_body_when_validated_then_should_reject() {
505 let over = MemoryRecord::Item {
506 id: "01KWM3K3XEP3NP5TN850J17YBP".to_owned(),
507 kind: "fact".to_owned(),
508 body: vec![0u8; MAX_MEMORY_BODY_BYTES + 1],
509 };
510 assert!(over.validate().is_err());
511 let ok = MemoryRecord::Item {
512 id: "01KWM3K3XEP3NP5TN850J17YBP".to_owned(),
513 kind: "fact".to_owned(),
514 body: b"checkout is slow".to_vec(),
515 };
516 assert!(ok.validate().is_ok());
517 assert!(
518 MemoryRecord::Forget {
519 target: "01KWM3K3XEP3NP5TN850J17YBP".to_owned()
520 }
521 .validate()
522 .is_ok()
523 );
524 }
525
526 #[test]
527 fn given_oversized_client_metadata_when_validated_then_should_reject() {
528 let over = ClientMetadata {
529 client_id: 1,
530 user_id: None,
531 transport: 1,
532 address: "127.0.0.1:8090".to_owned(),
533 consumer_groups_count: 0,
534 metadata: Some(vec![0u8; MAX_CLIENT_METADATA + 1]),
535 };
536 assert!(over.validate().is_err());
537 let ok = ClientMetadata {
538 client_id: 1,
539 user_id: None,
540 transport: 1,
541 address: "127.0.0.1:8090".to_owned(),
542 consumer_groups_count: 0,
543 metadata: Some(b"card".to_vec()),
544 };
545 assert!(ok.validate().is_ok());
546 }
547}