Skip to main content

gatehouse_runtime_protocol/
v2.rs

1//! Explicit Docker Unix-HTTP identity contract. No I/O or daemon attestation.
2//! Independent of v1: no automatic request, lease or persisted-intent conversion.
3use 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    /// Configured expectation, not independently authenticated rootless proof.
77    pub rootless: bool,
78    pub api_version: String,
79    pub server_version: String,
80    pub daemon_id: String,
81    /// Ownership scope only; never an encoding for missing engine pins.
82    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    // Explicit null is required for non-inspection facts, not a missing field.
114    #[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}