1use crate::{digest, nonempty, validate_labels, Invalid, Label, Operation, Outcome, Result};
4use serde::{Deserialize, Deserializer, Serialize};
5use std::collections::BTreeSet;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(try_from = "String")]
9pub enum Protocol {
10 #[serde(rename = "gate-runtime/v2")]
11 V2,
12}
13
14impl TryFrom<String> for Protocol {
15 type Error = Invalid;
16 fn try_from(value: String) -> Result<Self> {
17 if value == "gate-runtime/v2" {
18 Ok(Self::V2)
19 } else {
20 Err(Invalid("unsupported v2 identity"))
21 }
22 }
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(try_from = "String")]
27pub enum Engine {
28 #[serde(rename = "docker")]
29 Docker,
30}
31
32impl TryFrom<String> for Engine {
33 type Error = Invalid;
34 fn try_from(value: String) -> Result<Self> {
35 if value == "docker" {
36 Ok(Self::Docker)
37 } else {
38 Err(Invalid("unsupported v2 identity"))
39 }
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(try_from = "String")]
45pub enum Transport {
46 #[serde(rename = "unix-http")]
47 UnixHttp,
48}
49
50impl TryFrom<String> for Transport {
51 type Error = Invalid;
52 fn try_from(value: String) -> Result<Self> {
53 if value == "unix-http" {
54 Ok(Self::UnixHttp)
55 } else {
56 Err(Invalid("unsupported v2 identity"))
57 }
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(deny_unknown_fields)]
63pub struct Binding {
64 pub protocol: Protocol,
65 pub adapter_sha256: String,
66 pub adapter_version: String,
67 pub invocation: String,
68 pub operation_key: String,
69 pub generation: String,
70 pub adapter_config_sha256: String,
71 pub engine: Engine,
72 pub transport: Transport,
73 pub socket_path: String,
74 pub socket_uid: u32,
75 pub principal_uid: u32,
76 pub rootless: bool,
78 pub api_version: String,
79 pub server_version: String,
80 pub daemon_id: String,
81 pub scope: String,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(deny_unknown_fields)]
87pub struct Request {
88 pub binding: Binding,
89 pub operation: Operation,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(deny_unknown_fields)]
94pub struct ObservedIdentity {
95 pub api_version: String,
96 pub server_version: String,
97 pub daemon_id: String,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(deny_unknown_fields)]
102pub struct Observations {
103 pub observed_before: ObservedIdentity,
104 pub observed_after: ObservedIdentity,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108#[serde(deny_unknown_fields)]
109pub struct Response {
110 pub binding: Binding,
111 pub operation: Operation,
112 pub outcome: Outcome,
113 #[serde(deserialize_with = "required_observations")]
115 pub observations: Option<Observations>,
116}
117
118fn required_observations<'de, D: Deserializer<'de>>(
119 deserializer: D,
120) -> std::result::Result<Option<Observations>, D::Error> {
121 Option::deserialize(deserializer)
122}
123
124impl Binding {
125 pub fn validate(&self) -> Result<()> {
126 digest(&self.adapter_sha256)?;
127 digest(&self.adapter_config_sha256)?;
128 for value in [
129 &self.adapter_version,
130 &self.invocation,
131 &self.operation_key,
132 &self.generation,
133 &self.server_version,
134 &self.daemon_id,
135 &self.scope,
136 ] {
137 nonempty(value)?;
138 }
139 validate_socket_path(&self.socket_path)?;
140 validate_api_version(&self.api_version)
141 }
142}
143
144fn validate_socket_path(path: &str) -> Result<()> {
145 nonempty(path)?;
146 if !path.starts_with('/')
147 || path[1..]
148 .split('/')
149 .any(|part| matches!(part, "" | "." | ".."))
150 {
151 return Err(Invalid("expected canonical absolute Unix socket path"));
152 }
153 Ok(())
154}
155
156fn validate_api_version(version: &str) -> Result<()> {
157 let parts: Vec<_> = version.split('.').collect();
158 if parts.len() != 2
159 || parts.iter().any(|part| {
160 part.is_empty()
161 || (part.len() > 1 && part.starts_with('0'))
162 || !part.bytes().all(|b| b.is_ascii_digit())
163 || part.parse::<u16>().is_err()
164 })
165 {
166 return Err(Invalid("expected numeric major.minor API version"));
167 }
168 Ok(())
169}
170
171impl Request {
172 pub fn validate(&self) -> Result<()> {
173 self.binding.validate()?;
174 match &self.operation {
175 Operation::Capabilities => Ok(()),
176 Operation::Inspect { object_id } | Operation::Remove { object_id } => digest(object_id),
177 }
178 }
179
180 pub fn require_same_intent(&self, previous: &Self) -> Result<()> {
181 self.validate()?;
182 previous.validate()?;
183 if self != previous {
184 return Err(Invalid("v2 request differs from recorded intent"));
185 }
186 Ok(())
187 }
188}
189
190impl ObservedIdentity {
191 fn matches(&self, binding: &Binding) -> bool {
192 self.api_version == binding.api_version
193 && self.server_version == binding.server_version
194 && self.daemon_id == binding.daemon_id
195 }
196}
197
198impl Response {
199 pub fn validate(&self, expected: &Request) -> Result<()> {
200 expected.validate()?;
201 if self.binding != expected.binding || self.operation != expected.operation {
202 return Err(Invalid("v2 response binding mismatch"));
203 }
204 self.validate_observations()?;
205 match (&self.operation, &self.outcome) {
206 (Operation::Capabilities, Outcome::Capabilities { supported }) => {
207 let unique: BTreeSet<_> = supported.iter().collect();
208 if supported.is_empty() || unique.len() != supported.len() {
209 return Err(Invalid("empty or duplicate capabilities"));
210 }
211 }
212 (Operation::Inspect { object_id }, Outcome::Present { object }) => {
213 if object.object_id != *object_id {
214 return Err(Invalid("object identity mismatch"));
215 }
216 validate_labels(&object.labels)?;
217 }
218 (Operation::Inspect { .. }, Outcome::Absent | Outcome::Unavailable) => {}
219 (
220 Operation::Remove { .. },
221 Outcome::Completed | Outcome::RejectedBeforeEffect | Outcome::Unknown,
222 ) => {}
223 _ => return Err(Invalid("outcome incompatible with operation")),
224 }
225 Ok(())
226 }
227
228 fn validate_observations(&self) -> Result<()> {
229 match (&self.outcome, &self.observations) {
230 (Outcome::Present { .. } | Outcome::Absent, Some(pair))
231 if pair.observed_before.matches(&self.binding)
232 && pair.observed_after.matches(&self.binding) =>
233 {
234 Ok(())
235 }
236 (Outcome::Present { .. } | Outcome::Absent, _) => Err(Invalid(
237 "matching before and after engine observations required",
238 )),
239 (_, None) => Ok(()),
240 (_, Some(_)) => Err(Invalid("observations incompatible with outcome")),
241 }
242 }
243
244 pub fn require_ownership(&self, expected: &Request, labels: &[Label]) -> Result<()> {
245 self.validate(expected)?;
246 validate_labels(labels)?;
247 let Outcome::Present { object } = &self.outcome else {
248 return Err(Invalid("ownership requires a present object"));
249 };
250 if !labels.iter().all(|label| object.labels.contains(label)) {
251 return Err(Invalid("ownership labels mismatch"));
252 }
253 Ok(())
254 }
255}
256
257pub fn parse_request(bytes: &[u8]) -> Result<Request> {
258 let request: Request = crate::parse(bytes)?;
259 request.validate()?;
260 Ok(request)
261}
262
263pub fn parse_response(bytes: &[u8], expected: &Request) -> Result<Response> {
264 let response: Response = crate::parse(bytes)?;
265 response.validate(expected)?;
266 Ok(response)
267}
268
269pub fn require_absence(remove: &Request, inspection: &Response) -> Result<()> {
270 remove.validate()?;
271 let Operation::Remove { object_id } = &remove.operation else {
272 return Err(Invalid("reconciliation requires a removal intent"));
273 };
274 let expected = Request {
275 binding: remove.binding.clone(),
276 operation: Operation::Inspect {
277 object_id: object_id.clone(),
278 },
279 };
280 inspection.validate(&expected)?;
281 if inspection.outcome != Outcome::Absent {
282 return Err(Invalid("exact object absence not established"));
283 }
284 Ok(())
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use crate::{parse_request as parse_v1_request, Capability, Object, Operation, Outcome};
291 use serde_json::{json, Value};
292
293 fn binding() -> Binding {
294 Binding {
295 protocol: Protocol::V2,
296 adapter_sha256: "a".repeat(64),
297 adapter_version: "1.0.0".into(),
298 invocation: "inv-1".into(),
299 operation_key: "op-1".into(),
300 generation: "gen-1".into(),
301 adapter_config_sha256: "b".repeat(64),
302 engine: Engine::Docker,
303 transport: Transport::UnixHttp,
304 socket_path: "/run/docker.sock".into(),
305 socket_uid: 0,
306 principal_uid: 1000,
307 rootless: false,
308 api_version: "1.47".into(),
309 server_version: "27.0.1".into(),
310 daemon_id: "daemon-1".into(),
311 scope: "scope-1".into(),
312 }
313 }
314
315 fn id() -> String {
316 "c".repeat(64)
317 }
318
319 fn inspect_request() -> Request {
320 Request {
321 binding: binding(),
322 operation: Operation::Inspect { object_id: id() },
323 }
324 }
325
326 fn remove_request() -> Request {
327 Request {
328 binding: binding(),
329 operation: Operation::Remove { object_id: id() },
330 }
331 }
332
333 fn observed() -> ObservedIdentity {
334 ObservedIdentity {
335 api_version: "1.47".into(),
336 server_version: "27.0.1".into(),
337 daemon_id: "daemon-1".into(),
338 }
339 }
340
341 fn observations() -> Observations {
342 Observations {
343 observed_before: observed(),
344 observed_after: observed(),
345 }
346 }
347
348 fn present() -> Outcome {
349 Outcome::Present {
350 object: Object {
351 object_id: id(),
352 labels: vec![Label {
353 key: "harness-gate.owner".into(),
354 value: "harness-gate".into(),
355 }],
356 },
357 }
358 }
359
360 fn response(
361 operation: Operation,
362 outcome: Outcome,
363 observations: Option<Observations>,
364 ) -> Response {
365 Response {
366 binding: binding(),
367 operation,
368 outcome,
369 observations,
370 }
371 }
372
373 fn bytes(value: &Value) -> Vec<u8> {
374 serde_json::to_vec(value).unwrap()
375 }
376
377 fn request_value(request: &Request) -> Value {
378 serde_json::to_value(request).unwrap()
379 }
380
381 fn parse_ok(value: &Value, expected: &Request) -> Response {
382 parse_response(&bytes(value), expected).expect("valid v2 response")
383 }
384
385 #[test]
386 fn accepts_valid_v2_shapes() {
387 let inspect = inspect_request();
388 for outcome in [Outcome::Absent, Outcome::Unavailable, present()] {
389 let observations = if matches!(outcome, Outcome::Unavailable) {
390 None
391 } else {
392 Some(observations())
393 };
394 parse_ok(
395 &serde_json::to_value(response(inspect.operation.clone(), outcome, observations))
396 .unwrap(),
397 &inspect,
398 );
399 }
400 let remove = remove_request();
401 for outcome in [
402 Outcome::Completed,
403 Outcome::RejectedBeforeEffect,
404 Outcome::Unknown,
405 ] {
406 parse_ok(
407 &serde_json::to_value(response(remove.operation.clone(), outcome, None)).unwrap(),
408 &remove,
409 );
410 }
411 let capabilities = Request {
412 binding: binding(),
413 operation: Operation::Capabilities,
414 };
415 parse_ok(
416 &serde_json::to_value(response(
417 Operation::Capabilities,
418 Outcome::Capabilities {
419 supported: vec![Capability::Inspect, Capability::Remove],
420 },
421 None,
422 ))
423 .unwrap(),
424 &capabilities,
425 );
426 }
427
428 #[test]
429 fn observations_are_required_and_strict() {
430 let request = inspect_request();
431 let base = serde_json::to_value(response(
432 request.operation.clone(),
433 Outcome::Absent,
434 Some(observations()),
435 ))
436 .unwrap();
437 let mut missing = base.clone();
438 missing.as_object_mut().unwrap().remove("observations");
439 assert!(parse_response(&bytes(&missing), &request).is_err());
440
441 let mut null = base.clone();
442 null["observations"] = json!(null);
443 assert!(parse_response(&bytes(&null), &request).is_err());
444
445 let mut missing_after = base.clone();
446 missing_after["observations"]
447 .as_object_mut()
448 .unwrap()
449 .remove("observed_after");
450 assert!(parse_response(&bytes(&missing_after), &request).is_err());
451
452 let mut duplicate = base.clone();
453 duplicate["observations"]["extra"] = json!(1);
454 assert!(parse_response(&bytes(&duplicate), &request).is_err());
455 let raw = serde_json::to_string(&base).unwrap();
456 let duplicate_key = raw.replacen(
457 r#""observations":{"#,
458 r#""observations":null,"observations":{"#,
459 1,
460 );
461 assert!(parse_response(duplicate_key.as_bytes(), &request).is_err());
462
463 let mut missing_before = base.clone();
464 missing_before["observations"]
465 .as_object_mut()
466 .unwrap()
467 .remove("observed_before");
468 assert!(parse_response(&bytes(&missing_before), &request).is_err());
469
470 let mut unknown_observed = base.clone();
471 unknown_observed["observations"]["observed_before"]["extra"] = json!(1);
472 assert!(parse_response(&bytes(&unknown_observed), &request).is_err());
473
474 let mut before = base;
475 before["observations"]["observed_before"]["daemon_id"] = json!("other");
476 assert!(parse_response(&bytes(&before), &request).is_err());
477
478 let remove = remove_request();
479 let completed =
480 serde_json::to_value(response(remove.operation.clone(), Outcome::Completed, None))
481 .unwrap();
482 parse_ok(&completed, &remove);
483 let mut null_ok = completed.clone();
484 null_ok["observations"] = json!(null);
485 parse_ok(&null_ok, &remove);
486 let mut missing_ok = completed;
487 missing_ok.as_object_mut().unwrap().remove("observations");
488 assert!(parse_response(&bytes(&missing_ok), &remove).is_err());
489 let mut some_completed = serde_json::to_value(response(
490 remove.operation.clone(),
491 Outcome::Completed,
492 Some(observations()),
493 ))
494 .unwrap();
495 assert!(parse_response(&bytes(&some_completed), &remove).is_err());
496 some_completed["observations"] = json!([]);
497 assert!(parse_response(&bytes(&some_completed), &remove).is_err());
498 }
499
500 #[test]
501 fn binding_pins_and_docker_only_are_strict() {
502 for (name, value) in [
503 ("protocol", json!({"gate-runtime/v2": null})),
504 ("engine", json!({"docker": null})),
505 ("transport", json!({"unix-http": null})),
506 ] {
507 let mut request = request_value(&inspect_request());
508 request["binding"][name] = value;
509 assert!(parse_request(&bytes(&request)).is_err(), "{name}");
510 }
511 let mut missing = request_value(&inspect_request());
512 missing["binding"]
513 .as_object_mut()
514 .unwrap()
515 .remove("daemon_id");
516 assert!(parse_request(&bytes(&missing)).is_err());
517 let mut unknown = request_value(&inspect_request());
518 unknown["binding"]["extra"] = json!("x");
519 assert!(parse_request(&bytes(&unknown)).is_err());
520 let raw = serde_json::to_string(&request_value(&inspect_request())).unwrap();
521 let duplicate_binding = raw.replacen(r#""binding":{"#, r#""binding":null,"binding":{"#, 1);
522 assert!(parse_request(duplicate_binding.as_bytes()).is_err());
523
524 for (name, value) in [
525 ("protocol", json!("gate-runtime/v1")),
526 ("engine", json!("podman")),
527 ("transport", json!("cli")),
528 ("adapter_sha256", json!("A".repeat(64))),
529 ("adapter_config_sha256", json!("short")),
530 ("socket_path", json!("relative/docker.sock")),
531 ("socket_path", json!("/run/../docker.sock")),
532 ("socket_path", json!("/run//docker.sock")),
533 ("socket_path", json!("/run/docker.sock/")),
534 ("api_version", json!("1")),
535 ("api_version", json!("01.47")),
536 ("api_version", json!("1.2.3")),
537 ("api_version", json!("x.47")),
538 ("socket_uid", json!("0")),
539 ("principal_uid", json!(-1)),
540 ("rootless", json!("false")),
541 ("daemon_id", json!("")),
542 ("server_version", json!("line\nfeed")),
543 ("scope", json!("")),
544 ] {
545 let mut request = request_value(&inspect_request());
546 request["binding"][name] = value;
547 assert!(parse_request(&bytes(&request)).is_err(), "{name}");
548 }
549 }
550
551 #[test]
552 fn every_binding_change_is_rejected() {
553 let expected = inspect_request();
554 let base = serde_json::to_value(response(
555 expected.operation.clone(),
556 Outcome::Absent,
557 Some(observations()),
558 ))
559 .unwrap();
560 let mutations = [
561 ("protocol", json!("gate-runtime/v1")),
562 ("adapter_sha256", json!("d".repeat(64))),
563 ("adapter_version", json!("2")),
564 ("invocation", json!("inv-2")),
565 ("operation_key", json!("op-2")),
566 ("generation", json!("gen-2")),
567 ("adapter_config_sha256", json!("e".repeat(64))),
568 ("engine", json!("podman")),
569 ("transport", json!("cli")),
570 ("socket_path", json!("/run/other.sock")),
571 ("socket_uid", json!(1)),
572 ("principal_uid", json!(1001)),
573 ("rootless", json!(true)),
574 ("api_version", json!("1.48")),
575 ("server_version", json!("28")),
576 ("daemon_id", json!("daemon-2")),
577 ("scope", json!("scope-2")),
578 ];
579 for (field, value) in mutations {
580 let mut changed = base.clone();
581 changed["binding"][field] = value;
582 assert!(
583 parse_response(&bytes(&changed), &expected).is_err(),
584 "{field}"
585 );
586 }
587 }
588
589 #[test]
590 fn observations_must_match_before_and_after() {
591 let expected = inspect_request();
592 for mutation in [
593 |v: &mut Value| v["observations"]["observed_before"]["api_version"] = json!("1.48"),
594 |v: &mut Value| v["observations"]["observed_before"]["server_version"] = json!("28"),
595 |v: &mut Value| v["observations"]["observed_before"]["daemon_id"] = json!("other"),
596 |v: &mut Value| v["observations"]["observed_after"]["api_version"] = json!("1.48"),
597 |v: &mut Value| v["observations"]["observed_after"]["server_version"] = json!("28"),
598 |v: &mut Value| v["observations"]["observed_after"]["daemon_id"] = json!("other"),
599 ] {
600 let mut value = serde_json::to_value(response(
601 expected.operation.clone(),
602 Outcome::Absent,
603 Some(observations()),
604 ))
605 .unwrap();
606 mutation(&mut value);
607 assert!(parse_response(&bytes(&value), &expected).is_err());
608 }
609 }
610
611 #[test]
612 fn outcome_matrix_and_observations_are_consistent() {
613 let inspect = inspect_request();
614 for outcome in [
615 Outcome::Completed,
616 Outcome::RejectedBeforeEffect,
617 Outcome::Unknown,
618 ] {
619 let value =
620 serde_json::to_value(response(inspect.operation.clone(), outcome, None)).unwrap();
621 assert!(parse_response(&bytes(&value), &inspect).is_err());
622 }
623 let remove = remove_request();
624 for outcome in [present(), Outcome::Absent, Outcome::Unavailable] {
625 let value = serde_json::to_value(response(
626 remove.operation.clone(),
627 outcome,
628 Some(observations()),
629 ))
630 .unwrap();
631 assert!(parse_response(&bytes(&value), &remove).is_err());
632 }
633 let capabilities = Request {
634 binding: binding(),
635 operation: Operation::Capabilities,
636 };
637 for outcome in [
638 Outcome::Absent,
639 Outcome::Unavailable,
640 present(),
641 Outcome::Completed,
642 ] {
643 let value =
644 serde_json::to_value(response(Operation::Capabilities, outcome, None)).unwrap();
645 assert!(parse_response(&bytes(&value), &capabilities).is_err());
646 }
647 let mut wrong_object = serde_json::to_value(response(
648 inspect.operation.clone(),
649 present(),
650 Some(observations()),
651 ))
652 .unwrap();
653 wrong_object["outcome"]["present"]["object"]["object_id"] = json!(1);
654 assert!(parse_response(&bytes(&wrong_object), &inspect).is_err());
655
656 let empty = serde_json::to_value(response(
657 Operation::Capabilities,
658 Outcome::Capabilities { supported: vec![] },
659 None,
660 ))
661 .unwrap();
662 assert!(parse_response(&bytes(&empty), &capabilities).is_err());
663 let duplicate = serde_json::to_value(response(
664 Operation::Capabilities,
665 Outcome::Capabilities {
666 supported: vec![Capability::Inspect, Capability::Inspect],
667 },
668 None,
669 ))
670 .unwrap();
671 assert!(parse_response(&bytes(&duplicate), &capabilities).is_err());
672 }
673
674 #[test]
675 fn ownership_and_absence_helpers() {
676 let expected = inspect_request();
677 let presented = parse_ok(
678 &serde_json::to_value(response(
679 expected.operation.clone(),
680 present(),
681 Some(observations()),
682 ))
683 .unwrap(),
684 &expected,
685 );
686 let required = vec![Label {
687 key: "harness-gate.owner".into(),
688 value: "harness-gate".into(),
689 }];
690 assert!(presented.require_ownership(&expected, &required).is_ok());
691 let extra = vec![
692 Label {
693 key: "harness-gate.owner".into(),
694 value: "harness-gate".into(),
695 },
696 Label {
697 key: "missing".into(),
698 value: "v".into(),
699 },
700 ];
701 assert!(presented.require_ownership(&expected, &extra).is_err());
702 assert!(presented.require_ownership(&expected, &[]).is_err());
703 let duplicate = vec![required[0].clone(), required[0].clone()];
704 assert!(presented.require_ownership(&expected, &duplicate).is_err());
705 let absent = parse_ok(
706 &serde_json::to_value(response(
707 expected.operation.clone(),
708 Outcome::Absent,
709 Some(observations()),
710 ))
711 .unwrap(),
712 &expected,
713 );
714 assert!(absent.require_ownership(&expected, &required).is_err());
715
716 let remove = remove_request();
717 assert!(require_absence(&remove, &absent).is_ok());
718 assert!(require_absence(&expected, &absent).is_err());
719 let present_response = parse_ok(
720 &serde_json::to_value(response(
721 expected.operation.clone(),
722 present(),
723 Some(observations()),
724 ))
725 .unwrap(),
726 &expected,
727 );
728 assert!(require_absence(&remove, &present_response).is_err());
729 let mut wrong = absent.clone();
730 wrong.binding.generation = "gen-2".into();
731 assert!(require_absence(&remove, &wrong).is_err());
732 let mut wrong_observations = absent;
733 wrong_observations
734 .observations
735 .as_mut()
736 .unwrap()
737 .observed_after
738 .daemon_id = "other".into();
739 assert!(require_absence(&remove, &wrong_observations).is_err());
740 }
741
742 #[test]
743 fn v1_and_v2_reject_each_other() {
744 let v2 = bytes(&request_value(&inspect_request()));
745 assert!(parse_v1_request(&v2).is_err());
746 let v1 = br#"{"binding":{"protocol":"gate-runtime/v1","adapter_sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","adapter_version":"1","invocation":"i","operation_key":"k","generation":"g","namespace":{"engine":"docker","endpoint":"unix:///run/docker.sock","context":"ctx","principal":"uid:1000","rootless":false,"scope":"s"}},"operation":"capabilities"}"#;
747 assert!(parse_request(v1).is_err());
748 }
749
750 #[test]
751 fn size_and_errors_do_not_echo_input() {
752 let request = bytes(&request_value(&inspect_request()));
753 let mut oversized = request.clone();
754 oversized.extend(std::iter::repeat_n(b' ', crate::MAX_MESSAGE_BYTES));
755 assert!(parse_request(&oversized).is_err());
756 let sentinel = "SENTINEL-V2";
757 let invalid =
758 format!(r#"{{"binding":{{"protocol":"{sentinel}"}},"operation":"capabilities"}}"#);
759 let error = parse_request(invalid.as_bytes()).unwrap_err();
760 assert!(!error.to_string().contains(sentinel));
761 let expected = inspect_request();
762 let mut value = serde_json::to_value(response(
763 expected.operation.clone(),
764 Outcome::Absent,
765 Some(observations()),
766 ))
767 .unwrap();
768 value["observations"]["observed_before"]["daemon_id"] = json!(sentinel);
769 let error = parse_response(&bytes(&value), &expected).unwrap_err();
770 assert!(!error.to_string().contains(sentinel));
771 }
772}