Skip to main content

gatehouse_runtime_protocol/lifecycle_v2/
readonly.rs

1//! Read-only info/inspect/info composition over an injected GET exchange.
2//! The exchange must supply bounded, deadline-controlled HTTP transport. This
3//! module does not prove a connection's identity, authentication or lifetime.
4use super::{
5    native_image::{decode_image, image_route, ImageFacts},
6    native_info::decode_info,
7    native_status::{decode_inspect, inspect_route, InspectOutcome, State},
8    observation::INFO_ROUTE,
9    ownership::ExpectedContainer,
10    protected_context::ProtectedContext,
11};
12
13/// Evidence of the protected observation sequence, not transport authentication,
14/// freshness, restart continuity, installation verification or dispatch authority.
15/// There is deliberately no public constructor or deserializer. The injected
16/// exchange remains a trust boundary and can supply fabricated HTTP responses.
17pub struct CapturedImage {
18    facts: ImageFacts,
19    image_bytes: Vec<u8>,
20    config_bytes: Vec<u8>,
21    epoch: String,
22}
23impl std::fmt::Debug for CapturedImage {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        f.debug_struct("CapturedImage").finish_non_exhaustive()
26    }
27}
28impl CapturedImage {
29    pub fn facts(&self) -> &ImageFacts {
30        &self.facts
31    }
32    pub fn image_bytes(&self) -> &[u8] {
33        &self.image_bytes
34    }
35    pub fn config_bytes(&self) -> &[u8] {
36        &self.config_bytes
37    }
38    pub fn operator_epoch(&self) -> &str {
39        &self.epoch
40    }
41    /// Same exact context only; socket recreation/restart is not adopted.
42    pub fn match_context(&self, context: &ProtectedContext) -> Result<(), &'static str> {
43        let (expected, bytes) = context.snapshot()?;
44        if bytes != self.config_bytes || expected.operator_epoch != self.epoch {
45            return Err("captured image context mismatch");
46        }
47        Ok(())
48    }
49}
50
51impl ProtectedContext {
52    /// Capture exact bounded evidence only after protected info/image/info.
53    /// An eventual record constructor must accept this type, never ImageFacts.
54    pub fn capture_image_for_plan<F>(&self, id: &str, get: F) -> Result<CapturedImage, &'static str>
55    where
56        F: FnMut(&str) -> Result<(u16, Vec<u8>), &'static str>,
57    {
58        let (expected, bytes) = self.snapshot()?;
59        let config_bytes = bytes.to_vec();
60        let epoch = expected.operator_epoch.clone();
61        let (facts, image_bytes) =
62            self.inspect_checked_with(&image_route(id)?, get, |route, status, bytes| {
63                Ok((
64                    decode_image(id, "GET", route, status, bytes)?,
65                    bytes.to_vec(),
66                ))
67            })?;
68        let captured = CapturedImage {
69            facts,
70            image_bytes,
71            config_bytes,
72            epoch,
73        };
74        captured.match_context(self)?;
75        Ok(captured)
76    }
77
78    /// No retry, mutation or lease authority. Even an exact Missing result is
79    /// withheld until the final info observation and filesystem check succeed.
80    pub fn inspect_with<F>(&self, id: &str, get: F) -> Result<InspectOutcome, &'static str>
81    where
82        F: FnMut(&str) -> Result<(u16, Vec<u8>), &'static str>,
83    {
84        self.inspect_checked_with(&inspect_route(id)?, get, |route, status, bytes| {
85            decode_inspect(id, "GET", route, status, bytes)
86        })
87    }
88    /// Selected ownership facts under the same protected observation sequence.
89    /// The expected plan still requires durable installation/slot binding.
90    pub fn inspect_owned_with<F>(
91        &self,
92        owner: &ExpectedContainer,
93        get: F,
94    ) -> Result<State, &'static str>
95    where
96        F: FnMut(&str) -> Result<(u16, Vec<u8>), &'static str>,
97    {
98        owner.validate()?;
99        self.inspect_checked_with(&inspect_route(&owner.id)?, get, |route, status, bytes| {
100            owner.match_inspect("GET", route, status, bytes)
101        })
102    }
103    /// Image facts are returned only after matching both protected observations.
104    pub fn inspect_image_with<F>(&self, id: &str, get: F) -> Result<ImageFacts, &'static str>
105    where
106        F: FnMut(&str) -> Result<(u16, Vec<u8>), &'static str>,
107    {
108        self.inspect_checked_with(&image_route(id)?, get, |route, status, bytes| {
109            decode_image(id, "GET", route, status, bytes)
110        })
111    }
112    fn inspect_checked_with<F, T, D>(
113        &self,
114        route: &str,
115        mut get: F,
116        decode: D,
117    ) -> Result<T, &'static str>
118    where
119        F: FnMut(&str) -> Result<(u16, Vec<u8>), &'static str>,
120        D: FnOnce(&str, u16, &[u8]) -> Result<T, &'static str>,
121    {
122        let (expected, _) = self.snapshot()?;
123        expected.validate_local_socket_policy()?;
124        let exchange = |get: &mut F, path: &str| {
125            self.revalidate()?;
126            let response = get(path)?;
127            self.revalidate()?;
128            Ok::<_, &'static str>(response)
129        };
130        let (status, bytes) = exchange(&mut get, INFO_ROUTE)?;
131        if status != 200 {
132            return Err("initial native info unavailable");
133        }
134        let before = decode_info(&bytes, expected.observation.socket.clone())?;
135        expected.match_current_operation(expected, &before, &before)?;
136        let (status, bytes) = exchange(&mut get, route)?;
137        let result = decode(route, status, &bytes)?;
138        let (status, bytes) = exchange(&mut get, INFO_ROUTE)?;
139        if status != 200 {
140            return Err("final native info unavailable");
141        }
142        let after = decode_info(&bytes, expected.observation.socket.clone())?;
143        expected.match_current_operation(expected, &before, &after)?;
144        self.revalidate()?;
145        Ok(result)
146    }
147}