Skip to main content

amiss_wire/
requests.rs

1use crate::controls::Profile;
2use crate::controls::root;
3use crate::de::{self, Error, ErrorKind, Obj, fail};
4use crate::digest::Digest;
5use crate::json::Value;
6use crate::model::{BranchRef, ObjectFormat, Oid, RepositoryIdentity};
7
8pub const EVALUATION_REQUEST_SCHEMA: &str = "amiss/scanner-evaluation-request/v1";
9pub const SNAPSHOT_REQUEST_SCHEMA: &str = "amiss/scanner-snapshot-request/v1";
10pub const CONTROLS_REQUEST_SCHEMA: &str = "amiss/scanner-controls-request/v1";
11
12/// Every request stream is one complete bounded byte capture from byte zero
13/// through EOF; its diagnostic digest exists exactly when EOF was obtained
14/// within this cap.
15pub const REQUEST_STREAM_BYTES: u64 = 16_777_216;
16
17/// The published handle table's repository ordinal, constant across the
18/// in-process and future subprocess lanes.
19pub const REPOSITORY_HANDLE_ORDINAL: i64 = 3;
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum RequestMode {
23    CommitPair,
24    Index,
25}
26
27impl RequestMode {
28    #[must_use]
29    pub const fn as_str(self) -> &'static str {
30        match self {
31            Self::CommitPair => "commit-pair",
32            Self::Index => "index",
33        }
34    }
35}
36
37/// The run-identity request: profile, mode, and the exact snapshot
38/// identities to evaluate. The candidate commit is null exactly when the
39/// mode is `index`.
40#[derive(Clone, Debug, PartialEq, Eq)]
41pub struct EvaluationRequest {
42    pub profile: Profile,
43    pub mode: RequestMode,
44    pub object_format: ObjectFormat,
45    pub repository: Option<RepositoryIdentity>,
46    pub ref_name: Option<BranchRef>,
47    pub default_branch_ref: Option<BranchRef>,
48    pub base_commit: Oid,
49    pub candidate_commit: Option<Oid>,
50}
51
52impl EvaluationRequest {
53    /// # Errors
54    ///
55    /// Fails on strict-JSON defects, schema-shape violations, invalid
56    /// grammar values, and a candidate commit inconsistent with the mode.
57    pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
58        let value = root(bytes)?;
59        let mut obj = Obj::new("$", value)?;
60        de::const_str(
61            &obj.field("schema"),
62            obj.take("schema")?,
63            EVALUATION_REQUEST_SCHEMA,
64        )?;
65        let profile = Profile::decode(&obj.field("profile"), obj.take("profile")?)?;
66        let mode_path = obj.field("mode");
67        let mode = match de::string(&mode_path, obj.take("mode")?)?.as_str() {
68            "commit-pair" => RequestMode::CommitPair,
69            "index" => RequestMode::Index,
70            _ => return fail(&mode_path, ErrorKind::InvalidValue),
71        };
72        let format_path = obj.field("object_format");
73        let object_format = match de::string(&format_path, obj.take("object_format")?)?.as_str() {
74            "sha1" => ObjectFormat::Sha1,
75            "sha256" => ObjectFormat::Sha256,
76            _ => return fail(&format_path, ErrorKind::InvalidValue),
77        };
78        let repository_path = obj.field("repository");
79        let repository = match de::nullable(obj.take("repository")?) {
80            None => None,
81            Some(value) => Some(crate::controls::decode_repository(&repository_path, value)?),
82        };
83        let ref_path = obj.field("ref");
84        let ref_name = match de::nullable(obj.take("ref")?) {
85            None => None,
86            Some(value) => Some(decode_ref(&ref_path, value)?),
87        };
88        let default_path = obj.field("default_branch_ref");
89        let default_branch_ref = match de::nullable(obj.take("default_branch_ref")?) {
90            None => None,
91            Some(value) => Some(decode_ref(&default_path, value)?),
92        };
93        let base_path = obj.field("base_commit_oid");
94        let base_commit = Oid::new(
95            object_format,
96            de::string(&base_path, obj.take("base_commit_oid")?)?,
97        )
98        .ok_or_else(|| Error::new(&base_path, ErrorKind::InvalidValue))?;
99        let candidate_path = obj.field("candidate_commit_oid");
100        let candidate_commit = match de::nullable(obj.take("candidate_commit_oid")?) {
101            None => None,
102            Some(value) => Some(
103                Oid::new(object_format, de::string(&candidate_path, value)?)
104                    .ok_or_else(|| Error::new(&candidate_path, ErrorKind::InvalidValue))?,
105            ),
106        };
107        obj.finish()?;
108        let consistent = match mode {
109            RequestMode::CommitPair => candidate_commit.is_some(),
110            RequestMode::Index => candidate_commit.is_none(),
111        };
112        if !consistent {
113            return fail(&candidate_path, ErrorKind::Inconsistent);
114        }
115        Ok(Self {
116            profile,
117            mode,
118            object_format,
119            repository,
120            ref_name,
121            default_branch_ref,
122            base_commit,
123            candidate_commit,
124        })
125    }
126}
127
128fn decode_ref(path: &str, value: Value) -> Result<BranchRef, Error> {
129    BranchRef::new(de::string(path, value)?)
130        .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
131}
132
133/// The materialization request. `git-objects` pairs with mode `commit-pair`
134/// and `index` with mode `index`; the pairing law is checked against the
135/// evaluation request by the consumer, since each request parses alone.
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub struct SnapshotRequest {
138    pub materialization: RequestMode,
139}
140
141impl SnapshotRequest {
142    /// # Errors
143    ///
144    /// Fails on strict-JSON defects, schema-shape violations, and invalid
145    /// grammar values.
146    pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
147        let value = root(bytes)?;
148        let mut obj = Obj::new("$", value)?;
149        de::const_str(
150            &obj.field("schema"),
151            obj.take("schema")?,
152            SNAPSHOT_REQUEST_SCHEMA,
153        )?;
154        let materialization_path = obj.field("materialization");
155        let materialization =
156            match de::string(&materialization_path, obj.take("materialization")?)?.as_str() {
157                "git-objects" => RequestMode::CommitPair,
158                "index" => RequestMode::Index,
159                _ => return fail(&materialization_path, ErrorKind::InvalidValue),
160            };
161        let handle_path = obj.field("repository_handle");
162        if de::integer(&handle_path, obj.take("repository_handle")?)? != REPOSITORY_HANDLE_ORDINAL {
163            return fail(&handle_path, ErrorKind::InvalidValue);
164        }
165        let acquired_path = obj.field("pre_acquired");
166        if obj.take("pre_acquired")? != Value::Bool(true) {
167            return fail(&acquired_path, ErrorKind::InvalidValue);
168        }
169        obj.finish()?;
170        Ok(Self { materialization })
171    }
172}
173
174/// One supplied external control: the exact embedded JSON value, the
175/// independently acquired expected semantic digest, and the external trust
176/// source that authorized it.
177#[derive(Clone, Debug, PartialEq, Eq)]
178pub struct SuppliedControl {
179    pub value: Value,
180    pub expected_digest: Digest,
181    pub trust_source: RequestTrust,
182}
183
184#[derive(Clone, Copy, Debug, PartialEq, Eq)]
185pub enum RequestTrust {
186    ExternalRequiredWorkflow,
187    OrganizationRuleset,
188}
189
190impl RequestTrust {
191    #[must_use]
192    pub const fn as_str(self) -> &'static str {
193        match self {
194            Self::ExternalRequiredWorkflow => "external-required-workflow",
195            Self::OrganizationRuleset => "organization-ruleset",
196        }
197    }
198
199    fn decode(path: &str, value: Value) -> Result<Self, Error> {
200        match de::string(path, value)?.as_str() {
201            "external-required-workflow" => Ok(Self::ExternalRequiredWorkflow),
202            "organization-ruleset" => Ok(Self::OrganizationRuleset),
203            _ => fail(path, ErrorKind::InvalidValue),
204        }
205    }
206}
207
208/// The supplied trusted-time statement with the provider-authenticated run
209/// context the statement must identify. Its trust source is fixed.
210#[derive(Clone, Debug, PartialEq, Eq)]
211pub struct SuppliedTime {
212    pub value: Value,
213    pub expected_digest: Digest,
214    pub provider_run_id: String,
215    pub provider_run_attempt: u64,
216}
217
218/// The external-control request: five nullable supplied controls.
219#[derive(Clone, Debug, Default, PartialEq, Eq)]
220pub struct ControlsRequest {
221    pub organization_floor: Option<SuppliedControl>,
222    pub debt_snapshot: Option<SuppliedControl>,
223    pub waiver_bundle: Option<SuppliedControl>,
224    pub trusted_time: Option<SuppliedTime>,
225    pub execution_constraint: Option<SuppliedControl>,
226}
227
228impl ControlsRequest {
229    /// # Errors
230    ///
231    /// Fails on strict-JSON defects, schema-shape violations, and invalid
232    /// grammar values. Embedded control values are shape-checked as objects
233    /// only; their own schemas and digests are the consumer's verification.
234    pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
235        let value = root(bytes)?;
236        let mut obj = Obj::new("$", value)?;
237        de::const_str(
238            &obj.field("schema"),
239            obj.take("schema")?,
240            CONTROLS_REQUEST_SCHEMA,
241        )?;
242        let organization_floor = decode_supplied(
243            &obj.field("organization_floor"),
244            obj.take("organization_floor")?,
245        )?;
246        let debt_snapshot =
247            decode_supplied(&obj.field("debt_snapshot"), obj.take("debt_snapshot")?)?;
248        let waiver_bundle =
249            decode_supplied(&obj.field("waiver_bundle"), obj.take("waiver_bundle")?)?;
250        let trusted_time = decode_time(&obj.field("trusted_time"), obj.take("trusted_time")?)?;
251        let execution_constraint = decode_supplied(
252            &obj.field("execution_constraint"),
253            obj.take("execution_constraint")?,
254        )?;
255        obj.finish()?;
256        Ok(Self {
257            organization_floor,
258            debt_snapshot,
259            waiver_bundle,
260            trusted_time,
261            execution_constraint,
262        })
263    }
264}
265
266fn embedded_value(path: &str, value: Value) -> Result<Value, Error> {
267    match value {
268        Value::Object(_) => Ok(value),
269        Value::Null | Value::Bool(_) | Value::Integer(_) | Value::String(_) | Value::Array(_) => {
270            fail(path, ErrorKind::WrongType)
271        }
272    }
273}
274
275fn decode_supplied(path: &str, value: Value) -> Result<Option<SuppliedControl>, Error> {
276    let Some(value) = de::nullable(value) else {
277        return Ok(None);
278    };
279    let mut obj = Obj::new(path, value)?;
280    let embedded = embedded_value(&obj.field("value"), obj.take("value")?)?;
281    let digest_path = obj.field("expected_digest");
282    let expected_digest =
283        Digest::from_wire(&de::string(&digest_path, obj.take("expected_digest")?)?)
284            .ok_or_else(|| Error::new(&digest_path, ErrorKind::InvalidValue))?;
285    let trust_source = RequestTrust::decode(&obj.field("trust_source"), obj.take("trust_source")?)?;
286    obj.finish()?;
287    Ok(Some(SuppliedControl {
288        value: embedded,
289        expected_digest,
290        trust_source,
291    }))
292}
293
294fn decode_time(path: &str, value: Value) -> Result<Option<SuppliedTime>, Error> {
295    let Some(value) = de::nullable(value) else {
296        return Ok(None);
297    };
298    let mut obj = Obj::new(path, value)?;
299    let embedded = embedded_value(&obj.field("value"), obj.take("value")?)?;
300    let digest_path = obj.field("expected_digest");
301    let expected_digest =
302        Digest::from_wire(&de::string(&digest_path, obj.take("expected_digest")?)?)
303            .ok_or_else(|| Error::new(&digest_path, ErrorKind::InvalidValue))?;
304    let run_id_path = obj.field("provider_run_id");
305    let provider_run_id = de::string(&run_id_path, obj.take("provider_run_id")?)?;
306    let run_id_bytes = provider_run_id.as_bytes();
307    if run_id_bytes.is_empty()
308        || run_id_bytes.len() > 32
309        || !matches!(run_id_bytes.first(), Some(b'1'..=b'9'))
310        || !run_id_bytes.iter().all(u8::is_ascii_digit)
311    {
312        return fail(&run_id_path, ErrorKind::InvalidValue);
313    }
314    let attempt_path = obj.field("provider_run_attempt");
315    let attempt_raw = de::integer(&attempt_path, obj.take("provider_run_attempt")?)?;
316    let provider_run_attempt = u64::try_from(attempt_raw)
317        .ok()
318        .filter(|attempt| *attempt >= 1)
319        .ok_or_else(|| Error::new(&attempt_path, ErrorKind::InvalidValue))?;
320    obj.finish()?;
321    Ok(Some(SuppliedTime {
322        value: embedded,
323        expected_digest,
324        provider_run_id,
325        provider_run_attempt,
326    }))
327}