Skip to main content

gatehouse_runtime_protocol/
lib.rs

1//! Strict runtime data and intent state support. No process execution, artifact
2//! verification, ambient context discovery, or authority to delete a resource.
3pub mod intent;
4pub mod lifecycle;
5pub mod lifecycle_v2;
6#[cfg(unix)]
7pub mod store;
8pub mod v2;
9pub mod v3;
10mod wire;
11
12use serde::{Deserialize, Serialize};
13use std::collections::BTreeSet;
14
15pub const MAX_MESSAGE_BYTES: usize = 65536;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(try_from = "String")]
19pub enum Protocol {
20    #[serde(rename = "gate-runtime/v1")]
21    V1,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "kebab-case")]
26#[serde(try_from = "String")]
27pub enum Engine {
28    Docker,
29    Podman,
30}
31
32/// Explicit trusted configuration, never filled from the current environment.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(deny_unknown_fields)]
35pub struct Namespace {
36    pub engine: Engine,
37    pub endpoint: String,
38    pub context: String,
39    pub principal: String,
40    pub rootless: bool,
41    pub scope: String,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(deny_unknown_fields)]
46pub struct Binding {
47    pub protocol: Protocol,
48    pub adapter_sha256: String,
49    pub adapter_version: String,
50    pub invocation: String,
51    pub operation_key: String,
52    pub generation: String,
53    pub namespace: Namespace,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
57#[serde(rename_all = "kebab-case")]
58pub enum Operation {
59    Capabilities,
60    Inspect { object_id: String },
61    Remove { object_id: String },
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(deny_unknown_fields)]
66pub struct Request {
67    pub binding: Binding,
68    pub operation: Operation,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(deny_unknown_fields)]
73pub struct Label {
74    pub key: String,
75    pub value: String,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79#[serde(deny_unknown_fields)]
80pub struct Object {
81    pub object_id: String,
82    pub labels: Vec<Label>,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
86#[serde(rename_all = "kebab-case")]
87#[serde(try_from = "String")]
88pub enum Capability {
89    Inspect,
90    Remove,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
94#[serde(rename_all = "kebab-case")]
95pub enum Outcome {
96    Capabilities { supported: Vec<Capability> },
97    Present { object: Object },
98    Absent,
99    Unavailable,
100    Completed,
101    RejectedBeforeEffect,
102    Unknown,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106#[serde(deny_unknown_fields)]
107pub struct Response {
108    pub binding: Binding,
109    pub operation: Operation,
110    pub outcome: Outcome,
111}
112
113/// Deliberately excludes untrusted input values from diagnostics.
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct Invalid(pub &'static str);
116impl std::fmt::Display for Invalid {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        f.write_str(self.0)
119    }
120}
121impl std::error::Error for Invalid {}
122type Result<T> = std::result::Result<T, Invalid>;
123
124fn nonempty(value: &str) -> Result<()> {
125    if value.trim().is_empty() || value.chars().any(char::is_control) {
126        return Err(Invalid("missing or invalid identity"));
127    }
128    Ok(())
129}
130
131fn digest(value: &str) -> Result<()> {
132    if value.len() != 64
133        || !value
134            .bytes()
135            .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
136    {
137        return Err(Invalid("expected full lowercase hexadecimal identity"));
138    }
139    Ok(())
140}
141
142impl Binding {
143    pub fn validate(&self) -> Result<()> {
144        digest(&self.adapter_sha256)?;
145        for value in [
146            &self.adapter_version,
147            &self.invocation,
148            &self.operation_key,
149            &self.generation,
150            &self.namespace.endpoint,
151            &self.namespace.context,
152            &self.namespace.principal,
153            &self.namespace.scope,
154        ] {
155            nonempty(value)?;
156        }
157        Ok(())
158    }
159}
160
161impl Request {
162    pub fn validate(&self) -> Result<()> {
163        self.binding.validate()?;
164        match &self.operation {
165            Operation::Capabilities => Ok(()),
166            Operation::Inspect { object_id } | Operation::Remove { object_id } => digest(object_id),
167        }
168    }
169
170    /// Reject operation-key reuse with changed content, including generation.
171    pub fn require_same_intent(&self, previous: &Self) -> Result<()> {
172        self.validate()?;
173        previous.validate()?;
174        if self != previous {
175            return Err(Invalid("request differs from recorded intent"));
176        }
177        Ok(())
178    }
179}
180
181fn parse<T: for<'de> Deserialize<'de>>(bytes: &[u8]) -> Result<T> {
182    if bytes.len() > MAX_MESSAGE_BYTES {
183        return Err(Invalid("message exceeds protocol limit"));
184    }
185    serde_json::from_slice(bytes).map_err(|_| Invalid("invalid protocol message"))
186}
187
188pub fn parse_request(bytes: &[u8]) -> Result<Request> {
189    let request: Request = parse(bytes)?;
190    request.validate()?;
191    Ok(request)
192}
193
194pub fn parse_response(bytes: &[u8], expected: &Request) -> Result<Response> {
195    let response: Response = parse(bytes)?;
196    response.validate(expected)?;
197    Ok(response)
198}
199
200fn validate_labels(labels: &[Label]) -> Result<()> {
201    let mut keys = BTreeSet::new();
202    if labels.is_empty() {
203        return Err(Invalid("missing ownership labels"));
204    }
205    for label in labels {
206        nonempty(&label.key)?;
207        nonempty(&label.value)?;
208        if !keys.insert(&label.key) {
209            return Err(Invalid("duplicate ownership label"));
210        }
211    }
212    Ok(())
213}
214
215impl Response {
216    pub fn validate(&self, expected: &Request) -> Result<()> {
217        expected.validate()?;
218        if self.binding != expected.binding || self.operation != expected.operation {
219            return Err(Invalid("response binding mismatch"));
220        }
221        match (&self.operation, &self.outcome) {
222            (Operation::Capabilities, Outcome::Capabilities { supported }) => {
223                let unique: BTreeSet<_> = supported.iter().collect();
224                if supported.is_empty() || unique.len() != supported.len() {
225                    return Err(Invalid("empty or duplicate capabilities"));
226                }
227            }
228            (Operation::Inspect { object_id }, Outcome::Present { object }) => {
229                if object.object_id != *object_id {
230                    return Err(Invalid("object identity mismatch"));
231                }
232                validate_labels(&object.labels)?;
233            }
234            (Operation::Inspect { .. }, Outcome::Absent | Outcome::Unavailable) => {}
235            (
236                Operation::Remove { .. },
237                Outcome::Completed | Outcome::RejectedBeforeEffect | Outcome::Unknown,
238            ) => {}
239            _ => return Err(Invalid("outcome incompatible with operation")),
240        }
241        Ok(())
242    }
243
244    /// Core supplies the required ownership labels from its trusted lease.
245    pub fn require_ownership(&self, expected: &Request, labels: &[Label]) -> Result<()> {
246        self.validate(expected)?;
247        validate_labels(labels)?;
248        let Outcome::Present { object } = &self.outcome else {
249            return Err(Invalid("ownership requires a present object"));
250        };
251        if !labels.iter().all(|label| object.labels.contains(label)) {
252            return Err(Invalid("ownership labels mismatch"));
253        }
254        Ok(())
255    }
256}
257
258/// An adapter's mutation completion claim alone cannot clear an intent. This
259/// checks a separately obtained exact-ID inspection; persistence and locking
260/// remain the caller's responsibility, as does authenticating the adapter.
261pub fn require_absence(remove: &Request, inspection: &Response) -> Result<()> {
262    remove.validate()?;
263    let Operation::Remove { object_id } = &remove.operation else {
264        return Err(Invalid("reconciliation requires a removal intent"));
265    };
266    let expected = Request {
267        binding: remove.binding.clone(),
268        operation: Operation::Inspect {
269            object_id: object_id.clone(),
270        },
271    };
272    inspection.validate(&expected)?;
273    if inspection.outcome != Outcome::Absent {
274        return Err(Invalid("exact object absence not established"));
275    }
276    Ok(())
277}
278
279#[cfg(test)]
280mod contract;