lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

service_source_lock.rs (56988B)


      1 use std::{collections::BTreeSet, fmt, fmt::Write as _, fs, io::Read as _, path::Path};
      2 
      3 use serde::{
      4     Deserialize, Deserializer,
      5     de::{self, MapAccess, SeqAccess, Visitor},
      6 };
      7 use sha2::{Digest, Sha256};
      8 
      9 const CONTRACT_RELATIVE: &str =
     10     "contracts/architecture/decisions/services_hardening_source_lock.v2.json";
     11 const LOCK_SCHEMA: &str = "radroots.service.source-lock.v2";
     12 pub(crate) const LOCK_FILENAME: &str = "radroots.service.source-lock.v2.toml";
     13 pub(crate) const PREDECESSOR_LOCK_FILENAME: &str = "radroots.service.source-lock.v1.toml";
     14 pub(crate) const LIB_REPOSITORY: &str = "https://github.com/radrootslabs/lib";
     15 const ARCHITECTURE: &str = "radroots.crates.release.v2";
     16 const LIB_VERSION: &str = "0.1.0-alpha";
     17 const RUST_VERSION: &str = "1.97.1";
     18 const HOST_FEATURE_PROFILE: &str = "service-host";
     19 const MAX_LOCK_BYTES: usize = 4096;
     20 const MAX_SERVICE_BYTES: usize = 128;
     21 const MAX_CONTRACT_BYTES: usize = 32_768;
     22 const MAX_FLAKE_NIX_BYTES: usize = 1_048_576;
     23 const MAX_FLAKE_LOCK_BYTES: usize = 4_194_304;
     24 
     25 const DEFERRED_FIELD_ORDER: [&str; 20] = [
     26     "schema",
     27     "contract_version",
     28     "service",
     29     "repository",
     30     "revision",
     31     "architecture",
     32     "workspace_catalog_sha256",
     33     "version",
     34     "source_archive_sha256",
     35     "cargo_lock_sha256",
     36     "rust_version",
     37     "host_feature_profile",
     38     "nix.material",
     39     "nix.lib_revision",
     40     "nix.flake_lock_sha256",
     41     "contract_versions.config",
     42     "contract_versions.state",
     43     "contract_versions.admin",
     44     "contract_versions.status",
     45     "contract_versions.provider",
     46 ];
     47 
     48 const ABSENT_FIELD_ORDER: [&str; 18] = [
     49     "schema",
     50     "contract_version",
     51     "service",
     52     "repository",
     53     "revision",
     54     "architecture",
     55     "workspace_catalog_sha256",
     56     "version",
     57     "source_archive_sha256",
     58     "cargo_lock_sha256",
     59     "rust_version",
     60     "host_feature_profile",
     61     "nix.material",
     62     "contract_versions.config",
     63     "contract_versions.state",
     64     "contract_versions.admin",
     65     "contract_versions.status",
     66     "contract_versions.provider",
     67 ];
     68 
     69 const ERROR_CODES: [&str; 11] = [
     70     "invalid_contract_version",
     71     "invalid_digest",
     72     "invalid_feature_profile",
     73     "invalid_fixed_identity",
     74     "invalid_nix_material",
     75     "invalid_revision",
     76     "invalid_service",
     77     "invalid_toolchain",
     78     "malformed",
     79     "noncanonical",
     80     "too_large",
     81 ];
     82 
     83 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     84 pub(crate) enum ServiceSourceLockError {
     85     TooLarge,
     86     Malformed,
     87     Noncanonical,
     88     InvalidFixedIdentity,
     89     InvalidNixMaterial,
     90     InvalidService,
     91     InvalidRevision,
     92     InvalidDigest,
     93     InvalidToolchain,
     94     InvalidFeatureProfile,
     95     InvalidContractVersion,
     96 }
     97 
     98 impl ServiceSourceLockError {
     99     fn code(self) -> &'static str {
    100         match self {
    101             Self::TooLarge => "too_large",
    102             Self::Malformed => "malformed",
    103             Self::Noncanonical => "noncanonical",
    104             Self::InvalidFixedIdentity => "invalid_fixed_identity",
    105             Self::InvalidNixMaterial => "invalid_nix_material",
    106             Self::InvalidService => "invalid_service",
    107             Self::InvalidRevision => "invalid_revision",
    108             Self::InvalidDigest => "invalid_digest",
    109             Self::InvalidToolchain => "invalid_toolchain",
    110             Self::InvalidFeatureProfile => "invalid_feature_profile",
    111             Self::InvalidContractVersion => "invalid_contract_version",
    112         }
    113     }
    114 }
    115 
    116 impl fmt::Display for ServiceSourceLockError {
    117     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    118         formatter.write_str(match self {
    119             Self::TooLarge => "service source lock exceeds its byte limit",
    120             Self::Malformed => "service source lock is malformed",
    121             Self::Noncanonical => "service source lock is not canonical",
    122             Self::InvalidFixedIdentity => "service source lock identity is invalid",
    123             Self::InvalidNixMaterial => "service source lock Nix material is invalid",
    124             Self::InvalidService => "service source lock service is invalid",
    125             Self::InvalidRevision => "service source lock revision is invalid",
    126             Self::InvalidDigest => "service source lock digest is invalid",
    127             Self::InvalidToolchain => "service source lock toolchain is invalid",
    128             Self::InvalidFeatureProfile => "service source lock feature profile is invalid",
    129             Self::InvalidContractVersion => "service source lock contract version is invalid",
    130         })
    131     }
    132 }
    133 
    134 impl std::error::Error for ServiceSourceLockError {}
    135 
    136 #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
    137 #[serde(deny_unknown_fields)]
    138 pub(crate) struct ContractVersions {
    139     config: u32,
    140     state: u32,
    141     admin: u32,
    142     status: u32,
    143     provider: u32,
    144 }
    145 
    146 impl ContractVersions {
    147     pub(crate) const fn new(
    148         config: u32,
    149         state: u32,
    150         admin: u32,
    151         status: u32,
    152         provider: u32,
    153     ) -> Self {
    154         Self {
    155             config,
    156             state,
    157             admin,
    158             status,
    159             provider,
    160         }
    161     }
    162 
    163     fn is_valid(self) -> bool {
    164         self.config != 0
    165             && self.state != 0
    166             && self.admin != 0
    167             && self.status != 0
    168             && self.provider != 0
    169     }
    170 
    171     pub(crate) const fn config(self) -> u32 {
    172         self.config
    173     }
    174 
    175     pub(crate) const fn state(self) -> u32 {
    176         self.state
    177     }
    178 
    179     pub(crate) const fn admin(self) -> u32 {
    180         self.admin
    181     }
    182 
    183     pub(crate) const fn status(self) -> u32 {
    184         self.status
    185     }
    186 
    187     pub(crate) const fn provider(self) -> u32 {
    188         self.provider
    189     }
    190 }
    191 
    192 #[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
    193 #[serde(deny_unknown_fields)]
    194 struct RawServiceSourceLock {
    195     schema: String,
    196     contract_version: u32,
    197     service: String,
    198     repository: String,
    199     revision: String,
    200     architecture: String,
    201     workspace_catalog_sha256: String,
    202     version: String,
    203     source_archive_sha256: String,
    204     cargo_lock_sha256: String,
    205     nix: RawNixMaterial,
    206     rust_version: String,
    207     host_feature_profile: String,
    208     contract_versions: ContractVersions,
    209 }
    210 
    211 #[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
    212 #[serde(tag = "material", rename_all = "snake_case", deny_unknown_fields)]
    213 enum RawNixMaterial {
    214     Absent,
    215     Deferred {
    216         lib_revision: String,
    217         flake_lock_sha256: String,
    218     },
    219 }
    220 
    221 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    222 pub(crate) enum NixMaterialParts<'a> {
    223     Absent,
    224     Deferred {
    225         lib_revision: &'a str,
    226         flake_lock_sha256: &'a str,
    227     },
    228 }
    229 
    230 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    231 pub(crate) enum NixMaterialState {
    232     Absent,
    233     Deferred,
    234 }
    235 
    236 #[derive(Clone, Debug, Eq, PartialEq)]
    237 pub(crate) struct DeferredNixMaterialEvidence {
    238     lib_revision: String,
    239     flake_lock_sha256: String,
    240 }
    241 
    242 impl DeferredNixMaterialEvidence {
    243     pub(crate) fn lib_revision(&self) -> &str {
    244         &self.lib_revision
    245     }
    246 
    247     pub(crate) fn flake_lock_sha256(&self) -> &str {
    248         &self.flake_lock_sha256
    249     }
    250 }
    251 
    252 pub(crate) fn validate_deferred_nix_material(
    253     expression: &[u8],
    254     lock: &[u8],
    255 ) -> Result<DeferredNixMaterialEvidence, ServiceSourceLockError> {
    256     if expression.len() > MAX_FLAKE_NIX_BYTES || lock.len() > MAX_FLAKE_LOCK_BYTES {
    257         return Err(ServiceSourceLockError::InvalidNixMaterial);
    258     }
    259     let revision = validate_deferred_nix_lock(lock)?;
    260     let text =
    261         std::str::from_utf8(expression).map_err(|_| ServiceSourceLockError::InvalidNixMaterial)?;
    262     let expected = format!("url = \"github:radrootslabs/lib/{revision}\";");
    263     let lines = text
    264         .lines()
    265         .map(str::trim)
    266         .filter(|line| !line.is_empty())
    267         .collect::<Vec<_>>();
    268     let direct_blocks = lines
    269         .windows(4)
    270         .filter(|window| {
    271             window[0] == "inputs.lib = {"
    272                 && window[1] == expected
    273                 && window[2] == "flake = false;"
    274                 && window[3] == "};"
    275         })
    276         .count();
    277     let nested_blocks = lines
    278         .windows(5)
    279         .filter(|window| {
    280             window[0] == "inputs = {"
    281                 && window[1] == "lib = {"
    282                 && window[2] == expected
    283                 && window[3] == "flake = false;"
    284                 && window[4] == "};"
    285         })
    286         .count();
    287     let selecting_lines = lines
    288         .iter()
    289         .filter(|line| line.contains("github:radrootslabs/lib"))
    290         .count();
    291     if direct_blocks + nested_blocks != 1 || selecting_lines != 1 {
    292         return Err(ServiceSourceLockError::InvalidNixMaterial);
    293     }
    294     Ok(DeferredNixMaterialEvidence {
    295         lib_revision: revision,
    296         flake_lock_sha256: hex::encode(Sha256::digest(lock)),
    297     })
    298 }
    299 
    300 pub(crate) fn validate_deferred_nix_lock(bytes: &[u8]) -> Result<String, ServiceSourceLockError> {
    301     let mut deserializer = serde_json::Deserializer::from_slice(bytes);
    302     let value = NoDuplicateJsonValue::deserialize(&mut deserializer)
    303         .map_err(|_| ServiceSourceLockError::InvalidNixMaterial)?
    304         .0;
    305     deserializer
    306         .end()
    307         .map_err(|_| ServiceSourceLockError::InvalidNixMaterial)?;
    308     if value
    309         .as_object()
    310         .map(|object| object.keys().map(String::as_str).collect::<BTreeSet<_>>())
    311         != Some(BTreeSet::from(["nodes", "root", "version"]))
    312     {
    313         return Err(ServiceSourceLockError::InvalidNixMaterial);
    314     }
    315     let version = value.get("version").and_then(serde_json::Value::as_u64);
    316     let root_name = value.get("root").and_then(serde_json::Value::as_str);
    317     let nodes = value.get("nodes").and_then(serde_json::Value::as_object);
    318     if version != Some(7) || root_name.is_none_or(str::is_empty) || nodes.is_none() {
    319         return Err(ServiceSourceLockError::InvalidNixMaterial);
    320     }
    321     let nodes = nodes.ok_or(ServiceSourceLockError::InvalidNixMaterial)?;
    322     let root = nodes
    323         .get(root_name.ok_or(ServiceSourceLockError::InvalidNixMaterial)?)
    324         .and_then(|node| node.get("inputs"))
    325         .and_then(serde_json::Value::as_object)
    326         .ok_or(ServiceSourceLockError::InvalidNixMaterial)?;
    327     let direct_lib_node = root.get("lib").and_then(serde_json::Value::as_str);
    328     let mut lib_nodes = 0_usize;
    329     let mut exact_direct = 0_usize;
    330     let mut selected_revision = None;
    331     for (name, node) in nodes {
    332         let locked = node.get("locked").and_then(serde_json::Value::as_object);
    333         let original = node.get("original").and_then(serde_json::Value::as_object);
    334         let is_lib = locked.is_some_and(|locked| {
    335             locked.get("owner").and_then(serde_json::Value::as_str) == Some("radrootslabs")
    336                 && locked.get("repo").and_then(serde_json::Value::as_str) == Some("lib")
    337         }) || original.is_some_and(|original| {
    338             original.get("owner").and_then(serde_json::Value::as_str) == Some("radrootslabs")
    339                 && original.get("repo").and_then(serde_json::Value::as_str) == Some("lib")
    340         });
    341         if !is_lib {
    342             continue;
    343         }
    344         lib_nodes += 1;
    345         let locked = locked.ok_or(ServiceSourceLockError::InvalidNixMaterial)?;
    346         let original = original.ok_or(ServiceSourceLockError::InvalidNixMaterial)?;
    347         let locked_keys = locked.keys().map(String::as_str).collect::<BTreeSet<_>>();
    348         let original_keys = original.keys().map(String::as_str).collect::<BTreeSet<_>>();
    349         let locked_revision = locked.get("rev").and_then(serde_json::Value::as_str);
    350         let original_revision = original.get("rev").and_then(serde_json::Value::as_str);
    351         let exact = locked_keys
    352             == BTreeSet::from(["lastModified", "narHash", "owner", "repo", "rev", "type"])
    353             && original_keys == BTreeSet::from(["owner", "repo", "rev", "type"])
    354             && node.as_object().is_some_and(|node| {
    355                 node.keys().map(String::as_str).collect::<BTreeSet<_>>()
    356                     == BTreeSet::from(["locked", "original"])
    357             })
    358             && locked
    359                 .get("lastModified")
    360                 .and_then(serde_json::Value::as_u64)
    361                 .is_some()
    362             && locked.get("type").and_then(serde_json::Value::as_str) == Some("github")
    363             && locked.get("owner").and_then(serde_json::Value::as_str) == Some("radrootslabs")
    364             && locked.get("repo").and_then(serde_json::Value::as_str) == Some("lib")
    365             && locked_revision.is_some_and(|revision| valid_lower_hex(revision, 40))
    366             && locked
    367                 .get("narHash")
    368                 .and_then(serde_json::Value::as_str)
    369                 .is_some_and(valid_nix_sha256)
    370             && original.get("type").and_then(serde_json::Value::as_str) == Some("github")
    371             && original.get("owner").and_then(serde_json::Value::as_str) == Some("radrootslabs")
    372             && original.get("repo").and_then(serde_json::Value::as_str) == Some("lib")
    373             && original_revision == locked_revision
    374             && original.get("ref").is_none();
    375         if direct_lib_node == Some(name) && root_name != Some(name.as_str()) && exact {
    376             exact_direct += 1;
    377             selected_revision = locked_revision.map(str::to_owned);
    378         }
    379     }
    380     if lib_nodes == 1 && exact_direct == 1 {
    381         selected_revision.ok_or(ServiceSourceLockError::InvalidNixMaterial)
    382     } else {
    383         Err(ServiceSourceLockError::InvalidNixMaterial)
    384     }
    385 }
    386 
    387 struct NoDuplicateJsonValue(serde_json::Value);
    388 
    389 impl<'de> Deserialize<'de> for NoDuplicateJsonValue {
    390     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    391     where
    392         D: Deserializer<'de>,
    393     {
    394         deserializer.deserialize_any(NoDuplicateJsonValueVisitor)
    395     }
    396 }
    397 
    398 struct NoDuplicateJsonValueVisitor;
    399 
    400 impl<'de> Visitor<'de> for NoDuplicateJsonValueVisitor {
    401     type Value = NoDuplicateJsonValue;
    402 
    403     fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    404         formatter.write_str("JSON without duplicate object keys")
    405     }
    406 
    407     fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
    408         Ok(NoDuplicateJsonValue(serde_json::Value::Bool(value)))
    409     }
    410 
    411     fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
    412         Ok(NoDuplicateJsonValue(serde_json::Value::Number(
    413             value.into(),
    414         )))
    415     }
    416 
    417     fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
    418         Ok(NoDuplicateJsonValue(serde_json::Value::Number(
    419             value.into(),
    420         )))
    421     }
    422 
    423     fn visit_f64<E>(self, _value: f64) -> Result<Self::Value, E>
    424     where
    425         E: de::Error,
    426     {
    427         Err(E::custom("unsupported JSON number"))
    428     }
    429 
    430     fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> {
    431         Ok(NoDuplicateJsonValue(serde_json::Value::String(
    432             value.to_owned(),
    433         )))
    434     }
    435 
    436     fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
    437         Ok(NoDuplicateJsonValue(serde_json::Value::String(value)))
    438     }
    439 
    440     fn visit_none<E>(self) -> Result<Self::Value, E> {
    441         Ok(NoDuplicateJsonValue(serde_json::Value::Null))
    442     }
    443 
    444     fn visit_unit<E>(self) -> Result<Self::Value, E> {
    445         Ok(NoDuplicateJsonValue(serde_json::Value::Null))
    446     }
    447 
    448     fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
    449     where
    450         D: Deserializer<'de>,
    451     {
    452         Deserialize::deserialize(deserializer)
    453     }
    454 
    455     fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
    456     where
    457         A: SeqAccess<'de>,
    458     {
    459         let mut values = Vec::new();
    460         while let Some(value) = sequence.next_element::<NoDuplicateJsonValue>()? {
    461             values.push(value.0);
    462         }
    463         Ok(NoDuplicateJsonValue(serde_json::Value::Array(values)))
    464     }
    465 
    466     fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
    467     where
    468         A: MapAccess<'de>,
    469     {
    470         let mut values = serde_json::Map::new();
    471         while let Some(key) = map.next_key::<String>()? {
    472             if values.contains_key(&key) {
    473                 return Err(<A::Error as de::Error>::custom("duplicate JSON object key"));
    474             }
    475             let value = map.next_value::<NoDuplicateJsonValue>()?;
    476             values.insert(key, value.0);
    477         }
    478         Ok(NoDuplicateJsonValue(serde_json::Value::Object(values)))
    479     }
    480 }
    481 
    482 fn valid_nix_sha256(value: &str) -> bool {
    483     let Some(encoded) = value.strip_prefix("sha256-") else {
    484         return false;
    485     };
    486     let bytes = encoded.as_bytes();
    487     bytes.len() == 44
    488         && bytes[43] == b'='
    489         && bytes[..43]
    490             .iter()
    491             .copied()
    492             .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/'))
    493 }
    494 
    495 pub(crate) struct ServiceSourceLockParts<'a> {
    496     pub(crate) service: &'a str,
    497     pub(crate) revision: &'a str,
    498     pub(crate) workspace_catalog_sha256: &'a str,
    499     pub(crate) source_archive_sha256: &'a str,
    500     pub(crate) cargo_lock_sha256: &'a str,
    501     pub(crate) nix: NixMaterialParts<'a>,
    502     pub(crate) contract_versions: ContractVersions,
    503 }
    504 
    505 #[derive(Clone, Eq, PartialEq)]
    506 pub(crate) struct ServiceSourceLockV2 {
    507     raw: RawServiceSourceLock,
    508     canonical: Box<[u8]>,
    509 }
    510 
    511 impl fmt::Debug for ServiceSourceLockV2 {
    512     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    513         formatter
    514             .debug_struct("ServiceSourceLockV2")
    515             .finish_non_exhaustive()
    516     }
    517 }
    518 
    519 impl ServiceSourceLockV2 {
    520     pub(crate) fn new(parts: ServiceSourceLockParts<'_>) -> Result<Self, ServiceSourceLockError> {
    521         validate_parts(&parts)?;
    522         let raw = RawServiceSourceLock {
    523             schema: LOCK_SCHEMA.to_owned(),
    524             contract_version: 2,
    525             service: parts.service.to_owned(),
    526             repository: LIB_REPOSITORY.to_owned(),
    527             revision: parts.revision.to_owned(),
    528             architecture: ARCHITECTURE.to_owned(),
    529             workspace_catalog_sha256: parts.workspace_catalog_sha256.to_owned(),
    530             version: LIB_VERSION.to_owned(),
    531             source_archive_sha256: parts.source_archive_sha256.to_owned(),
    532             cargo_lock_sha256: parts.cargo_lock_sha256.to_owned(),
    533             nix: match parts.nix {
    534                 NixMaterialParts::Absent => RawNixMaterial::Absent,
    535                 NixMaterialParts::Deferred {
    536                     lib_revision,
    537                     flake_lock_sha256,
    538                 } => RawNixMaterial::Deferred {
    539                     lib_revision: lib_revision.to_owned(),
    540                     flake_lock_sha256: flake_lock_sha256.to_owned(),
    541                 },
    542             },
    543             rust_version: RUST_VERSION.to_owned(),
    544             host_feature_profile: HOST_FEATURE_PROFILE.to_owned(),
    545             contract_versions: parts.contract_versions,
    546         };
    547         Ok(Self::from_validated_raw(raw))
    548     }
    549 
    550     pub(crate) fn from_canonical_bytes(bytes: &[u8]) -> Result<Self, ServiceSourceLockError> {
    551         if bytes.len() > MAX_LOCK_BYTES {
    552             return Err(ServiceSourceLockError::TooLarge);
    553         }
    554         let text = std::str::from_utf8(bytes).map_err(|_| ServiceSourceLockError::Malformed)?;
    555         let raw = toml::from_str::<RawServiceSourceLock>(text)
    556             .map_err(|_| ServiceSourceLockError::Malformed)?;
    557         validate_raw(&raw)?;
    558         let lock = Self::from_validated_raw(raw);
    559         if lock.canonical.as_ref() != bytes {
    560             return Err(ServiceSourceLockError::Noncanonical);
    561         }
    562         Ok(lock)
    563     }
    564 
    565     fn from_validated_raw(raw: RawServiceSourceLock) -> Self {
    566         let canonical = render(&raw).into_bytes().into_boxed_slice();
    567         debug_assert!(canonical.len() <= MAX_LOCK_BYTES);
    568         Self { raw, canonical }
    569     }
    570 
    571     pub(crate) fn canonical_bytes(&self) -> &[u8] {
    572         debug_assert_eq!(self.raw.schema, LOCK_SCHEMA);
    573         &self.canonical
    574     }
    575 
    576     pub(crate) fn service(&self) -> &str {
    577         &self.raw.service
    578     }
    579 
    580     pub(crate) fn revision(&self) -> &str {
    581         &self.raw.revision
    582     }
    583 
    584     pub(crate) fn cargo_lock_sha256(&self) -> &str {
    585         &self.raw.cargo_lock_sha256
    586     }
    587 
    588     pub(crate) const fn nix_material_state(&self) -> NixMaterialState {
    589         match &self.raw.nix {
    590             RawNixMaterial::Absent => NixMaterialState::Absent,
    591             RawNixMaterial::Deferred { .. } => NixMaterialState::Deferred,
    592         }
    593     }
    594 
    595     pub(crate) fn nix_lib_revision(&self) -> Option<&str> {
    596         match &self.raw.nix {
    597             RawNixMaterial::Absent => None,
    598             RawNixMaterial::Deferred { lib_revision, .. } => Some(lib_revision),
    599         }
    600     }
    601 
    602     pub(crate) fn flake_lock_sha256(&self) -> Option<&str> {
    603         match &self.raw.nix {
    604             RawNixMaterial::Absent => None,
    605             RawNixMaterial::Deferred {
    606                 flake_lock_sha256, ..
    607             } => Some(flake_lock_sha256),
    608         }
    609     }
    610 
    611     pub(crate) const fn contract_versions(&self) -> ContractVersions {
    612         self.raw.contract_versions
    613     }
    614 }
    615 
    616 #[derive(Debug, Deserialize)]
    617 #[serde(deny_unknown_fields)]
    618 struct SourceLockDecision {
    619     schema: String,
    620     contract_version: u32,
    621     decision_state: String,
    622     predecessor: PredecessorDecision,
    623     lock_filename: String,
    624     lock_schema: String,
    625     canonical_encoding: String,
    626     maximum_lock_utf8_bytes: usize,
    627     maximum_service_utf8_bytes: usize,
    628     canonical_field_order_deferred: Vec<String>,
    629     canonical_field_order_absent: Vec<String>,
    630     fixed: FixedDecision,
    631     revision_encoding: String,
    632     digest_encoding: String,
    633     digest_subjects: DigestSubjects,
    634     service_identifier: String,
    635     contract_version_rule: String,
    636     negative_error_codes: Vec<String>,
    637     canonical_vectors: CanonicalVectors,
    638     operations: OperationsDecision,
    639     deferred_operations: Vec<String>,
    640 }
    641 
    642 #[derive(Debug, Deserialize)]
    643 #[serde(deny_unknown_fields)]
    644 struct PredecessorDecision {
    645     schema: String,
    646     filename: String,
    647     transition: String,
    648 }
    649 
    650 #[derive(Debug, Deserialize)]
    651 #[serde(deny_unknown_fields)]
    652 struct OperationsDecision {
    653     command: String,
    654     modes: Vec<String>,
    655     required_arguments: Vec<String>,
    656     service_metadata_path: String,
    657     service_metadata_fields: Vec<String>,
    658     lib_dependency_inventory: String,
    659     source_cleanliness: String,
    660     predecessor_lock_presence: String,
    661     service_revision_stability: String,
    662     active_revision_agreement: Vec<String>,
    663     nix_material_states: Vec<String>,
    664     deferred_nix_agreement: Vec<String>,
    665     maximum_source_archive_bytes: u64,
    666 }
    667 
    668 #[derive(Debug, Deserialize)]
    669 #[serde(deny_unknown_fields)]
    670 struct DigestSubjects {
    671     workspace_catalog_sha256: String,
    672     source_archive_sha256: String,
    673     cargo_lock_sha256: String,
    674     flake_lock_sha256: String,
    675 }
    676 
    677 #[derive(Debug, Deserialize)]
    678 #[serde(deny_unknown_fields)]
    679 struct FixedDecision {
    680     repository: String,
    681     architecture: String,
    682     version: String,
    683     rust_version: String,
    684     host_feature_profile: String,
    685 }
    686 
    687 #[derive(Debug, Deserialize)]
    688 #[serde(deny_unknown_fields)]
    689 struct CanonicalVector {
    690     toml: String,
    691     sha256: String,
    692 }
    693 
    694 #[derive(Debug, Deserialize)]
    695 #[serde(deny_unknown_fields)]
    696 struct CanonicalVectors {
    697     deferred: CanonicalVector,
    698     absent: CanonicalVector,
    699 }
    700 
    701 pub(crate) fn validate_contract(workspace_root: &Path) -> Result<(), String> {
    702     validate_contract_inner(workspace_root).map_err(|error| error.to_string())
    703 }
    704 
    705 fn validate_contract_inner(workspace_root: &Path) -> Result<(), ServiceSourceLockError> {
    706     let path = workspace_root.join(CONTRACT_RELATIVE);
    707     let metadata = fs::symlink_metadata(&path).map_err(|_| ServiceSourceLockError::Malformed)?;
    708     if !metadata.is_file() || metadata.len() > MAX_CONTRACT_BYTES as u64 {
    709         return Err(ServiceSourceLockError::Malformed);
    710     }
    711     let file = fs::File::open(path).map_err(|_| ServiceSourceLockError::Malformed)?;
    712     let mut bytes = Vec::with_capacity(metadata.len() as usize);
    713     file.take(MAX_CONTRACT_BYTES as u64 + 1)
    714         .read_to_end(&mut bytes)
    715         .map_err(|_| ServiceSourceLockError::Malformed)?;
    716     if bytes.len() > MAX_CONTRACT_BYTES {
    717         return Err(ServiceSourceLockError::Malformed);
    718     }
    719     let decision = serde_json::from_slice::<SourceLockDecision>(&bytes)
    720         .map_err(|_| ServiceSourceLockError::Malformed)?;
    721     validate_decision(&decision)?;
    722 
    723     for (vector, expected) in [
    724         (
    725             &decision.canonical_vectors.deferred,
    726             canonical_deferred_vector(),
    727         ),
    728         (
    729             &decision.canonical_vectors.absent,
    730             canonical_absent_vector(),
    731         ),
    732     ] {
    733         let parsed = ServiceSourceLockV2::from_canonical_bytes(vector.toml.as_bytes())?;
    734         if parsed != expected
    735             || hex::encode(Sha256::digest(parsed.canonical_bytes())) != vector.sha256
    736         {
    737             return Err(ServiceSourceLockError::Noncanonical);
    738         }
    739     }
    740     Ok(())
    741 }
    742 
    743 fn validate_decision(decision: &SourceLockDecision) -> Result<(), ServiceSourceLockError> {
    744     let error_codes = [
    745         ServiceSourceLockError::InvalidContractVersion,
    746         ServiceSourceLockError::InvalidDigest,
    747         ServiceSourceLockError::InvalidFeatureProfile,
    748         ServiceSourceLockError::InvalidFixedIdentity,
    749         ServiceSourceLockError::InvalidNixMaterial,
    750         ServiceSourceLockError::InvalidRevision,
    751         ServiceSourceLockError::InvalidService,
    752         ServiceSourceLockError::InvalidToolchain,
    753         ServiceSourceLockError::Malformed,
    754         ServiceSourceLockError::Noncanonical,
    755         ServiceSourceLockError::TooLarge,
    756     ]
    757     .map(ServiceSourceLockError::code);
    758     let exact = decision.schema == "radroots.services-hardening.source-lock-decisions.v2"
    759         && decision.contract_version == 2
    760         && decision.decision_state == "active"
    761         && decision.predecessor.schema == "radroots.service.source-lock.v1"
    762         && decision.predecessor.filename == PREDECESSOR_LOCK_FILENAME
    763         && decision.predecessor.transition == "forward_only_replace"
    764         && decision.lock_filename == LOCK_FILENAME
    765         && decision.lock_schema == LOCK_SCHEMA
    766         && decision.canonical_encoding == "compact_canonical_toml_with_final_newline"
    767         && decision.maximum_lock_utf8_bytes == MAX_LOCK_BYTES
    768         && decision.maximum_service_utf8_bytes == MAX_SERVICE_BYTES
    769         && decision.canonical_field_order_deferred == DEFERRED_FIELD_ORDER
    770         && decision.canonical_field_order_absent == ABSENT_FIELD_ORDER
    771         && decision.fixed.repository == LIB_REPOSITORY
    772         && decision.fixed.architecture == ARCHITECTURE
    773         && decision.fixed.version == LIB_VERSION
    774         && decision.fixed.rust_version == RUST_VERSION
    775         && decision.fixed.host_feature_profile == HOST_FEATURE_PROFILE
    776         && decision.revision_encoding == "git_oid_lowercase_hex_40"
    777         && decision.digest_encoding == "sha256_lowercase_hex_64"
    778         && decision.digest_subjects.workspace_catalog_sha256
    779             == "lib/contracts/crates/catalog.v2.toml"
    780         && decision.digest_subjects.source_archive_sha256
    781             == "canonical_lib_revision_source_archive"
    782         && decision.digest_subjects.cargo_lock_sha256 == "service/Cargo.lock"
    783         && decision.digest_subjects.flake_lock_sha256 == "service/flake.lock"
    784         && decision.service_identifier
    785             == "ascii_lower_snake_case_starting_with_letter_ending_with_letter_or_digit_no_empty_segments_1_to_128_bytes"
    786         && decision.contract_version_rule == "u32_nonzero"
    787         && error_codes == ERROR_CODES
    788         && decision.negative_error_codes == error_codes
    789         && decision.operations.command == "cargo xtask service-source-lock"
    790         && decision.operations.modes == ["check", "write"]
    791         && decision.operations.required_arguments == ["mode", "service_root", "source_archive"]
    792         && decision.operations.service_metadata_path
    793             == "Cargo.toml.workspace.metadata.radroots.service_source_lock"
    794         && decision.operations.service_metadata_fields
    795             == [
    796                 "service",
    797                 "host_feature_profile",
    798                 "nix_material",
    799                 "config_contract_version",
    800                 "state_contract_version",
    801                 "admin_contract_version",
    802                 "status_contract_version",
    803                 "provider_contract_version",
    804             ]
    805         && decision.operations.lib_dependency_inventory
    806             == "verified_source_archive_workspace_catalog"
    807         && decision.operations.source_cleanliness
    808             == "all_changes_forbidden_except_exact_generated_lock_path"
    809         && decision.operations.predecessor_lock_presence == "forbidden"
    810         && decision.operations.service_revision_stability
    811             == "same_head_before_and_after_evidence_and_output"
    812         && decision.operations.active_revision_agreement
    813             == [
    814                 "cargo_manifests",
    815                 "cargo_lock",
    816                 "source_archive",
    817                 "canonical_public_remote",
    818             ]
    819         && decision.operations.nix_material_states == ["absent", "deferred"]
    820         && decision.operations.deferred_nix_agreement
    821             == [
    822                 "flake_expression",
    823                 "flake_lock",
    824                 "source_lock_nix_revision",
    825                 "canonical_public_remote",
    826             ]
    827         && decision.operations.maximum_source_archive_bytes == 1_073_741_824
    828         && decision.deferred_operations
    829             == [
    830                 "embedded_build_information_agreement",
    831                 "nix_qualification",
    832                 "nix_active_revision_alignment",
    833             ];
    834     if exact {
    835         Ok(())
    836     } else {
    837         Err(ServiceSourceLockError::InvalidFixedIdentity)
    838     }
    839 }
    840 
    841 fn canonical_deferred_vector() -> ServiceSourceLockV2 {
    842     ServiceSourceLockV2::new(ServiceSourceLockParts {
    843         service: "fixture_service",
    844         revision: "2222222222222222222222222222222222222222",
    845         workspace_catalog_sha256: "2222222222222222222222222222222222222222222222222222222222222222",
    846         source_archive_sha256: "3333333333333333333333333333333333333333333333333333333333333333",
    847         cargo_lock_sha256: "3f32f227550b26ffccf6ee73ceab7471b3d8ce40b3e7c345d2ed65af7e9affa0",
    848         nix: NixMaterialParts::Deferred {
    849             lib_revision: "1111111111111111111111111111111111111111",
    850             flake_lock_sha256: "13638c254efcc7ccc5798242d2c095934e84fbc406a9af244fc754b18a6f9353",
    851         },
    852         contract_versions: ContractVersions::new(1, 2, 3, 4, 5),
    853     })
    854     .expect("the governed source-lock vector is valid")
    855 }
    856 
    857 fn canonical_absent_vector() -> ServiceSourceLockV2 {
    858     ServiceSourceLockV2::new(ServiceSourceLockParts {
    859         service: "fixture_service",
    860         revision: "2222222222222222222222222222222222222222",
    861         workspace_catalog_sha256: "2222222222222222222222222222222222222222222222222222222222222222",
    862         source_archive_sha256: "3333333333333333333333333333333333333333333333333333333333333333",
    863         cargo_lock_sha256: "3f32f227550b26ffccf6ee73ceab7471b3d8ce40b3e7c345d2ed65af7e9affa0",
    864         nix: NixMaterialParts::Absent,
    865         contract_versions: ContractVersions::new(1, 2, 3, 4, 5),
    866     })
    867     .expect("the governed absent-Nix source-lock vector is valid")
    868 }
    869 
    870 fn validate_raw(raw: &RawServiceSourceLock) -> Result<(), ServiceSourceLockError> {
    871     if raw.schema != LOCK_SCHEMA
    872         || raw.contract_version != 2
    873         || raw.repository != LIB_REPOSITORY
    874         || raw.architecture != ARCHITECTURE
    875         || raw.version != LIB_VERSION
    876     {
    877         return Err(ServiceSourceLockError::InvalidFixedIdentity);
    878     }
    879     if raw.rust_version != RUST_VERSION {
    880         return Err(ServiceSourceLockError::InvalidToolchain);
    881     }
    882     if raw.host_feature_profile != HOST_FEATURE_PROFILE {
    883         return Err(ServiceSourceLockError::InvalidFeatureProfile);
    884     }
    885     validate_parts(&ServiceSourceLockParts {
    886         service: &raw.service,
    887         revision: &raw.revision,
    888         workspace_catalog_sha256: &raw.workspace_catalog_sha256,
    889         source_archive_sha256: &raw.source_archive_sha256,
    890         cargo_lock_sha256: &raw.cargo_lock_sha256,
    891         nix: match &raw.nix {
    892             RawNixMaterial::Absent => NixMaterialParts::Absent,
    893             RawNixMaterial::Deferred {
    894                 lib_revision,
    895                 flake_lock_sha256,
    896             } => NixMaterialParts::Deferred {
    897                 lib_revision,
    898                 flake_lock_sha256,
    899             },
    900         },
    901         contract_versions: raw.contract_versions,
    902     })
    903 }
    904 
    905 fn validate_parts(parts: &ServiceSourceLockParts<'_>) -> Result<(), ServiceSourceLockError> {
    906     if !valid_service(parts.service) {
    907         return Err(ServiceSourceLockError::InvalidService);
    908     }
    909     if !valid_lower_hex(parts.revision, 40) {
    910         return Err(ServiceSourceLockError::InvalidRevision);
    911     }
    912     if ![
    913         parts.workspace_catalog_sha256,
    914         parts.source_archive_sha256,
    915         parts.cargo_lock_sha256,
    916     ]
    917     .into_iter()
    918     .all(|value| valid_lower_hex(value, 64))
    919     {
    920         return Err(ServiceSourceLockError::InvalidDigest);
    921     }
    922     if let NixMaterialParts::Deferred {
    923         lib_revision,
    924         flake_lock_sha256,
    925     } = parts.nix
    926         && (!valid_lower_hex(lib_revision, 40) || !valid_lower_hex(flake_lock_sha256, 64))
    927     {
    928         return Err(ServiceSourceLockError::InvalidNixMaterial);
    929     }
    930     if !parts.contract_versions.is_valid() {
    931         return Err(ServiceSourceLockError::InvalidContractVersion);
    932     }
    933     Ok(())
    934 }
    935 
    936 fn valid_service(value: &str) -> bool {
    937     let bytes = value.as_bytes();
    938     !bytes.is_empty()
    939         && bytes.len() <= MAX_SERVICE_BYTES
    940         && bytes[0].is_ascii_lowercase()
    941         && (bytes[bytes.len() - 1].is_ascii_lowercase() || bytes[bytes.len() - 1].is_ascii_digit())
    942         && bytes
    943             .iter()
    944             .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'_')
    945         && !bytes.windows(2).any(|pair| pair == b"__")
    946 }
    947 
    948 fn valid_lower_hex(value: &str, length: usize) -> bool {
    949     value.len() == length
    950         && value
    951             .bytes()
    952             .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
    953 }
    954 
    955 fn render(raw: &RawServiceSourceLock) -> String {
    956     let mut output = String::with_capacity(1024);
    957     writeln!(output, "schema = \"{}\"", raw.schema).expect("render to String");
    958     writeln!(output, "contract_version = {}", raw.contract_version).expect("render to String");
    959     writeln!(output, "service = \"{}\"", raw.service).expect("render to String");
    960     writeln!(output, "repository = \"{}\"", raw.repository).expect("render to String");
    961     writeln!(output, "revision = \"{}\"", raw.revision).expect("render to String");
    962     writeln!(output, "architecture = \"{}\"", raw.architecture).expect("render to String");
    963     writeln!(
    964         output,
    965         "workspace_catalog_sha256 = \"{}\"",
    966         raw.workspace_catalog_sha256
    967     )
    968     .expect("render to String");
    969     writeln!(output, "version = \"{}\"", raw.version).expect("render to String");
    970     writeln!(
    971         output,
    972         "source_archive_sha256 = \"{}\"",
    973         raw.source_archive_sha256
    974     )
    975     .expect("render to String");
    976     writeln!(output, "cargo_lock_sha256 = \"{}\"", raw.cargo_lock_sha256)
    977         .expect("render to String");
    978     writeln!(output, "rust_version = \"{}\"", raw.rust_version).expect("render to String");
    979     writeln!(
    980         output,
    981         "host_feature_profile = \"{}\"",
    982         raw.host_feature_profile
    983     )
    984     .expect("render to String");
    985     output.push_str("\n[nix]\n");
    986     match &raw.nix {
    987         RawNixMaterial::Absent => {
    988             output.push_str("material = \"absent\"\n");
    989         }
    990         RawNixMaterial::Deferred {
    991             lib_revision,
    992             flake_lock_sha256,
    993         } => {
    994             output.push_str("material = \"deferred\"\n");
    995             writeln!(output, "lib_revision = \"{lib_revision}\"").expect("render to String");
    996             writeln!(output, "flake_lock_sha256 = \"{flake_lock_sha256}\"")
    997                 .expect("render to String");
    998         }
    999     }
   1000     output.push_str("\n[contract_versions]\n");
   1001     writeln!(output, "config = {}", raw.contract_versions.config).expect("render to String");
   1002     writeln!(output, "state = {}", raw.contract_versions.state).expect("render to String");
   1003     writeln!(output, "admin = {}", raw.contract_versions.admin).expect("render to String");
   1004     writeln!(output, "status = {}", raw.contract_versions.status).expect("render to String");
   1005     writeln!(output, "provider = {}", raw.contract_versions.provider).expect("render to String");
   1006     output
   1007 }
   1008 
   1009 #[cfg(test)]
   1010 mod tests {
   1011     use std::error::Error as _;
   1012 
   1013     use crate::workspace_root;
   1014 
   1015     use super::*;
   1016 
   1017     #[test]
   1018     fn canonical_vectors_round_trip_with_exact_digests() {
   1019         let lock = canonical_deferred_vector();
   1020         assert_eq!(
   1021             hex::encode(Sha256::digest(lock.canonical_bytes())),
   1022             "da7b8894a6480e7022d9937369a5c9bafbf90128a901652923e501c52aced1a2"
   1023         );
   1024         assert_eq!(
   1025             ServiceSourceLockV2::from_canonical_bytes(lock.canonical_bytes()),
   1026             Ok(lock)
   1027         );
   1028         let lock = canonical_absent_vector();
   1029         assert_eq!(
   1030             hex::encode(Sha256::digest(lock.canonical_bytes())),
   1031             "2af058ba042509c77efd6dc264c60a7abf4371378223e52e34eb441cab7e263c"
   1032         );
   1033         assert_eq!(
   1034             ServiceSourceLockV2::from_canonical_bytes(lock.canonical_bytes()),
   1035             Ok(lock)
   1036         );
   1037     }
   1038 
   1039     #[test]
   1040     fn deferred_material_parser_accepts_both_exact_layouts_and_rejects_ambiguous_selection() {
   1041         let revision = "1".repeat(40);
   1042         let mut original = serde_json::json!({"version":7,"root":"root","nodes":{
   1043             "root":{"inputs":{"lib":"lib"}},
   1044             "lib":{
   1045                 "locked":{"lastModified":1,"narHash":format!("sha256-{}=", "A".repeat(43)),"owner":"radrootslabs","repo":"lib","rev":revision,"type":"github"},
   1046                 "original":{"owner":"radrootslabs","repo":"lib","rev":revision,"type":"github"}
   1047             }
   1048         }});
   1049         let bytes = serde_json::to_vec(&original).unwrap();
   1050         assert_eq!(validate_deferred_nix_lock(&bytes).unwrap(), revision);
   1051         let direct = format!(
   1052             "inputs.lib = {{\nurl = \"github:radrootslabs/lib/{revision}\";\nflake = false;\n}};\n"
   1053         );
   1054         let nested = format!(
   1055             "inputs = {{\nlib = {{\nurl = \"github:radrootslabs/lib/{revision}\";\nflake = false;\n}};\n}};\n"
   1056         );
   1057         for expression in [&direct, &nested] {
   1058             assert_eq!(
   1059                 validate_deferred_nix_material(expression.as_bytes(), &bytes)
   1060                     .unwrap()
   1061                     .lib_revision(),
   1062                 revision
   1063             );
   1064             for changed in [
   1065                 expression.replace("flake = false;", "flake = true;"),
   1066                 expression.replace(&revision, &"2".repeat(40)),
   1067                 format!("{expression}{direct}"),
   1068             ] {
   1069                 assert_eq!(
   1070                     validate_deferred_nix_material(changed.as_bytes(), &bytes),
   1071                     Err(ServiceSourceLockError::InvalidNixMaterial)
   1072                 );
   1073             }
   1074         }
   1075         assert!(validate_deferred_nix_material(&[255], &bytes).is_err());
   1076         for (pointer, value) in [
   1077             ("/root", serde_json::json!("")),
   1078             ("/root", serde_json::json!(null)),
   1079             ("/nodes", serde_json::json!([])),
   1080             ("/nodes/root/inputs/lib", serde_json::json!([])),
   1081             ("/nodes/lib/locked/owner", serde_json::json!("elsewhere")),
   1082             ("/nodes/lib/original/owner", serde_json::json!("elsewhere")),
   1083         ] {
   1084             let mut changed = original.clone();
   1085             *changed.pointer_mut(pointer).unwrap() = value;
   1086             assert!(
   1087                 validate_deferred_nix_lock(&serde_json::to_vec(&changed).unwrap()).is_err(),
   1088                 "{pointer}"
   1089             );
   1090         }
   1091         original["nodes"]["unrelated"] =
   1092             serde_json::json!({"values":[null,true,1,-1,"text",{},[]]});
   1093         assert_eq!(
   1094             validate_deferred_nix_lock(&serde_json::to_vec(&original).unwrap()).unwrap(),
   1095             revision
   1096         );
   1097         original["unknown"] = serde_json::json!(true);
   1098         assert!(validate_deferred_nix_lock(&serde_json::to_vec(&original).unwrap()).is_err());
   1099         for digest in [
   1100             "invalid".to_owned(),
   1101             format!("sha256-{}x", "A".repeat(43)),
   1102             format!("sha256-{}?=", "A".repeat(42)),
   1103         ] {
   1104             assert!(!valid_nix_sha256(&digest));
   1105         }
   1106         for service in ["a_", "a-b", "a.B"] {
   1107             assert!(!valid_service(service));
   1108         }
   1109     }
   1110 
   1111     #[test]
   1112     fn source_lock_contract_input_must_be_a_bounded_regular_file() {
   1113         let root = tempfile::TempDir::new().unwrap();
   1114         let contract = root.path().join(CONTRACT_RELATIVE);
   1115         fs::create_dir_all(contract.parent().unwrap()).unwrap();
   1116         fs::create_dir(&contract).unwrap();
   1117         assert!(validate_contract_inner(root.path()).is_err());
   1118         fs::remove_dir(&contract).unwrap();
   1119         fs::write(&contract, vec![b' '; MAX_CONTRACT_BYTES + 1]).unwrap();
   1120         assert!(validate_contract_inner(root.path()).is_err());
   1121     }
   1122 
   1123     #[test]
   1124     fn decision_rejects_every_independent_governed_field_drift() {
   1125         let bytes = fs::read(workspace_root().join(CONTRACT_RELATIVE)).expect("decision");
   1126         let canonical = serde_json::from_slice::<serde_json::Value>(&bytes).expect("decision json");
   1127         for (pointer, replacement) in [
   1128             ("/schema", serde_json::json!("other")),
   1129             ("/contract_version", serde_json::json!(1)),
   1130             ("/decision_state", serde_json::json!("draft")),
   1131             ("/predecessor/schema", serde_json::json!("other")),
   1132             ("/predecessor/filename", serde_json::json!("other")),
   1133             ("/predecessor/transition", serde_json::json!("other")),
   1134             ("/lock_filename", serde_json::json!("other")),
   1135             ("/lock_schema", serde_json::json!("other")),
   1136             ("/canonical_encoding", serde_json::json!("other")),
   1137             ("/maximum_lock_utf8_bytes", serde_json::json!(1)),
   1138             ("/maximum_service_utf8_bytes", serde_json::json!(1)),
   1139             ("/canonical_field_order_deferred", serde_json::json!([])),
   1140             ("/canonical_field_order_absent", serde_json::json!([])),
   1141             ("/fixed/repository", serde_json::json!("other")),
   1142             ("/fixed/architecture", serde_json::json!("other")),
   1143             ("/fixed/version", serde_json::json!("other")),
   1144             ("/fixed/rust_version", serde_json::json!("other")),
   1145             ("/fixed/host_feature_profile", serde_json::json!("other")),
   1146             ("/revision_encoding", serde_json::json!("other")),
   1147             ("/digest_encoding", serde_json::json!("other")),
   1148             (
   1149                 "/digest_subjects/workspace_catalog_sha256",
   1150                 serde_json::json!("other"),
   1151             ),
   1152             (
   1153                 "/digest_subjects/source_archive_sha256",
   1154                 serde_json::json!("other"),
   1155             ),
   1156             (
   1157                 "/digest_subjects/cargo_lock_sha256",
   1158                 serde_json::json!("other"),
   1159             ),
   1160             (
   1161                 "/digest_subjects/flake_lock_sha256",
   1162                 serde_json::json!("other"),
   1163             ),
   1164             ("/service_identifier", serde_json::json!("other")),
   1165             ("/contract_version_rule", serde_json::json!("other")),
   1166             ("/negative_error_codes", serde_json::json!([])),
   1167             ("/operations/command", serde_json::json!("other")),
   1168             ("/operations/modes", serde_json::json!([])),
   1169             ("/operations/required_arguments", serde_json::json!([])),
   1170             (
   1171                 "/operations/service_metadata_path",
   1172                 serde_json::json!("other"),
   1173             ),
   1174             ("/operations/service_metadata_fields", serde_json::json!([])),
   1175             (
   1176                 "/operations/lib_dependency_inventory",
   1177                 serde_json::json!("other"),
   1178             ),
   1179             ("/operations/source_cleanliness", serde_json::json!("other")),
   1180             (
   1181                 "/operations/predecessor_lock_presence",
   1182                 serde_json::json!("other"),
   1183             ),
   1184             (
   1185                 "/operations/service_revision_stability",
   1186                 serde_json::json!("other"),
   1187             ),
   1188             (
   1189                 "/operations/active_revision_agreement",
   1190                 serde_json::json!([]),
   1191             ),
   1192             ("/operations/nix_material_states", serde_json::json!([])),
   1193             ("/operations/deferred_nix_agreement", serde_json::json!([])),
   1194             (
   1195                 "/operations/maximum_source_archive_bytes",
   1196                 serde_json::json!(1),
   1197             ),
   1198             ("/deferred_operations", serde_json::json!(["future"])),
   1199         ] {
   1200             let mut drifted = canonical.clone();
   1201             *drifted.pointer_mut(pointer).expect("governed field") = replacement;
   1202             let decision = serde_json::from_value::<SourceLockDecision>(drifted)
   1203                 .expect("structurally valid drift");
   1204             assert_eq!(
   1205                 validate_decision(&decision),
   1206                 Err(ServiceSourceLockError::InvalidFixedIdentity),
   1207                 "accepted drift at {pointer}"
   1208             );
   1209         }
   1210     }
   1211 
   1212     #[test]
   1213     fn parser_rejects_noncanonical_and_ambiguous_toml() {
   1214         let canonical = String::from_utf8(canonical_deferred_vector().canonical_bytes().to_vec())
   1215             .expect("canonical UTF-8");
   1216         for malformed in [
   1217             canonical.replacen("schema =", "unknown = 1\nschema =", 1),
   1218             canonical.replacen(
   1219                 "contract_version = 2\nservice = \"fixture_service\"",
   1220                 "service = \"fixture_service\"\ncontract_version = 2",
   1221                 1,
   1222             ),
   1223             format!(" {canonical}"),
   1224             canonical.replacen("schema =", "schema=", 1),
   1225             canonical.replacen("contract_version = 2", "contract_version = 02", 1),
   1226         ] {
   1227             assert!(matches!(
   1228                 ServiceSourceLockV2::from_canonical_bytes(malformed.as_bytes()),
   1229                 Err(ServiceSourceLockError::Malformed | ServiceSourceLockError::Noncanonical)
   1230             ));
   1231         }
   1232         let duplicate = canonical.replacen(
   1233             "service = \"fixture_service\"",
   1234             "service = \"fixture_service\"\nservice = \"fixture_service\"",
   1235             1,
   1236         );
   1237         assert_eq!(
   1238             ServiceSourceLockV2::from_canonical_bytes(duplicate.as_bytes()),
   1239             Err(ServiceSourceLockError::Malformed)
   1240         );
   1241     }
   1242 
   1243     #[test]
   1244     fn nix_material_variants_are_closed_and_independently_bounded() {
   1245         let absent = canonical_absent_vector();
   1246         assert_eq!(absent.nix_material_state(), NixMaterialState::Absent);
   1247         assert_eq!(absent.nix_lib_revision(), None);
   1248         assert_eq!(absent.flake_lock_sha256(), None);
   1249 
   1250         let deferred = canonical_deferred_vector();
   1251         assert_eq!(deferred.nix_material_state(), NixMaterialState::Deferred);
   1252         assert_eq!(
   1253             deferred.nix_lib_revision(),
   1254             Some("1111111111111111111111111111111111111111")
   1255         );
   1256         assert_eq!(
   1257             deferred.flake_lock_sha256(),
   1258             Some("13638c254efcc7ccc5798242d2c095934e84fbc406a9af244fc754b18a6f9353")
   1259         );
   1260 
   1261         let absent_text = String::from_utf8(absent.canonical_bytes().to_vec()).expect("UTF-8");
   1262         let deferred_text = String::from_utf8(deferred.canonical_bytes().to_vec()).expect("UTF-8");
   1263         for malformed in [
   1264             absent_text.replacen(
   1265                 "material = \"absent\"",
   1266                 "material = \"absent\"\nlib_revision = \"1111111111111111111111111111111111111111\"",
   1267                 1,
   1268             ),
   1269             absent_text.replacen("material = \"absent\"", "material = \"other\"", 1),
   1270             deferred_text.replacen(
   1271                 "lib_revision = \"1111111111111111111111111111111111111111\"\n",
   1272                 "",
   1273                 1,
   1274             ),
   1275             deferred_text.replacen(
   1276                 "flake_lock_sha256 = \"13638c254efcc7ccc5798242d2c095934e84fbc406a9af244fc754b18a6f9353\"\n",
   1277                 "",
   1278                 1,
   1279             ),
   1280         ] {
   1281             assert!(matches!(
   1282                 ServiceSourceLockV2::from_canonical_bytes(malformed.as_bytes()),
   1283                 Err(ServiceSourceLockError::Malformed | ServiceSourceLockError::Noncanonical)
   1284             ));
   1285         }
   1286 
   1287         assert_eq!(
   1288             validate_deferred_nix_material(&vec![b'x'; MAX_FLAKE_NIX_BYTES + 1], b"{}"),
   1289             Err(ServiceSourceLockError::InvalidNixMaterial)
   1290         );
   1291         assert_eq!(
   1292             validate_deferred_nix_material(b"", &vec![b'x'; MAX_FLAKE_LOCK_BYTES + 1]),
   1293             Err(ServiceSourceLockError::InvalidNixMaterial)
   1294         );
   1295     }
   1296 
   1297     #[test]
   1298     fn parser_rejects_every_identity_and_bound_drift() {
   1299         let canonical = String::from_utf8(canonical_deferred_vector().canonical_bytes().to_vec())
   1300             .expect("canonical UTF-8");
   1301         let cases = [
   1302             (
   1303                 "radroots.service.source-lock.v2",
   1304                 "wrong",
   1305                 ServiceSourceLockError::InvalidFixedIdentity,
   1306             ),
   1307             (
   1308                 "contract_version = 2",
   1309                 "contract_version = 1",
   1310                 ServiceSourceLockError::InvalidFixedIdentity,
   1311             ),
   1312             (
   1313                 LIB_REPOSITORY,
   1314                 "https://example.invalid/lib",
   1315                 ServiceSourceLockError::InvalidFixedIdentity,
   1316             ),
   1317             (
   1318                 ARCHITECTURE,
   1319                 "radroots.crates.release.v1",
   1320                 ServiceSourceLockError::InvalidFixedIdentity,
   1321             ),
   1322             (
   1323                 LIB_VERSION,
   1324                 "0.1.1",
   1325                 ServiceSourceLockError::InvalidFixedIdentity,
   1326             ),
   1327             (
   1328                 "fixture_service",
   1329                 "Fixture",
   1330                 ServiceSourceLockError::InvalidService,
   1331             ),
   1332             (
   1333                 "service = \"fixture_service\"",
   1334                 "service = \"\"",
   1335                 ServiceSourceLockError::InvalidService,
   1336             ),
   1337             (
   1338                 "2222222222222222222222222222222222222222",
   1339                 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
   1340                 ServiceSourceLockError::InvalidRevision,
   1341             ),
   1342             (
   1343                 "2222222222222222222222222222222222222222",
   1344                 "222222222222222222222222222222222222222",
   1345                 ServiceSourceLockError::InvalidRevision,
   1346             ),
   1347             (
   1348                 "2222222222222222222222222222222222222222222222222222222222222222",
   1349                 "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG",
   1350                 ServiceSourceLockError::InvalidDigest,
   1351             ),
   1352             (
   1353                 "3333333333333333333333333333333333333333333333333333333333333333",
   1354                 "333333333333333333333333333333333333333333333333333333333333333",
   1355                 ServiceSourceLockError::InvalidDigest,
   1356             ),
   1357             (
   1358                 "3f32f227550b26ffccf6ee73ceab7471b3d8ce40b3e7c345d2ed65af7e9affa0",
   1359                 "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
   1360                 ServiceSourceLockError::InvalidDigest,
   1361             ),
   1362             (
   1363                 "13638c254efcc7ccc5798242d2c095934e84fbc406a9af244fc754b18a6f9353",
   1364                 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
   1365                 ServiceSourceLockError::InvalidNixMaterial,
   1366             ),
   1367             (
   1368                 "1111111111111111111111111111111111111111",
   1369                 "invalid",
   1370                 ServiceSourceLockError::InvalidNixMaterial,
   1371             ),
   1372             (
   1373                 RUST_VERSION,
   1374                 "1.97.0",
   1375                 ServiceSourceLockError::InvalidToolchain,
   1376             ),
   1377             (
   1378                 HOST_FEATURE_PROFILE,
   1379                 "default",
   1380                 ServiceSourceLockError::InvalidFeatureProfile,
   1381             ),
   1382             (
   1383                 "config = 1",
   1384                 "config = 0",
   1385                 ServiceSourceLockError::InvalidContractVersion,
   1386             ),
   1387             (
   1388                 "state = 2",
   1389                 "state = 0",
   1390                 ServiceSourceLockError::InvalidContractVersion,
   1391             ),
   1392             (
   1393                 "admin = 3",
   1394                 "admin = 0",
   1395                 ServiceSourceLockError::InvalidContractVersion,
   1396             ),
   1397             (
   1398                 "status = 4",
   1399                 "status = 0",
   1400                 ServiceSourceLockError::InvalidContractVersion,
   1401             ),
   1402             (
   1403                 "provider = 5",
   1404                 "provider = 0",
   1405                 ServiceSourceLockError::InvalidContractVersion,
   1406             ),
   1407         ];
   1408         for (from, to, expected) in cases {
   1409             let mutated = canonical.replacen(from, to, 1);
   1410             assert_eq!(
   1411                 ServiceSourceLockV2::from_canonical_bytes(mutated.as_bytes()),
   1412                 Err(expected)
   1413             );
   1414         }
   1415 
   1416         let too_large = vec![b' '; MAX_LOCK_BYTES + 1];
   1417         assert_eq!(
   1418             ServiceSourceLockV2::from_canonical_bytes(&too_large),
   1419             Err(ServiceSourceLockError::TooLarge)
   1420         );
   1421     }
   1422 
   1423     #[test]
   1424     fn exact_service_and_contract_version_maxima_are_admitted() {
   1425         let service = "a".repeat(MAX_SERVICE_BYTES);
   1426         let maximum = ServiceSourceLockV2::new(ServiceSourceLockParts {
   1427             service: &service,
   1428             revision: &"a".repeat(40),
   1429             workspace_catalog_sha256: &"b".repeat(64),
   1430             source_archive_sha256: &"c".repeat(64),
   1431             cargo_lock_sha256: &"d".repeat(64),
   1432             nix: NixMaterialParts::Deferred {
   1433                 lib_revision: &"f".repeat(40),
   1434                 flake_lock_sha256: &"e".repeat(64),
   1435             },
   1436             contract_versions: ContractVersions::new(
   1437                 u32::MAX,
   1438                 u32::MAX,
   1439                 u32::MAX,
   1440                 u32::MAX,
   1441                 u32::MAX,
   1442             ),
   1443         })
   1444         .expect("maximum lock");
   1445         assert!(maximum.canonical_bytes().len() <= MAX_LOCK_BYTES);
   1446 
   1447         let overlong_service = "a".repeat(MAX_SERVICE_BYTES + 1);
   1448         assert_eq!(
   1449             ServiceSourceLockV2::new(ServiceSourceLockParts {
   1450                 service: &overlong_service,
   1451                 revision: &"a".repeat(40),
   1452                 workspace_catalog_sha256: &"b".repeat(64),
   1453                 source_archive_sha256: &"c".repeat(64),
   1454                 cargo_lock_sha256: &"d".repeat(64),
   1455                 nix: NixMaterialParts::Absent,
   1456                 contract_versions: ContractVersions::new(1, 1, 1, 1, 1),
   1457             }),
   1458             Err(ServiceSourceLockError::InvalidService)
   1459         );
   1460 
   1461         for invalid in [
   1462             "1service",
   1463             "_service",
   1464             "service_",
   1465             "service__name",
   1466             "service-name",
   1467         ] {
   1468             assert_eq!(
   1469                 ServiceSourceLockV2::new(ServiceSourceLockParts {
   1470                     service: invalid,
   1471                     revision: &"a".repeat(40),
   1472                     workspace_catalog_sha256: &"b".repeat(64),
   1473                     source_archive_sha256: &"c".repeat(64),
   1474                     cargo_lock_sha256: &"d".repeat(64),
   1475                     nix: NixMaterialParts::Absent,
   1476                     contract_versions: ContractVersions::new(1, 1, 1, 1, 1),
   1477                 }),
   1478                 Err(ServiceSourceLockError::InvalidService)
   1479             );
   1480         }
   1481     }
   1482 
   1483     #[test]
   1484     fn diagnostics_are_fixed_and_source_free() {
   1485         let lock = canonical_deferred_vector();
   1486         let debug = format!("{lock:?}");
   1487         assert_eq!(debug, "ServiceSourceLockV2 { .. }");
   1488         for sensitive in [
   1489             "fixture_service",
   1490             "radrootslabs",
   1491             "1111111111111111111111111111111111111111",
   1492             "2222222222222222222222222222222222222222222222222222222222222222",
   1493         ] {
   1494             assert!(!debug.contains(sensitive));
   1495         }
   1496         let errors = [
   1497             ServiceSourceLockError::TooLarge,
   1498             ServiceSourceLockError::Malformed,
   1499             ServiceSourceLockError::Noncanonical,
   1500             ServiceSourceLockError::InvalidFixedIdentity,
   1501             ServiceSourceLockError::InvalidNixMaterial,
   1502             ServiceSourceLockError::InvalidService,
   1503             ServiceSourceLockError::InvalidRevision,
   1504             ServiceSourceLockError::InvalidDigest,
   1505             ServiceSourceLockError::InvalidToolchain,
   1506             ServiceSourceLockError::InvalidFeatureProfile,
   1507             ServiceSourceLockError::InvalidContractVersion,
   1508         ];
   1509         for error in errors {
   1510             assert!(error.source().is_none());
   1511             assert!(!error.to_string().contains("fixture_service"));
   1512         }
   1513         let mut codes = errors.map(ServiceSourceLockError::code);
   1514         codes.sort_unstable();
   1515         assert_eq!(codes, ERROR_CODES);
   1516     }
   1517 }