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}