Skip to main content

a3s_runtime/contract/
protocol.rs

1use super::{RuntimeObservation, RuntimeUnitSpec};
2use serde::{Deserialize, Serialize};
3use sha2::{Digest, Sha256};
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(deny_unknown_fields)]
7pub struct RuntimeApplyRequest {
8    pub schema: String,
9    pub request_id: String,
10    pub deadline_at_ms: Option<u64>,
11    pub spec: RuntimeUnitSpec,
12}
13
14impl RuntimeApplyRequest {
15    pub const SCHEMA: &'static str = "a3s.runtime.apply-request.v1";
16
17    pub fn validate(&self) -> Result<(), String> {
18        if self.schema != Self::SCHEMA {
19            return Err(format!(
20                "unsupported Runtime apply schema {:?}",
21                self.schema
22            ));
23        }
24        super::validate_id("request_id", &self.request_id, 512)?;
25        if self.deadline_at_ms == Some(0) {
26            return Err("deadline_at_ms must be positive when present".into());
27        }
28        self.spec.validate()
29    }
30
31    pub fn digest(&self) -> Result<String, String> {
32        canonical_digest(self, self.validate())
33    }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(deny_unknown_fields)]
38pub struct RuntimeActionRequest {
39    pub schema: String,
40    pub request_id: String,
41    pub unit_id: String,
42    pub generation: u64,
43    pub deadline_at_ms: Option<u64>,
44}
45
46impl RuntimeActionRequest {
47    pub const SCHEMA: &'static str = "a3s.runtime.action-request.v1";
48
49    pub fn validate(&self) -> Result<(), String> {
50        if self.schema != Self::SCHEMA {
51            return Err(format!(
52                "unsupported Runtime action schema {:?}",
53                self.schema
54            ));
55        }
56        super::validate_id("request_id", &self.request_id, 512)?;
57        super::validate_id("unit_id", &self.unit_id, 512)?;
58        if self.generation == 0 || self.deadline_at_ms == Some(0) {
59            return Err("action generation and deadline must be positive".into());
60        }
61        Ok(())
62    }
63
64    pub fn digest(&self) -> Result<String, String> {
65        canonical_digest(self, self.validate())
66    }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(deny_unknown_fields)]
71pub struct RuntimeRemoval {
72    pub schema: String,
73    pub request_id: String,
74    pub unit_id: String,
75    pub generation: u64,
76    pub removed_at_ms: u64,
77    pub already_absent: bool,
78}
79
80impl RuntimeRemoval {
81    pub const SCHEMA: &'static str = "a3s.runtime.removal.v1";
82
83    pub fn validate(&self) -> Result<(), String> {
84        if self.schema != Self::SCHEMA {
85            return Err(format!(
86                "unsupported Runtime removal schema {:?}",
87                self.schema
88            ));
89        }
90        super::validate_id("request_id", &self.request_id, 512)?;
91        super::validate_id("unit_id", &self.unit_id, 512)?;
92        if self.generation == 0 {
93            return Err("removal generation must be positive".into());
94        }
95        Ok(())
96    }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(rename_all = "snake_case")]
101pub enum RuntimeLogStream {
102    Stdout,
103    Stderr,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(deny_unknown_fields)]
108pub struct RuntimeLogQuery {
109    pub schema: String,
110    pub unit_id: String,
111    pub generation: u64,
112    pub cursor: Option<String>,
113    pub limit: u32,
114    pub stream: Option<RuntimeLogStream>,
115}
116
117impl RuntimeLogQuery {
118    pub const SCHEMA: &'static str = "a3s.runtime.log-query.v1";
119
120    pub fn validate(&self) -> Result<(), String> {
121        if self.schema != Self::SCHEMA {
122            return Err(format!(
123                "unsupported Runtime log query schema {:?}",
124                self.schema
125            ));
126        }
127        super::validate_id("unit_id", &self.unit_id, 512)?;
128        if self.generation == 0 || self.limit == 0 || self.limit > 10_000 {
129            return Err("log generation or limit is invalid".into());
130        }
131        if self
132            .cursor
133            .as_ref()
134            .is_some_and(|value| value.is_empty() || value.len() > 1024 || value.contains('\0'))
135        {
136            return Err("log cursor is invalid".into());
137        }
138        Ok(())
139    }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143#[serde(deny_unknown_fields)]
144pub struct RuntimeLogChunk {
145    pub schema: String,
146    pub cursor: String,
147    pub sequence: u64,
148    pub observed_at_ms: u64,
149    pub stream: RuntimeLogStream,
150    pub data: String,
151}
152
153impl RuntimeLogChunk {
154    pub const SCHEMA: &'static str = "a3s.runtime.log-chunk.v1";
155
156    pub fn validate(&self) -> Result<(), String> {
157        if self.schema != Self::SCHEMA {
158            return Err(format!(
159                "unsupported Runtime log chunk schema {:?}",
160                self.schema
161            ));
162        }
163        super::validate_nonempty("log cursor", &self.cursor, 1024)?;
164        if self.data.len() > 1024 * 1024 {
165            return Err("log chunk exceeds one MiB".into());
166        }
167        Ok(())
168    }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172#[serde(deny_unknown_fields)]
173pub struct RuntimeExecRequest {
174    pub schema: String,
175    pub request_id: String,
176    pub unit_id: String,
177    pub generation: u64,
178    pub command: Vec<String>,
179    pub timeout_ms: u64,
180    pub deadline_at_ms: Option<u64>,
181}
182
183impl RuntimeExecRequest {
184    pub const SCHEMA: &'static str = "a3s.runtime.exec-request.v1";
185
186    pub fn validate(&self) -> Result<(), String> {
187        if self.schema != Self::SCHEMA {
188            return Err(format!(
189                "unsupported Runtime exec request schema {:?}",
190                self.schema
191            ));
192        }
193        super::validate_id("request_id", &self.request_id, 512)?;
194        super::validate_id("unit_id", &self.unit_id, 512)?;
195        if self.generation == 0
196            || self.timeout_ms == 0
197            || self.deadline_at_ms == Some(0)
198            || self.command.is_empty()
199            || self.command.len() > 256
200            || self
201                .command
202                .iter()
203                .any(|value| value.is_empty() || value.len() > 32 * 1024 || value.contains('\0'))
204        {
205            return Err("exec request is invalid".into());
206        }
207        Ok(())
208    }
209
210    pub fn digest(&self) -> Result<String, String> {
211        canonical_digest(self, self.validate())
212    }
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216#[serde(deny_unknown_fields)]
217pub struct RuntimeExecResult {
218    pub schema: String,
219    pub request_id: String,
220    pub observation: RuntimeObservation,
221    pub exit_code: i32,
222    pub stdout: String,
223    pub stderr: String,
224    pub truncated: bool,
225}
226
227impl RuntimeExecResult {
228    pub const SCHEMA: &'static str = "a3s.runtime.exec-result.v1";
229
230    pub fn validate(&self) -> Result<(), String> {
231        if self.schema != Self::SCHEMA {
232            return Err(format!(
233                "unsupported Runtime exec result schema {:?}",
234                self.schema
235            ));
236        }
237        super::validate_id("request_id", &self.request_id, 512)?;
238        self.observation.validate()?;
239        if self.stdout.len() > 16 * 1024 * 1024 || self.stderr.len() > 16 * 1024 * 1024 {
240            return Err("exec output exceeds protocol limits".into());
241        }
242        Ok(())
243    }
244}
245
246fn canonical_digest<T: Serialize>(value: &T, valid: Result<(), String>) -> Result<String, String> {
247    valid?;
248    let bytes = serde_json::to_vec(value)
249        .map_err(|error| format!("could not encode Runtime request: {error}"))?;
250    Ok(format!("sha256:{:x}", Sha256::digest(bytes)))
251}