1use std::process::{Child, ExitStatus};
2use std::time::{Duration, Instant};
3
4use amiss_wire::controls::{ExecutionConstraintDescriptor, TrustedTimeStatement};
5use amiss_wire::digest::hj;
6use amiss_wire::json::{Value, canonical, parse};
7use amiss_wire::model::RepositoryIdentity;
8use amiss_wire::report::{ENVELOPE_SCHEMA, PAYLOAD_SCHEMA};
9use amiss_wire::requests::CANDIDATE_IDENTITY_DOMAIN;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum AcceptanceDefect {
15 Shape,
17 Noncanonical,
19 PayloadDigest,
21 Engine,
23 BaseIdentity,
25 CandidateIdentity,
27 SealedIdentity,
29 SealedControls,
31 Completeness,
33 FindingCount,
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct Expectations {
43 pub engine_digest: String,
44 pub base_commit: String,
45 pub candidate_commit: Option<String>,
46 pub sealed: Option<SealedExpectations>,
47}
48
49#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct SealedExpectations {
53 pub profile: String,
54 pub candidate_ref: String,
55 pub target_ref: String,
56 pub repository: RepositoryIdentity,
57 pub provider: String,
58 pub provider_run_id: String,
59 pub provider_run_attempt: u64,
60 pub candidate_identity_digest: String,
61 pub organization_floor: Option<SealedControlExpectation>,
62 pub debt_snapshot: Option<SealedControlExpectation>,
63 pub waiver_bundle: Option<SealedControlExpectation>,
64 pub execution_constraint: SealedControlExpectation,
65 pub trusted_time_digest: String,
66}
67
68#[derive(Clone, Debug, PartialEq, Eq)]
71pub struct SealedControlExpectation {
72 pub digest: String,
73 pub trust_source: String,
74}
75
76fn member<'value>(value: &'value Value, key: &str) -> Option<&'value Value> {
77 match value {
78 Value::Object(members) => members
79 .iter()
80 .find(|(name, _)| name == key)
81 .map(|(_, member)| member),
82 Value::Null | Value::Bool(_) | Value::Integer(_) | Value::String(_) | Value::Array(_) => {
83 None
84 }
85 }
86}
87
88fn text<'value>(value: &'value Value, key: &str) -> Option<&'value str> {
89 match member(value, key) {
90 Some(Value::String(text)) => Some(text),
91 _ => None,
92 }
93}
94
95pub fn accept(wire: &[u8], expectations: &Expectations) -> Result<i64, AcceptanceDefect> {
107 let trimmed = wire
108 .strip_suffix(b"\n")
109 .ok_or(AcceptanceDefect::Noncanonical)?;
110 let envelope = parse(trimmed).map_err(|_defect| AcceptanceDefect::Shape)?;
111 if canonical(&envelope) != trimmed {
112 return Err(AcceptanceDefect::Noncanonical);
113 }
114 if text(&envelope, "schema") != Some(ENVELOPE_SCHEMA) {
115 return Err(AcceptanceDefect::Shape);
116 }
117 let payload = member(&envelope, "payload").ok_or(AcceptanceDefect::Shape)?;
118 if text(payload, "schema") != Some(PAYLOAD_SCHEMA) {
119 return Err(AcceptanceDefect::Shape);
120 }
121 let recorded = text(&envelope, "payload_digest").ok_or(AcceptanceDefect::Shape)?;
122 if hj(PAYLOAD_SCHEMA, payload).to_string() != recorded {
123 return Err(AcceptanceDefect::PayloadDigest);
124 }
125 let engine_row = member(payload, "engine").ok_or(AcceptanceDefect::Shape)?;
126 if text(engine_row, "engine_digest") != Some(expectations.engine_digest.as_str()) {
127 return Err(AcceptanceDefect::Engine);
128 }
129 let evaluation = member(payload, "evaluation").ok_or(AcceptanceDefect::Shape)?;
130 let resolved = text(evaluation, "status") != Some("unavailable");
131 if expectations.sealed.is_some() && !resolved {
132 return Err(AcceptanceDefect::SealedIdentity);
133 }
134 if resolved {
135 let base = member(evaluation, "base").ok_or(AcceptanceDefect::Shape)?;
136 if text(base, "commit_oid") != Some(expectations.base_commit.as_str()) {
137 return Err(AcceptanceDefect::BaseIdentity);
138 }
139 let candidate = member(evaluation, "candidate").ok_or(AcceptanceDefect::Shape)?;
140 if let Some(expected) = expectations.candidate_commit.as_deref()
141 && (text(candidate, "kind") != Some("git-commit")
142 || text(candidate, "commit_oid") != Some(expected))
143 {
144 return Err(AcceptanceDefect::CandidateIdentity);
145 }
146 if let Some(sealed) = &expectations.sealed {
147 accept_sealed(payload, evaluation, sealed)?;
148 }
149 }
150 let result = member(payload, "result").ok_or(AcceptanceDefect::Shape)?;
151 let exit_code = match member(result, "exit_code") {
152 Some(Value::Integer(code)) => *code,
153 _ => return Err(AcceptanceDefect::Shape),
154 };
155 let complete = member(result, "complete") == Some(&Value::Bool(true));
156 if complete != (exit_code == 0 || exit_code == 1) {
157 return Err(AcceptanceDefect::Completeness);
158 }
159 let count = match member(result, "finding_count") {
160 Some(Value::Integer(count)) => *count,
161 _ => return Err(AcceptanceDefect::Shape),
162 };
163 let findings = match member(payload, "findings") {
164 Some(Value::Array(rows)) => rows.len(),
165 _ => return Err(AcceptanceDefect::Shape),
166 };
167 if i64::try_from(findings).map_err(|_defect| AcceptanceDefect::Shape)? != count {
168 return Err(AcceptanceDefect::FindingCount);
169 }
170 Ok(exit_code)
171}
172
173fn accept_sealed(
174 payload: &Value,
175 evaluation: &Value,
176 expected: &SealedExpectations,
177) -> Result<(), AcceptanceDefect> {
178 if text(evaluation, "candidate_ref") != Some(expected.candidate_ref.as_str())
179 || text(evaluation, "target_ref") != Some(expected.target_ref.as_str())
180 || member(evaluation, "trusted_time") != Some(&Value::Bool(true))
181 {
182 return Err(AcceptanceDefect::SealedIdentity);
183 }
184 let Value::Object(evaluation_members) = evaluation else {
185 return Err(AcceptanceDefect::Shape);
186 };
187 let mut identity_members: Vec<(String, Value)> = evaluation_members
188 .iter()
189 .filter(|(name, _value)| name != "evaluation_instant" && name != "trusted_time")
190 .cloned()
191 .collect();
192 identity_members.push((
193 "schema".to_owned(),
194 Value::String(CANDIDATE_IDENTITY_DOMAIN.to_owned()),
195 ));
196 let identity = Value::Object(identity_members);
197 let identity_digest = hj(CANDIDATE_IDENTITY_DOMAIN, &identity).to_string();
198 if identity_digest != expected.candidate_identity_digest {
199 return Err(AcceptanceDefect::SealedIdentity);
200 }
201
202 let controls = member(payload, "controls").ok_or(AcceptanceDefect::SealedControls)?;
203 if text(controls, "profile") != Some(expected.profile.as_str()) {
204 return Err(AcceptanceDefect::SealedControls);
205 }
206 accept_optional_control(
207 controls,
208 "organization_floor",
209 expected.organization_floor.as_ref(),
210 )?;
211 accept_optional_control(controls, "debt_snapshot", expected.debt_snapshot.as_ref())?;
212 accept_optional_control(controls, "waiver_bundle", expected.waiver_bundle.as_ref())?;
213 let constraint =
214 member(controls, "execution_constraint").ok_or(AcceptanceDefect::SealedControls)?;
215 let descriptor = member(constraint, "descriptor").ok_or(AcceptanceDefect::SealedControls)?;
216 let descriptor = ExecutionConstraintDescriptor::parse(&canonical(descriptor))
217 .map_err(|_defect| AcceptanceDefect::SealedControls)?;
218 if text(constraint, "status") != Some("verified")
219 || text(constraint, "descriptor_digest")
220 != Some(expected.execution_constraint.digest.as_str())
221 || text(constraint, "trust_source")
222 != Some(expected.execution_constraint.trust_source.as_str())
223 || descriptor.digest.to_string() != expected.execution_constraint.digest
224 {
225 return Err(AcceptanceDefect::SealedControls);
226 }
227 let trusted =
228 member(controls, "trusted_time_source").ok_or(AcceptanceDefect::SealedControls)?;
229 let statement = member(trusted, "statement").ok_or(AcceptanceDefect::SealedControls)?;
230 let statement = TrustedTimeStatement::parse(&canonical(statement))
231 .map_err(|_defect| AcceptanceDefect::SealedControls)?;
232 if text(trusted, "status") != Some("verified")
233 || text(trusted, "trust_source") != Some("external-required-check")
234 || text(trusted, "statement_digest") != Some(expected.trusted_time_digest.as_str())
235 || statement.digest.to_string() != expected.trusted_time_digest
236 || statement.provider != expected.provider
237 || statement.provider_run_id != expected.provider_run_id
238 || statement.provider_run_attempt != expected.provider_run_attempt
239 || statement.repository != expected.repository
240 || statement.ref_name.as_str() != expected.target_ref
241 || statement.candidate_identity_digest.to_string() != identity_digest
242 || text(evaluation, "evaluation_instant") != Some(statement.evaluation_instant.as_str())
243 {
244 return Err(AcceptanceDefect::SealedControls);
245 }
246 let sandbox = member(controls, "sandbox").ok_or(AcceptanceDefect::SealedControls)?;
247 if text(sandbox, "assurance") != Some("self-asserted")
248 || text(sandbox, "enforcement_source") != Some("local-process")
249 || member(sandbox, "verification") != Some(&Value::Null)
250 {
251 return Err(AcceptanceDefect::SealedControls);
252 }
253 Ok(())
254}
255
256fn accept_optional_control(
257 controls: &Value,
258 name: &str,
259 expected: Option<&SealedControlExpectation>,
260) -> Result<(), AcceptanceDefect> {
261 let control = member(controls, name).ok_or(AcceptanceDefect::SealedControls)?;
262 let accepted = match expected {
263 Some(expected) => {
264 text(control, "status") == Some("verified")
265 && text(control, "digest") == Some(expected.digest.as_str())
266 && text(control, "trust_source") == Some(expected.trust_source.as_str())
267 }
268 None => {
269 text(control, "status") == Some("none")
270 && member(control, "digest") == Some(&Value::Null)
271 && text(control, "trust_source") == Some("none")
272 }
273 };
274 if accepted {
275 Ok(())
276 } else {
277 Err(AcceptanceDefect::SealedControls)
278 }
279}
280
281#[derive(Debug)]
283pub enum Supervised {
284 Completed(ExitStatus),
286 Killed,
289}
290
291pub fn supervise(child: &mut Child, ceiling: Duration) -> std::io::Result<Supervised> {
301 let start = Instant::now();
302 loop {
303 if let Some(status) = child.try_wait()? {
304 return Ok(Supervised::Completed(status));
305 }
306 if start.elapsed() >= ceiling {
307 let _signalled = child.kill();
308 let _reaped = child.wait();
309 return Ok(Supervised::Killed);
310 }
311 std::thread::sleep(Duration::from_millis(25));
312 }
313}
314
315#[derive(Clone, Copy, Debug, PartialEq, Eq)]
319pub enum Defect {
320 Killed,
322 Signalled,
324 Oversize,
326 ExitMismatch,
328 Acceptance(AcceptanceDefect),
330}
331
332pub fn settle(
341 outcome: &Supervised,
342 stdout: &[u8],
343 expectations: &Expectations,
344) -> Result<i64, Defect> {
345 let status = match *outcome {
346 Supervised::Killed => return Err(Defect::Killed),
347 Supervised::Completed(status) => status,
348 };
349 if u64::try_from(stdout.len()).unwrap_or(u64::MAX) > amiss_wire::report::MACHINE_JSON_BYTES {
350 return Err(Defect::Oversize);
351 }
352 let code = status.code().ok_or(Defect::Signalled)?;
353 let class = accept(stdout, expectations).map_err(Defect::Acceptance)?;
354 if i64::from(code) != class {
355 return Err(Defect::ExitMismatch);
356 }
357 Ok(class)
358}