1use crate::batch::BatchRequest;
2use crate::browse::RegisterSchema;
3use crate::clients::ClientMetadata;
4use crate::codes::AGDX_BATCH_CODE;
5use crate::codes::KV_LEASE_OP_VERSION;
6use crate::error::InvalidError;
7use crate::graph::{GraphEdge, GraphNode, GraphQuery, GraphUpsert, SourceRef};
8use crate::kv::{KvCasFenced, KvLease, KvLeaseRenew, KvRelease, KvScan, KvSet};
9use crate::limits::{
10 MAX_BATCH_OPS, MAX_CLIENT_METADATA, MAX_FRAME_BYTES, MAX_GRAPH_LABEL_BYTES,
11 MAX_GRAPH_NODE_LABELS, MAX_GRAPH_RESULT_ELEMENTS, MAX_GRAPH_TRAVERSE_DEPTH,
12 MAX_HOLDER_ID_BYTES, MAX_KEY_BYTES, MAX_LEASE_TTL_MICROS, MAX_MEMORY_BODY_BYTES,
13 MAX_MEMORY_ID_BYTES, MAX_METADATA_ENTRIES, MAX_METADATA_KEY_BYTES, MAX_PAGE_SIZE,
14 MAX_QUERY_NAME_BYTES, MAX_SCAN_LIMIT, MAX_SCHEMA_NAME_BYTES, MAX_SOURCE_REF_BYTES,
15 MAX_TEXT_QUERY_BYTES, MAX_VALUE_BYTES, MAX_VECTOR_DIMENSIONS, MIN_LEASE_TTL_MICROS,
16};
17use crate::memory::MemoryRecord;
18use crate::query::{TextQuery, Value, VectorQuery};
19
20pub trait Validate {
26 fn validate(&self) -> Result<(), InvalidError>;
28}
29
30pub(crate) fn validate_safelisted_name(
36 label: &str,
37 value: &str,
38 cap: usize,
39) -> Result<(), InvalidError> {
40 if value.is_empty() {
41 return Err(InvalidError::new(format!("{label} must not be empty")));
42 }
43 if value.len() > cap {
44 return Err(InvalidError::new(format!(
45 "{label} is {}B, exceeds cap {cap}B",
46 value.len()
47 )));
48 }
49 if let Some(bad) = value
50 .bytes()
51 .find(|byte| !matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' | b'.'))
52 {
53 return Err(InvalidError::new(format!(
54 "{label} has a disallowed byte {bad:#04x}: allowed are ASCII letters, digits, '-', '_', '.'"
55 )));
56 }
57 Ok(())
58}
59
60pub(crate) fn validate_bounded_text(
63 label: &str,
64 value: &str,
65 cap: usize,
66) -> Result<(), InvalidError> {
67 if value.is_empty() {
68 return Err(InvalidError::new(format!("{label} must not be empty")));
69 }
70 if value.len() > cap {
71 return Err(InvalidError::new(format!(
72 "{label} is {}B, exceeds cap {cap}B",
73 value.len()
74 )));
75 }
76 if value.chars().any(char::is_control) {
77 return Err(InvalidError::new(format!(
78 "{label} contains a control character"
79 )));
80 }
81 Ok(())
82}
83
84impl Validate for BatchRequest {
85 fn validate(&self) -> Result<(), InvalidError> {
86 if self.ops.len() > MAX_BATCH_OPS {
87 return Err(InvalidError::new(format!(
88 "batch has {} ops, exceeds cap {MAX_BATCH_OPS}",
89 self.ops.len()
90 )));
91 }
92 let mut total = 0usize;
93 for item in &self.ops {
94 if item.code == AGDX_BATCH_CODE {
97 return Err(InvalidError::new(
98 "a batch may not contain a batch op".to_owned(),
99 ));
100 }
101 if item.payload.len() > MAX_VALUE_BYTES {
102 return Err(InvalidError::new(format!(
103 "batch op payload is {}B, exceeds cap {MAX_VALUE_BYTES}B",
104 item.payload.len()
105 )));
106 }
107 total = total.saturating_add(item.payload.len());
108 }
109 if total > MAX_FRAME_BYTES {
110 return Err(InvalidError::new(format!(
111 "batch total payload is {total}B, exceeds cap {MAX_FRAME_BYTES}B"
112 )));
113 }
114 Ok(())
115 }
116}
117
118impl Validate for KvSet {
119 fn validate(&self) -> Result<(), InvalidError> {
120 crate::kv::validate_namespace(&self.namespace)?;
121 if self.key.is_empty() {
122 return Err(InvalidError::new("key-value key is empty".to_owned()));
123 }
124 if self.key.len() > MAX_KEY_BYTES {
125 return Err(InvalidError::new(format!(
126 "key is {}B, exceeds cap {MAX_KEY_BYTES}B",
127 self.key.len()
128 )));
129 }
130 if self.value.len() > MAX_VALUE_BYTES {
131 return Err(InvalidError::new(format!(
132 "value is {}B, exceeds cap {MAX_VALUE_BYTES}B",
133 self.value.len()
134 )));
135 }
136 Ok(())
137 }
138}
139
140impl Validate for KvScan {
141 fn validate(&self) -> Result<(), InvalidError> {
142 crate::kv::validate_namespace(&self.namespace)?;
143 if self.limit > MAX_SCAN_LIMIT {
144 return Err(InvalidError::new(format!(
145 "scan limit {} exceeds cap {MAX_SCAN_LIMIT}",
146 self.limit
147 )));
148 }
149 Ok(())
150 }
151}
152
153fn validate_lease_shape(
157 v: u32,
158 namespace: &str,
159 key: &[u8],
160 holder_id: &str,
161) -> Result<(), InvalidError> {
162 if v != KV_LEASE_OP_VERSION {
163 return Err(InvalidError::new(format!(
164 "unsupported fenced-lease op version (expected {KV_LEASE_OP_VERSION}, got {v})"
165 )));
166 }
167 crate::kv::validate_namespace(namespace)?;
168 validate_kv_key("lease key", key)?;
169 if holder_id.is_empty() {
170 return Err(InvalidError::new("lease holder id must not be empty"));
171 }
172 if holder_id.len() > MAX_HOLDER_ID_BYTES {
173 return Err(InvalidError::new(format!(
174 "lease holder id is {}B, exceeds cap {MAX_HOLDER_ID_BYTES}B",
175 holder_id.len()
176 )));
177 }
178 if holder_id.chars().any(char::is_control) {
179 return Err(InvalidError::new(
180 "lease holder id contains a control character",
181 ));
182 }
183 Ok(())
184}
185
186fn validate_lease_ttl(ttl_micros: u64) -> Result<(), InvalidError> {
193 if !(MIN_LEASE_TTL_MICROS..=MAX_LEASE_TTL_MICROS).contains(&ttl_micros) {
194 return Err(InvalidError::new(format!(
195 "lease ttl is {ttl_micros}\u{b5}s, outside the supported range \
196 {MIN_LEASE_TTL_MICROS}..={MAX_LEASE_TTL_MICROS}\u{b5}s"
197 )));
198 }
199 Ok(())
200}
201
202fn validate_kv_key(label: &str, key: &[u8]) -> Result<(), InvalidError> {
203 if key.is_empty() {
204 return Err(InvalidError::new(format!("{label} is empty")));
205 }
206 if key.len() > MAX_KEY_BYTES {
207 return Err(InvalidError::new(format!(
208 "{label} is {}B, exceeds cap {MAX_KEY_BYTES}B",
209 key.len()
210 )));
211 }
212 Ok(())
213}
214
215impl Validate for KvLease {
216 fn validate(&self) -> Result<(), InvalidError> {
217 validate_lease_shape(self.v, &self.namespace, &self.key, &self.holder_id)?;
218 validate_lease_ttl(self.lease_ttl_micros)?;
219 Ok(())
220 }
221}
222
223impl Validate for KvLeaseRenew {
224 fn validate(&self) -> Result<(), InvalidError> {
225 validate_lease_shape(self.v, &self.namespace, &self.key, &self.holder_id)?;
226 validate_lease_ttl(self.lease_ttl_micros)?;
227 if self.lease_token == 0 {
228 return Err(InvalidError::new("lease token must not be zero"));
229 }
230 Ok(())
231 }
232}
233
234impl Validate for KvRelease {
235 fn validate(&self) -> Result<(), InvalidError> {
236 validate_lease_shape(self.v, &self.namespace, &self.key, &self.holder_id)?;
237 if self.lease_token == 0 {
238 return Err(InvalidError::new("lease token must not be zero"));
239 }
240 Ok(())
241 }
242}
243
244impl Validate for KvCasFenced {
245 fn validate(&self) -> Result<(), InvalidError> {
246 if self.v != KV_LEASE_OP_VERSION {
247 return Err(InvalidError::new(format!(
248 "unsupported fenced-lease op version (expected {KV_LEASE_OP_VERSION}, got {})",
249 self.v
250 )));
251 }
252 crate::kv::validate_namespace(&self.namespace)?;
253 crate::kv::validate_namespace(&self.fence_namespace)?;
254 validate_kv_key("key-value key", &self.key)?;
255 validate_kv_key("fence key", &self.fence_key)?;
256 if self.value.len() > MAX_VALUE_BYTES {
257 return Err(InvalidError::new(format!(
258 "value is {}B, exceeds cap {MAX_VALUE_BYTES}B",
259 self.value.len()
260 )));
261 }
262 if self.fence_token == 0 {
264 return Err(InvalidError::new("fence token must not be zero"));
265 }
266 Ok(())
267 }
268}
269
270impl Validate for GraphQuery {
271 fn validate(&self) -> Result<(), InvalidError> {
272 crate::graph::validate_graph_name(&self.graph)?;
273 if self.traverse.len() > MAX_GRAPH_TRAVERSE_DEPTH as usize {
274 return Err(InvalidError::new(format!(
275 "graph traversal depth {} exceeds cap {MAX_GRAPH_TRAVERSE_DEPTH}",
276 self.traverse.len()
277 )));
278 }
279 if self.limit > MAX_GRAPH_RESULT_ELEMENTS {
280 return Err(InvalidError::new(format!(
281 "graph result limit {} exceeds cap {MAX_GRAPH_RESULT_ELEMENTS}",
282 self.limit
283 )));
284 }
285 Ok(())
286 }
287}
288
289impl Validate for SourceRef {
290 fn validate(&self) -> Result<(), InvalidError> {
291 let size = match self {
292 SourceRef::Message { conversation, .. } => conversation.as_ref().map_or(0, String::len),
295 SourceRef::Kv { namespace, key } => namespace.len() + key.len(),
296 SourceRef::Memory { id } => id.len(),
297 };
298 if size > MAX_SOURCE_REF_BYTES {
299 return Err(InvalidError::new(format!(
300 "source reference is {size}B, exceeds cap {MAX_SOURCE_REF_BYTES}B"
301 )));
302 }
303 Ok(())
304 }
305}
306
307fn validate_attrs(label: &str, attrs: &[(String, Value)]) -> Result<(), InvalidError> {
309 if attrs.len() > MAX_METADATA_ENTRIES {
310 return Err(InvalidError::new(format!(
311 "{label} carries {} attributes, exceeds cap {MAX_METADATA_ENTRIES}",
312 attrs.len()
313 )));
314 }
315 for (key, _) in attrs {
316 if key.len() > MAX_METADATA_KEY_BYTES {
317 return Err(InvalidError::new(format!(
318 "{label} attribute name is {}B, exceeds cap {MAX_METADATA_KEY_BYTES}B",
319 key.len()
320 )));
321 }
322 }
323 Ok(())
324}
325
326impl Validate for GraphNode {
327 fn validate(&self) -> Result<(), InvalidError> {
328 if self.labels.len() > MAX_GRAPH_NODE_LABELS {
329 return Err(InvalidError::new(format!(
330 "graph node carries {} labels, exceeds cap {MAX_GRAPH_NODE_LABELS}",
331 self.labels.len()
332 )));
333 }
334 for label in &self.labels {
335 validate_bounded_text("graph node label", label, MAX_GRAPH_LABEL_BYTES)?;
336 }
337 validate_attrs("graph node", &self.attrs)?;
338 if let Some(source) = &self.source {
339 source.validate()?;
340 }
341 Ok(())
342 }
343}
344
345impl Validate for GraphEdge {
346 fn validate(&self) -> Result<(), InvalidError> {
347 validate_bounded_text("graph edge type", &self.edge_type, MAX_GRAPH_LABEL_BYTES)?;
348 validate_attrs("graph edge", &self.attrs)?;
349 if let Some(source) = &self.source {
350 source.validate()?;
351 }
352 Ok(())
353 }
354}
355
356impl Validate for GraphUpsert {
357 fn validate(&self) -> Result<(), InvalidError> {
358 crate::graph::validate_graph_name(&self.graph)?;
359 let elements = self.nodes.len() + self.edges.len();
360 if elements > MAX_GRAPH_RESULT_ELEMENTS {
361 return Err(InvalidError::new(format!(
362 "graph upsert carries {elements} elements, exceeds cap {MAX_GRAPH_RESULT_ELEMENTS}"
363 )));
364 }
365 for node in &self.nodes {
366 node.validate()?;
367 }
368 for edge in &self.edges {
369 edge.validate()?;
370 }
371 Ok(())
372 }
373}
374
375impl Validate for TextQuery {
376 fn validate(&self) -> Result<(), InvalidError> {
377 if self.query.trim().is_empty() || self.query.len() > MAX_TEXT_QUERY_BYTES {
378 return Err(InvalidError::new(format!(
379 "text query is {}B, expected 1..={MAX_TEXT_QUERY_BYTES}B",
380 self.query.len()
381 )));
382 }
383 if self.query.chars().any(char::is_control) {
384 return Err(InvalidError::new("text query contains a control character"));
385 }
386 if let Some(field) = &self.field
387 && (field.is_empty()
388 || field.len() > MAX_QUERY_NAME_BYTES
389 || field.chars().any(char::is_control))
390 {
391 return Err(InvalidError::new("text query field is invalid"));
392 }
393 Ok(())
394 }
395}
396
397impl Validate for VectorQuery {
398 fn validate(&self) -> Result<(), InvalidError> {
399 if self.field.is_empty()
400 || self.field.len() > MAX_QUERY_NAME_BYTES
401 || self.field.chars().any(char::is_control)
402 {
403 return Err(InvalidError::new("vector field is invalid"));
404 }
405 if self.embedding.is_empty() || self.embedding.len() > MAX_VECTOR_DIMENSIONS {
406 return Err(InvalidError::new(format!(
407 "vector dimensions must be in 1..={MAX_VECTOR_DIMENSIONS}"
408 )));
409 }
410 if self
411 .embedding
412 .iter()
413 .any(|value| !value.is_finite() || (*value == 0.0 && value.is_sign_negative()))
414 {
415 return Err(InvalidError::new(
416 "vector values must be finite canonical floats",
417 ));
418 }
419 if self.top_k == 0 || self.top_k > MAX_PAGE_SIZE as u32 {
420 return Err(InvalidError::new(format!(
421 "vector top_k {} is outside 1..={MAX_PAGE_SIZE}",
422 self.top_k
423 )));
424 }
425 Ok(())
426 }
427}
428
429impl Validate for MemoryRecord {
430 fn validate(&self) -> Result<(), InvalidError> {
431 match self {
432 MemoryRecord::Item { id, kind, .. } => {
433 validate_bounded_text("memory item id", id, MAX_MEMORY_ID_BYTES)?;
434 validate_bounded_text("memory item kind", kind, MAX_MEMORY_ID_BYTES)?;
435 }
436 MemoryRecord::Forget { target, .. } | MemoryRecord::Feedback { target, .. } => {
437 validate_bounded_text("memory target", target, MAX_MEMORY_ID_BYTES)?;
438 }
439 }
440 if let MemoryRecord::Item { body, .. } = self
441 && body.len() > MAX_MEMORY_BODY_BYTES
442 {
443 return Err(InvalidError::new(format!(
444 "memory body is {}B, exceeds cap {MAX_MEMORY_BODY_BYTES}B",
445 body.len()
446 )));
447 }
448 if let MemoryRecord::Forget {
449 conversation: Some(conversation),
450 ..
451 }
452 | MemoryRecord::Feedback {
453 conversation: Some(conversation),
454 ..
455 } = self
456 {
457 conversation
458 .parse::<crate::agent::ConversationId>()
459 .map_err(|_| InvalidError::new("memory record conversation is not a valid id"))?;
460 }
461 Ok(())
462 }
463}
464
465impl Validate for RegisterSchema {
466 fn validate(&self) -> Result<(), InvalidError> {
467 if let Some(name) = &self.name {
468 validate_bounded_text("schema name", name, MAX_SCHEMA_NAME_BYTES)?;
469 }
470 Ok(())
471 }
472}
473
474impl Validate for ClientMetadata {
475 fn validate(&self) -> Result<(), InvalidError> {
476 let size = self.metadata.as_ref().map_or(0, Vec::len);
477 if size > MAX_CLIENT_METADATA {
478 return Err(InvalidError::new(format!(
479 "client metadata is {size}B, exceeds cap {MAX_CLIENT_METADATA}B"
480 )));
481 }
482 Ok(())
483 }
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use crate::batch::BatchItem;
490 use crate::codes::{
491 AGDX_KV_GET_CODE, BATCH_OP_VERSION, KV_LEASE_OP_VERSION, KV_OP_VERSION, QUERY_OP_VERSION,
492 };
493 use crate::control::SchemaSource;
494 use crate::limits::{MAX_GRAPH_LABEL_BYTES, MAX_MEMORY_ID_BYTES, MAX_SCHEMA_NAME_BYTES};
495
496 #[test]
497 fn given_an_invalid_graph_label_or_edge_type_when_upserted_then_should_refuse_it() {
498 let at_cap = "l".repeat(MAX_GRAPH_LABEL_BYTES);
499 let alice = node_with(vec![at_cap.clone()], Vec::new());
500 let acme = node_with(vec!["Company".to_owned()], Vec::new());
501 alice.validate().expect("a node label at the cap is valid");
502 GraphEdge::relate(&alice, at_cap, &acme)
503 .validate()
504 .expect("an edge type at the cap is valid");
505 let long = "l".repeat(MAX_GRAPH_LABEL_BYTES + 1);
506 for invalid in [String::new(), long, "Per\nson".to_owned()] {
507 let node = node_with(vec![invalid.clone()], Vec::new());
508 assert!(node.validate().is_err(), "{invalid:?}");
509 assert!(
510 GraphEdge::relate(&alice, invalid.clone(), &acme)
511 .validate()
512 .is_err()
513 );
514 let upsert = GraphUpsert {
515 v: 1,
516 graph: "knowledge".to_owned(),
517 session: None,
518 nodes: vec![node],
519 edges: Vec::new(),
520 };
521 assert!(upsert.validate().is_err());
522 }
523 }
524
525 #[test]
526 fn given_an_invalid_memory_id_kind_or_target_when_validated_then_should_refuse_it() {
527 let item = |id: &str, kind: &str| MemoryRecord::Item {
528 origin: None,
529 producer: None,
530 id: id.to_owned(),
531 kind: kind.to_owned(),
532 body: b"auth is slow".to_vec(),
533 };
534 let at_cap = "m".repeat(MAX_MEMORY_ID_BYTES);
535 item(&at_cap, &at_cap)
536 .validate()
537 .expect("an id and a kind at the cap are valid");
538 for invalid in [
539 String::new(),
540 "m".repeat(MAX_MEMORY_ID_BYTES + 1),
541 "fa\u{0}ct".to_owned(),
542 ] {
543 assert!(item(&invalid, "fact").validate().is_err(), "{invalid:?}");
544 assert!(
545 item("01KWM3K3XEP3NP5TN850J17YBP", &invalid)
546 .validate()
547 .is_err()
548 );
549 assert!(
550 MemoryRecord::Forget {
551 target: invalid.clone(),
552 conversation: None,
553 }
554 .validate()
555 .is_err()
556 );
557 assert!(
558 MemoryRecord::Feedback {
559 target: invalid,
560 weight: 1.0,
561 conversation: None,
562 }
563 .validate()
564 .is_err()
565 );
566 }
567 }
568
569 #[test]
570 fn given_a_lease_holder_with_a_control_character_when_validated_then_should_refuse_it() {
571 let holder = "node\n1".to_owned();
572 let lease = KvLease {
573 v: KV_LEASE_OP_VERSION,
574 namespace: "connectors.coordination".to_owned(),
575 key: b"source-owner".to_vec(),
576 lease_ttl_micros: 30_000_000,
577 holder_id: holder.clone(),
578 subject_user_id: None,
579 };
580 assert!(lease.validate().is_err());
581 let renew = KvLeaseRenew {
582 v: KV_LEASE_OP_VERSION,
583 namespace: "connectors.coordination".to_owned(),
584 key: b"source-owner".to_vec(),
585 holder_id: holder.clone(),
586 subject_user_id: None,
587 lease_token: 7,
588 lease_ttl_micros: 30_000_000,
589 };
590 assert!(renew.validate().is_err());
591 let release = KvRelease {
592 v: KV_LEASE_OP_VERSION,
593 namespace: "connectors.coordination".to_owned(),
594 key: b"source-owner".to_vec(),
595 lease_token: 7,
596 holder_id: holder,
597 };
598 assert!(release.validate().is_err());
599 }
600
601 #[test]
602 fn given_an_invalid_schema_name_when_a_registration_is_validated_then_should_refuse_it() {
603 let register = |name: Option<String>| RegisterSchema {
604 v: QUERY_OP_VERSION,
605 source: SchemaSource::JsonSchema {
606 schema: "{}".to_owned(),
607 },
608 name,
609 version: None,
610 stream: Some("fleet".to_owned()),
611 };
612 register(None)
613 .validate()
614 .expect("a registration without a name is valid");
615 register(Some("reading".to_owned()))
616 .validate()
617 .expect("a short schema name is valid");
618 assert!(register(Some(String::new())).validate().is_err());
619 assert!(
620 register(Some("s".repeat(MAX_SCHEMA_NAME_BYTES + 1)))
621 .validate()
622 .is_err()
623 );
624 assert!(register(Some("read\ting".to_owned())).validate().is_err());
625 }
626
627 #[test]
628 fn given_a_batch_over_the_op_cap_when_validated_then_should_reject() {
629 let request = BatchRequest {
630 v: BATCH_OP_VERSION,
631 ops: (0..MAX_BATCH_OPS + 1)
632 .map(|_| BatchItem {
633 code: AGDX_KV_GET_CODE,
634 payload: Vec::new(),
635 })
636 .collect(),
637 };
638 assert!(request.validate().is_err());
639 }
640
641 #[test]
642 fn given_a_batch_nesting_a_batch_when_validated_then_should_reject() {
643 let request = BatchRequest {
644 v: BATCH_OP_VERSION,
645 ops: vec![BatchItem {
646 code: AGDX_BATCH_CODE,
647 payload: Vec::new(),
648 }],
649 };
650 assert!(request.validate().is_err());
651 }
652
653 #[test]
654 fn given_namespaces_when_validated_then_should_enforce_bounds() {
655 use crate::kv::validate_namespace;
656 use crate::limits::MAX_NAMESPACE_BYTES;
657 assert!(validate_namespace("default").is_ok());
658 assert!(validate_namespace("agent-abc/session").is_ok(), "hierarchy");
659 assert!(validate_namespace("").is_err(), "empty");
660 assert!(validate_namespace("bad\nns").is_err(), "control byte");
661 assert!(validate_namespace(&"n".repeat(MAX_NAMESPACE_BYTES)).is_ok());
662 assert!(validate_namespace(&"n".repeat(MAX_NAMESPACE_BYTES + 1)).is_err());
663 }
664
665 #[test]
666 fn given_graph_names_when_validated_then_should_enforce_bounds() {
667 use crate::graph::validate_graph_name;
668 use crate::limits::MAX_GRAPH_NAME_BYTES;
669 assert!(validate_graph_name("knowledge").is_ok());
670 assert!(validate_graph_name("").is_err(), "empty");
671 assert!(validate_graph_name("bad\tname").is_err(), "control byte");
672 assert!(validate_graph_name(&"g".repeat(MAX_GRAPH_NAME_BYTES + 1)).is_err());
673 }
674
675 #[test]
676 fn given_a_lease_request_when_validated_then_should_enforce_version_holder_and_ttl() {
677 use crate::codes::KV_LEASE_OP_VERSION;
678 use crate::kv::KvLease;
679 use crate::limits::MAX_HOLDER_ID_BYTES;
680 let valid = KvLease {
681 v: KV_LEASE_OP_VERSION,
682 namespace: "connectors.coordination".to_owned(),
683 key: b"source-owner".to_vec(),
684 lease_ttl_micros: 30_000_000,
685 holder_id: "node-1".to_owned(),
686 subject_user_id: Some(42),
687 };
688 assert!(valid.validate().is_ok());
689 let wrong_version = KvLease {
690 v: KV_LEASE_OP_VERSION + 1,
691 ..valid.clone()
692 };
693 assert!(
694 wrong_version.validate().is_err(),
695 "an unsupported version is rejected typed"
696 );
697 let empty_holder = KvLease {
698 holder_id: String::new(),
699 ..valid.clone()
700 };
701 assert!(empty_holder.validate().is_err());
702 let long_holder = KvLease {
703 holder_id: "h".repeat(MAX_HOLDER_ID_BYTES + 1),
704 ..valid.clone()
705 };
706 assert!(long_holder.validate().is_err());
707 let zero_ttl = KvLease {
708 lease_ttl_micros: 0,
709 ..valid
710 };
711 assert!(zero_ttl.validate().is_err());
712 }
713
714 #[test]
717 fn given_out_of_range_lease_ttls_when_validated_then_should_reject_both_ends() {
718 use crate::codes::KV_LEASE_OP_VERSION;
719 use crate::kv::{KvLease, KvLeaseRenew};
720 use crate::limits::{MAX_LEASE_TTL_MICROS, MIN_LEASE_TTL_MICROS};
721 let acquire = KvLease {
722 v: KV_LEASE_OP_VERSION,
723 namespace: "connectors.coordination".to_owned(),
724 key: b"source-owner".to_vec(),
725 lease_ttl_micros: MIN_LEASE_TTL_MICROS,
726 holder_id: "node-1".to_owned(),
727 subject_user_id: None,
728 };
729 assert!(acquire.validate().is_ok(), "the floor itself is grantable");
730 assert!(
731 KvLease {
732 lease_ttl_micros: MAX_LEASE_TTL_MICROS,
733 ..acquire.clone()
734 }
735 .validate()
736 .is_ok(),
737 "the ceiling itself is grantable"
738 );
739 assert!(
740 KvLease {
741 lease_ttl_micros: MIN_LEASE_TTL_MICROS - 1,
742 ..acquire.clone()
743 }
744 .validate()
745 .is_err(),
746 "a lease too short to renew is rejected locally"
747 );
748 assert!(
749 KvLease {
750 lease_ttl_micros: MAX_LEASE_TTL_MICROS + 1,
751 ..acquire
752 }
753 .validate()
754 .is_err(),
755 "an unbounded ownership window is rejected locally"
756 );
757 let renew = KvLeaseRenew {
758 v: KV_LEASE_OP_VERSION,
759 namespace: "connectors.coordination".to_owned(),
760 key: b"source-owner".to_vec(),
761 holder_id: "node-1".to_owned(),
762 subject_user_id: None,
763 lease_token: 7,
764 lease_ttl_micros: MAX_LEASE_TTL_MICROS + 1,
765 };
766 assert!(renew.validate().is_err(), "renewal shares the same range");
767 assert!(
768 KvLeaseRenew {
769 lease_ttl_micros: MIN_LEASE_TTL_MICROS - 1,
770 ..renew
771 }
772 .validate()
773 .is_err()
774 );
775 }
776
777 #[test]
778 fn given_renew_and_release_requests_when_validated_then_should_require_a_nonzero_token() {
779 use crate::codes::KV_LEASE_OP_VERSION;
780 use crate::kv::{KvLeaseRenew, KvRelease};
781 let renew = KvLeaseRenew {
782 v: KV_LEASE_OP_VERSION,
783 namespace: "connectors.coordination".to_owned(),
784 key: b"source-owner".to_vec(),
785 holder_id: "node-1".to_owned(),
786 subject_user_id: None,
787 lease_token: 7,
788 lease_ttl_micros: 30_000_000,
789 };
790 assert!(renew.validate().is_ok());
791 let zero = KvLeaseRenew {
792 lease_token: 0,
793 ..renew
794 };
795 assert!(zero.validate().is_err());
796 let release = KvRelease {
797 v: KV_LEASE_OP_VERSION,
798 namespace: "connectors.coordination".to_owned(),
799 key: b"source-owner".to_vec(),
800 lease_token: 7,
801 holder_id: "node-1".to_owned(),
802 };
803 assert!(release.validate().is_ok());
804 let zero = KvRelease {
805 lease_token: 0,
806 ..release
807 };
808 assert!(zero.validate().is_err());
809 }
810
811 #[test]
812 fn given_a_fenced_cas_when_validated_then_should_enforce_both_namespaces_and_the_token() {
813 use crate::codes::KV_LEASE_OP_VERSION;
814 use crate::kv::{CasExpect, KvCasFenced};
815 let valid = KvCasFenced {
816 v: KV_LEASE_OP_VERSION,
817 namespace: "connectors.state".to_owned(),
818 key: b"source_state/pg".to_vec(),
819 value: b"cursor".to_vec(),
820 expires_at_micros: None,
821 expect: CasExpect::Absent,
822 fence_namespace: "connectors.coordination".to_owned(),
823 fence_key: b"source-owner".to_vec(),
824 fence_token: 8,
825 };
826 assert!(valid.validate().is_ok());
827 let wrong_version = KvCasFenced {
828 v: KV_LEASE_OP_VERSION + 1,
829 ..valid.clone()
830 };
831 assert!(
832 wrong_version.validate().is_err(),
833 "an unsupported version is rejected typed"
834 );
835 let bad_fence_namespace = KvCasFenced {
836 fence_namespace: "bad\nns".to_owned(),
837 ..valid.clone()
838 };
839 assert!(bad_fence_namespace.validate().is_err());
840 let zero_token = KvCasFenced {
841 fence_token: 0,
842 ..valid
843 };
844 assert!(zero_token.validate().is_err());
845 }
846
847 #[test]
848 fn given_an_oversized_key_when_validated_then_should_reject_and_a_valid_one_passes() {
849 let over = KvSet {
850 v: KV_OP_VERSION,
851 namespace: "ns".to_owned(),
852 session: None,
853 key: vec![b'x'; MAX_KEY_BYTES + 1],
854 value: vec![1, 2, 3],
855 expires_at_micros: None,
856 };
857 assert!(over.validate().is_err());
858 let ok = KvSet {
859 v: KV_OP_VERSION,
860 namespace: "ns".to_owned(),
861 session: None,
862 key: vec![b'x'; 8],
863 value: vec![1, 2, 3],
864 expires_at_micros: None,
865 };
866 assert!(ok.validate().is_ok());
867 }
868
869 fn node_with(labels: Vec<String>, attrs: Vec<(String, Value)>) -> GraphNode {
870 GraphNode {
871 producer: None,
872 id: crate::graph::NodeId::from_u128(1),
873 labels,
874 attrs,
875 embedding: None,
876 source: None,
877 }
878 }
879
880 #[test]
881 fn given_a_node_over_the_label_cap_when_validated_then_should_reject() {
882 let over = node_with(
883 (0..=MAX_GRAPH_NODE_LABELS)
884 .map(|index| format!("label-{index}"))
885 .collect(),
886 Vec::new(),
887 );
888 assert!(over.validate().is_err());
889 assert!(
890 node_with(vec!["Person".to_owned()], Vec::new())
891 .validate()
892 .is_ok()
893 );
894 }
895
896 #[test]
897 fn given_a_node_over_the_attribute_cap_when_validated_then_should_reject() {
898 let over = node_with(
899 Vec::new(),
900 (0..=MAX_METADATA_ENTRIES)
901 .map(|index| (format!("attr-{index}"), Value::from("x")))
902 .collect(),
903 );
904 assert!(over.validate().is_err());
905 }
906
907 #[test]
908 fn given_an_oversized_source_reference_when_validated_then_should_reject() {
909 let over = SourceRef::Kv {
910 namespace: "n".repeat(MAX_SOURCE_REF_BYTES),
911 key: "k".repeat(MAX_SOURCE_REF_BYTES),
912 };
913 assert!(over.validate().is_err());
914 assert!(
915 SourceRef::Memory {
916 id: "01KWM3K3XEP3NP5TN850J17YBP".to_owned()
917 }
918 .validate()
919 .is_ok()
920 );
921 }
922
923 #[test]
924 fn given_an_upsert_over_the_element_cap_when_validated_then_should_reject() {
925 let over = GraphUpsert {
926 v: 1,
927 graph: "knowledge".to_owned(),
928 session: None,
929 nodes: (0..=MAX_GRAPH_RESULT_ELEMENTS)
930 .map(|_| node_with(Vec::new(), Vec::new()))
931 .collect(),
932 edges: Vec::new(),
933 };
934 assert!(over.validate().is_err());
935 let ok = GraphUpsert {
936 v: 1,
937 graph: "knowledge".to_owned(),
938 session: None,
939 nodes: vec![node_with(vec!["Person".to_owned()], Vec::new())],
940 edges: Vec::new(),
941 };
942 assert!(ok.validate().is_ok());
943 }
944
945 #[test]
946 fn given_an_upsert_carrying_an_over_cap_node_when_validated_then_should_reject() {
947 let upsert = GraphUpsert {
948 v: 1,
949 graph: "knowledge".to_owned(),
950 session: None,
951 nodes: vec![node_with(
952 (0..=MAX_GRAPH_NODE_LABELS)
953 .map(|index| format!("label-{index}"))
954 .collect(),
955 Vec::new(),
956 )],
957 edges: Vec::new(),
958 };
959 assert!(
960 upsert.validate().is_err(),
961 "an upsert must enforce its elements' caps, not just its own"
962 );
963 }
964
965 #[test]
966 fn given_an_oversized_text_query_when_validated_then_should_reject() {
967 let over = TextQuery {
968 field: None,
969 query: "q".repeat(MAX_TEXT_QUERY_BYTES + 1),
970 };
971 assert!(over.validate().is_err());
972 let ok = TextQuery {
973 field: None,
974 query: "auth is slow".to_owned(),
975 };
976 assert!(ok.validate().is_ok());
977 }
978
979 #[test]
980 fn given_an_over_cap_vector_top_k_when_validated_then_should_reject() {
981 let over = VectorQuery {
982 field: "embedding".to_owned(),
983 embedding: vec![0.0; 4],
984 top_k: MAX_PAGE_SIZE as u32 + 1,
985 };
986 assert!(over.validate().is_err());
987 let ok = VectorQuery {
988 field: "embedding".to_owned(),
989 embedding: vec![0.0; 4],
990 top_k: 10,
991 };
992 assert!(ok.validate().is_ok());
993 }
994
995 #[test]
996 fn given_an_oversized_memory_body_when_validated_then_should_reject() {
997 let over = MemoryRecord::Item {
998 origin: None,
999 producer: None,
1000 id: "01KWM3K3XEP3NP5TN850J17YBP".to_owned(),
1001 kind: "fact".to_owned(),
1002 body: vec![0u8; MAX_MEMORY_BODY_BYTES + 1],
1003 };
1004 assert!(over.validate().is_err());
1005 let ok = MemoryRecord::Item {
1006 origin: None,
1007 producer: None,
1008 id: "01KWM3K3XEP3NP5TN850J17YBP".to_owned(),
1009 kind: "fact".to_owned(),
1010 body: b"auth is slow".to_vec(),
1011 };
1012 assert!(ok.validate().is_ok());
1013 assert!(
1014 MemoryRecord::Forget {
1015 target: "01KWM3K3XEP3NP5TN850J17YBP".to_owned(),
1016 conversation: None,
1017 }
1018 .validate()
1019 .is_ok()
1020 );
1021 assert!(
1022 MemoryRecord::Forget {
1023 target: "01KWM3K3XEP3NP5TN850J17YBP".to_owned(),
1024 conversation: Some("not an id".to_owned()),
1025 }
1026 .validate()
1027 .is_err()
1028 );
1029 }
1030
1031 #[test]
1032 fn given_oversized_client_metadata_when_validated_then_should_reject() {
1033 let over = ClientMetadata {
1034 client_id: 1,
1035 user_id: None,
1036 transport: 1,
1037 address: "127.0.0.1:8090".to_owned(),
1038 consumer_groups_count: 0,
1039 metadata: Some(vec![0u8; MAX_CLIENT_METADATA + 1]),
1040 };
1041 assert!(over.validate().is_err());
1042 let ok = ClientMetadata {
1043 client_id: 1,
1044 user_id: None,
1045 transport: 1,
1046 address: "127.0.0.1:8090".to_owned(),
1047 consumer_groups_count: 0,
1048 metadata: Some(b"card".to_vec()),
1049 };
1050 assert!(ok.validate().is_ok());
1051 }
1052}