Skip to main content

amiss_bootstrap/
constraint.rs

1use std::fmt;
2
3use amiss_git::{GitResources, Repository};
4use amiss_wire::action::executable_platform;
5use amiss_wire::controls::{ExecutionConstraintDescriptor, ExecutionConstraintInput, GitMode};
6use amiss_wire::digest::{Digest, hb};
7use amiss_wire::model::{Oid, RepoPathText, RepositoryIdentity};
8
9use crate::build::{RELEASE_MANIFEST_DIGEST_PATH, RELEASE_MANIFEST_PATH};
10use crate::{
11    BOOTSTRAP_DOMAIN, Refusal, blob, load_release_manifest, resolve_action_tree, validate_release,
12};
13
14/// A provisioning failure with no runtime trust classification.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct ConstraintError {
17    /// Stable diagnostic for the failed derivation step.
18    pub reason: &'static str,
19}
20
21impl fmt::Display for ConstraintError {
22    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
23        formatter.write_str(self.reason)
24    }
25}
26
27impl std::error::Error for ConstraintError {}
28
29/// Derives the existing execution-constraint contract from one exact action
30/// commit and one bootstrap executable, then validates every dependency lock
31/// and the selected platform's runtime closure before returning it.
32///
33/// # Errors
34///
35/// The action commit, manifest marker, dependency locks, selected runtime,
36/// bootstrap platform, or supplied constraint fields are unavailable or
37/// inconsistent.
38pub fn derive_execution_constraint(
39    action: &Repository,
40    resources: &mut GitResources,
41    action_repository: &RepositoryIdentity,
42    action_commit_oid: &Oid,
43    required_status_name: &str,
44    bootstrap_bytes: &[u8],
45) -> Result<ExecutionConstraintDescriptor, ConstraintError> {
46    let platform = executable_platform(bootstrap_bytes).ok_or(ConstraintError {
47        reason: "bootstrap-platform-mismatch",
48    })?;
49    let tree =
50        resolve_action_tree(action, resources, action_commit_oid).map_err(constraint_error)?;
51    let manifest_path =
52        RepoPathText::new(RELEASE_MANIFEST_PATH.to_owned()).ok_or(ConstraintError {
53            reason: "execution-constraint-invalid",
54        })?;
55    let manifest = load_release_manifest(action, resources, &tree, &manifest_path)
56        .map_err(constraint_error)?;
57    let marker_path =
58        RepoPathText::new(RELEASE_MANIFEST_DIGEST_PATH.to_owned()).ok_or(ConstraintError {
59            reason: "execution-constraint-invalid",
60        })?;
61    let (marker, mode) = blob(action, resources, &tree, &marker_path).map_err(constraint_error)?;
62    if mode != GitMode::RegularFile {
63        return Err(ConstraintError {
64            reason: "manifest-digest-mismatch",
65        });
66    }
67    let marked_digest = parse_marker(&marker).ok_or(ConstraintError {
68        reason: "manifest-digest-mismatch",
69    })?;
70    if marked_digest != manifest.digest {
71        return Err(ConstraintError {
72            reason: "manifest-digest-mismatch",
73        });
74    }
75    let descriptor = ExecutionConstraintDescriptor::new(ExecutionConstraintInput {
76        action_repository: action_repository.clone(),
77        action_object_format: action.object_format(),
78        action_commit_oid: action_commit_oid.clone(),
79        action_tree_oid: tree.clone(),
80        manifest_path,
81        release_manifest_digest: manifest.digest,
82        selected_platform: platform,
83        required_status_name: required_status_name.to_owned(),
84        bootstrap_digest: hb(BOOTSTRAP_DOMAIN, bootstrap_bytes),
85    })
86    .map_err(|_defect| ConstraintError {
87        reason: "execution-constraint-invalid",
88    })?;
89    validate_release(action, resources, &tree, manifest, platform).map_err(constraint_error)?;
90    Ok(descriptor)
91}
92
93const fn constraint_error(refusal: Refusal) -> ConstraintError {
94    match refusal {
95        Refusal::Unavailable(reason) | Refusal::Tampered(reason) => ConstraintError { reason },
96    }
97}
98
99fn parse_marker(bytes: &[u8]) -> Option<Digest> {
100    bytes
101        .strip_suffix(b"\n")
102        .and_then(|text| std::str::from_utf8(text).ok())
103        .and_then(Digest::from_wire)
104}