Skip to main content

arkhe_forge_platform/
manifest.rs

1//! Shell Manifest TOML loader.
2//!
3//! The manifest is the single source of ground-truth shell policy: which
4//! audit signature class to require, which compliance tier applies, what
5//! the runtime version bound is. This module ships the loader surface:
6//!
7//! 1. [`ManifestSnapshot`] — typed view of a parsed manifest.
8//! 2. [`ManifestLoader::load`] — parse + validate + BLAKE3 digest in one
9//!    step, returning the snapshot and a canonical digest suitable for the
10//!    `RuntimeBootstrap` chain anchor (E12).
11//! 3. [`emit_runtime_bootstrap`] — helper that turns a loaded snapshot into
12//!    an `Op::EmitEvent`-bound [`RuntimeBootstrap`] event on an
13//!    [`ActionContext`].
14//!
15//! The canonical digest is computed as
16//! `blake3::keyed_hash(derive_key("arkhe-forge-manifest-digest", &[]),
17//! toml_canonical_bytes)` — domain-separated.
18
19use arkhe_forge_core::context::{ActionContext, ActionError};
20use arkhe_forge_core::event::{RuntimeBootstrap, RuntimeSignatureClass, SemVer};
21use arkhe_kernel::abi::{Tick, TypeCode};
22use serde::{Deserialize, Serialize};
23
24/// 32-byte canonical digest of a [`ManifestSnapshot`] (anchor C5).
25pub type ManifestDigest = [u8; 32];
26
27/// BLAKE3 domain separator for canonical manifest digest.
28const DIGEST_DOMAIN: &str = "arkhe-forge-manifest-digest";
29
30// ===================== Sections =====================
31
32/// Shell identity + public presentation metadata.
33#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
34#[serde(deny_unknown_fields)]
35pub struct ShellSection {
36    /// Shell identifier — stable across releases (max 32 bytes UTF-8).
37    pub shell_id: String,
38    /// Human-readable shell name shown to end users.
39    pub display_name: String,
40}
41
42/// Runtime version bounds for this shell.
43#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45pub struct RuntimeSection {
46    /// Highest Runtime semver the shell promises compatibility with
47    /// (e.g. `"0.15"`). Must be ≥ `runtime_current`.
48    pub runtime_max: String,
49    /// Runtime semver the manifest author tested against (e.g. `"0.13"`).
50    pub runtime_current: String,
51}
52
53/// Audit / crypto stance — compliance tier classification.
54#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
55#[serde(deny_unknown_fields)]
56pub struct AuditSection {
57    /// PII AEAD family identifier — e.g. `"xchacha20-poly1305"`,
58    /// `"aes-256-gcm"`, `"aes-256-gcm-siv"`.
59    pub pii_cipher: String,
60    /// DEK backend identifier — `"software-kek"` (Tier-0 dev only),
61    /// `"hsm"`, `"aws-kms"`, `"gcp-kms"`, etc.
62    pub dek_backend: String,
63    /// KMS auto-promote policy — `"manual"` (operator approval) or
64    /// `"after_60min"` (auto-promote after 60 minutes of health-check
65    /// consensus).
66    pub kms_auto_promote: String,
67    /// Audit receipt signature class (the E13 axiom). Wire format
68    /// accepts `"ed25519"` / `"ml-dsa-65"` / `"hybrid"`. Forge L2
69    /// attestation emits `"ed25519"`.
70    pub signature_class: String,
71    /// Compliance tier — `0` (software KEK, dev), `1` (single KMS
72    /// free-tier), `2` (production Multi-KMS + threshold HSM).
73    pub compliance_tier: u8,
74}
75
76/// Frontend / TLS / credential policy. Defaults apply when the TOML omits
77/// the `[frontend]` table entirely or any sub-field.
78#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
79#[serde(default, deny_unknown_fields)]
80pub struct FrontendSection {
81    /// TLS is mandatory on the public ingress (default `true`).
82    pub tls_required: bool,
83    /// Alpha-tier credentials must rotate on promotion (default `true`).
84    pub alpha_credential_rotation_required: bool,
85}
86
87impl Default for FrontendSection {
88    fn default() -> Self {
89        Self {
90            tls_required: true,
91            alpha_credential_rotation_required: true,
92        }
93    }
94}
95
96/// Typed view of a loaded shell manifest.
97#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
98#[serde(deny_unknown_fields)]
99pub struct ManifestSnapshot {
100    /// Wire schema version.
101    pub schema_version: u16,
102    /// `[shell]` table.
103    pub shell: ShellSection,
104    /// `[runtime]` table.
105    pub runtime: RuntimeSection,
106    /// `[audit]` table.
107    pub audit: AuditSection,
108    /// `[frontend]` table (optional in TOML, filled via `Default`).
109    #[serde(default)]
110    pub frontend: FrontendSection,
111}
112
113// ===================== Errors =====================
114
115/// Manifest-loading failure taxonomy.
116#[derive(Debug, thiserror::Error)]
117#[non_exhaustive]
118pub enum ManifestError {
119    /// TOML parse or UTF-8 decode failure.
120    #[error("parse error: {0}")]
121    ParseError(String),
122
123    /// A required field was absent (reserved for future custom checks
124    /// beyond what `serde(deny_unknown_fields)` already enforces).
125    #[error("missing required field: {0}")]
126    MissingRequired(&'static str),
127
128    /// Compliance-tier / DEK-backend pair is inconsistent.
129    #[error("tier {tier} incompatible with backend '{backend}'")]
130    TierBackendMismatch {
131        /// Declared tier.
132        tier: u8,
133        /// Declared backend.
134        backend: String,
135    },
136
137    /// `software-kek` is allowed only on Tier-0 dev runs; production
138    /// runtimes (`runtime_current > 0.15`) reject it.
139    #[error("software-kek rejected: runtime_current {current} > 0.15")]
140    SoftwareKekProductionRefused {
141        /// Declared current runtime version.
142        current: String,
143    },
144
145    /// `runtime_max` lexicographically precedes `runtime_current` (illegal
146    /// because the shell would claim forward-compat to a version less than
147    /// the one it tested).
148    #[error("version mismatch: runtime_max < runtime_current")]
149    VersionMismatch,
150
151    /// Reserved — future custom deserializer returns this when extracting
152    /// the offending key from a `deny_unknown_fields` failure.
153    #[error("unknown field: {0}")]
154    UnknownField(String),
155
156    /// Field failed a validation predicate (e.g. malformed semver).
157    #[error("invalid value for {field}: {reason}")]
158    InvalidValue {
159        /// Field name whose value was rejected.
160        field: &'static str,
161        /// Human-readable reason.
162        reason: String,
163    },
164}
165
166// ===================== Loader =====================
167
168/// Zero-state manifest loader.
169pub struct ManifestLoader;
170
171impl ManifestLoader {
172    /// Parse + validate + digest a TOML-encoded manifest in one call.
173    pub fn load(toml_bytes: &[u8]) -> Result<(ManifestSnapshot, ManifestDigest), ManifestError> {
174        let text = core::str::from_utf8(toml_bytes)
175            .map_err(|e| ManifestError::ParseError(format!("utf-8: {}", e)))?;
176        let snapshot: ManifestSnapshot =
177            toml::from_str(text).map_err(|e| ManifestError::ParseError(e.to_string()))?;
178        Self::validate(&snapshot)?;
179        let digest = Self::canonical_digest(&snapshot)?;
180        Ok((snapshot, digest))
181    }
182
183    /// Run cross-field validators on a parsed snapshot. Called by
184    /// [`ManifestLoader::load`]; exposed for callers that construct
185    /// `ManifestSnapshot` directly.
186    pub fn validate(m: &ManifestSnapshot) -> Result<(), ManifestError> {
187        // shell_id length — stays within BoundedString<32> budget.
188        if m.shell.shell_id.len() > 32 {
189            return Err(ManifestError::InvalidValue {
190                field: "shell.shell_id",
191                reason: format!("length {} > 32", m.shell.shell_id.len()),
192            });
193        }
194
195        // Parseability of runtime versions.
196        let current =
197            parse_version(&m.runtime.runtime_current).ok_or(ManifestError::InvalidValue {
198                field: "runtime.runtime_current",
199                reason: format!(
200                    "not a 'M.N' or 'M.N.P' semver: {}",
201                    m.runtime.runtime_current
202                ),
203            })?;
204        let max = parse_version(&m.runtime.runtime_max).ok_or(ManifestError::InvalidValue {
205            field: "runtime.runtime_max",
206            reason: format!("not a 'M.N' or 'M.N.P' semver: {}", m.runtime.runtime_max),
207        })?;
208        if max < current {
209            return Err(ManifestError::VersionMismatch);
210        }
211
212        // Tier ↔ backend cross-check.
213        match (m.audit.compliance_tier, m.audit.dek_backend.as_str()) {
214            (0, "software-kek") => {}
215            (0, other) => {
216                return Err(ManifestError::TierBackendMismatch {
217                    tier: 0,
218                    backend: other.to_string(),
219                });
220            }
221            (1 | 2, "software-kek") => {
222                return Err(ManifestError::TierBackendMismatch {
223                    tier: m.audit.compliance_tier,
224                    backend: "software-kek".to_string(),
225                });
226            }
227            (1 | 2, _) => {}
228            _ => {
229                return Err(ManifestError::InvalidValue {
230                    field: "audit.compliance_tier",
231                    reason: format!("unsupported tier {}", m.audit.compliance_tier),
232                });
233            }
234        }
235
236        // software-kek refused past runtime 0.15.
237        if m.audit.dek_backend == "software-kek" && (current.0, current.1) > (0, 15) {
238            return Err(ManifestError::SoftwareKekProductionRefused {
239                current: m.runtime.runtime_current.clone(),
240            });
241        }
242
243        // audit.signature_class must be a known RuntimeSignatureClass token
244        // (the E13 policy-pinned class). An unknown token is rejected rather
245        // than silently treated as `none`.
246        if RuntimeSignatureClass::from_manifest_str(&m.audit.signature_class).is_none() {
247            return Err(ManifestError::InvalidValue {
248                field: "audit.signature_class",
249                reason: format!("unknown signature class {}", m.audit.signature_class),
250            });
251        }
252
253        Ok(())
254    }
255
256    /// Compute the canonical BLAKE3 digest of a snapshot. The manifest is
257    /// re-serialized to TOML through serde (struct field order is
258    /// deterministic), then hashed under the `arkhe-forge-manifest-digest`
259    /// domain.
260    ///
261    /// # `toml` crate dependency note
262    ///
263    /// The digest depends on the underlying `toml` crate's serialise output —
264    /// specifically its `BTreeMap` traversal order, key spacing, and string
265    /// quoting style. `Cargo.lock` pins the `toml` minor version so the
266    /// emitted bytes stay stable across builds within the pinned-toml
267    /// version window.
268    ///
269    /// A future `toml` **major** version bump (currently `1.x`, hypothetical
270    /// `2.x`) is allowed to change those low-level emit details and must be
271    /// accompanied by:
272    ///
273    /// 1. A regression sentinel update in the `digest_invariant` test module
274    ///    (`TIER0_DEV_DIGEST_V0_11` constant; test will surface the byte
275    ///    drift on first run).
276    /// 2. A manifest schema micro-patch documenting the digest re-pin —
277    ///    the manifest `schema_version` is **not** bumped on its own (the
278    ///    schema itself did not change); the spec patch records the toml
279    ///    crate bump as the cause.
280    ///
281    /// Design: regression sentinel + spec drift correction. The digest
282    /// input keeps wire-bytes pure (no toml-version literal mixed into the
283    /// keyed_hash key); the regression test catches any drift and turns it
284    /// into an explicit operator decision rather than a silent re-pin.
285    pub fn canonical_digest(m: &ManifestSnapshot) -> Result<ManifestDigest, ManifestError> {
286        let canonical = toml::to_string(m)
287            .map_err(|e| ManifestError::ParseError(format!("toml serialize: {}", e)))?;
288        let key = blake3::derive_key(DIGEST_DOMAIN, &[]);
289        let hash = blake3::keyed_hash(&key, canonical.as_bytes());
290        let mut out = [0u8; 32];
291        out.copy_from_slice(hash.as_bytes());
292        Ok(out)
293    }
294}
295
296// ===================== RuntimeBootstrap helper =====================
297
298/// Emit a `RuntimeBootstrap` event onto `ctx` using `digest` as the
299/// manifest anchor (the E12 axiom). The caller supplies the L0 and
300/// Runtime semver plus the active TypeCode pin set — downstream boot code
301/// plugs in the live registry.
302pub fn emit_runtime_bootstrap(
303    digest: &ManifestDigest,
304    l0_semver: SemVer,
305    runtime_semver: SemVer,
306    typecode_pins: Vec<TypeCode>,
307    bootstrap_tick: Tick,
308    ctx: &mut ActionContext<'_>,
309) -> Result<(), ActionError> {
310    let event = RuntimeBootstrap {
311        schema_version: 1,
312        l0_semver,
313        runtime_semver,
314        manifest_digest: *digest,
315        typecode_pins,
316        bootstrap_tick,
317    };
318    ctx.emit_event(&event)
319}
320
321// ===================== Helpers =====================
322
323/// Parse `"M.N"` or `"M.N.P"` into `(major, minor, patch)`. Rejects
324/// pre-release / build-metadata suffixes (Runtime does
325/// not accept SemVer 2.0 suffixes for canonical-bytes stability).
326fn parse_version(s: &str) -> Option<(u16, u16, u16)> {
327    let mut parts = s.splitn(3, '.');
328    let major: u16 = parts.next()?.parse().ok()?;
329    let minor: u16 = parts.next()?.parse().ok()?;
330    let patch: u16 = match parts.next() {
331        Some(p) => p.parse().ok()?,
332        None => 0,
333    };
334    Some((major, minor, patch))
335}
336
337// ===================== Tests =====================
338
339#[cfg(test)]
340#[allow(clippy::unwrap_used, clippy::expect_used)]
341mod tests {
342    use super::*;
343    use arkhe_kernel::abi::{CapabilityMask, InstanceId, Principal};
344
345    pub(super) const TIER0_DEV_TOML: &str = r#"
346schema_version = 1
347
348[shell]
349shell_id = "shell.dev.example"
350display_name = "Dev Sandbox"
351
352[runtime]
353runtime_max = "0.15"
354runtime_current = "0.13"
355
356[audit]
357pii_cipher = "xchacha20-poly1305"
358dek_backend = "software-kek"
359kms_auto_promote = "manual"
360signature_class = "ed25519"
361compliance_tier = 0
362"#;
363
364    const TIER1_PROD_TOML: &str = r#"
365schema_version = 1
366
367[shell]
368shell_id = "shell.prod.example"
369display_name = "Prod Shell"
370
371[runtime]
372runtime_max = "0.20"
373runtime_current = "0.13"
374
375[audit]
376pii_cipher = "aes-256-gcm-siv"
377dek_backend = "aws-kms"
378kms_auto_promote = "after_60min"
379signature_class = "hybrid"
380compliance_tier = 1
381
382[frontend]
383tls_required = true
384alpha_credential_rotation_required = true
385"#;
386
387    #[test]
388    fn load_valid_tier0_dev_manifest() {
389        let (snap, digest) = ManifestLoader::load(TIER0_DEV_TOML.as_bytes()).unwrap();
390        assert_eq!(snap.schema_version, 1);
391        assert_eq!(snap.audit.compliance_tier, 0);
392        assert_eq!(snap.audit.dek_backend, "software-kek");
393        assert_eq!(digest.len(), 32);
394        assert!(snap.frontend.tls_required); // default
395    }
396
397    #[test]
398    fn load_valid_tier1_prod_manifest() {
399        let (snap, digest) = ManifestLoader::load(TIER1_PROD_TOML.as_bytes()).unwrap();
400        assert_eq!(snap.audit.compliance_tier, 1);
401        assert_eq!(snap.audit.dek_backend, "aws-kms");
402        assert_eq!(digest.len(), 32);
403    }
404
405    #[test]
406    fn digest_is_deterministic_across_loads() {
407        let (_, d1) = ManifestLoader::load(TIER1_PROD_TOML.as_bytes()).unwrap();
408        let (_, d2) = ManifestLoader::load(TIER1_PROD_TOML.as_bytes()).unwrap();
409        assert_eq!(d1, d2);
410    }
411
412    #[test]
413    fn digest_is_whitespace_and_comment_invariant() {
414        let a = TIER0_DEV_TOML;
415        // Add comments + trailing whitespace; canonicalization should erase them.
416        let b = r#"
417# comment block
418schema_version = 1
419
420[shell]
421shell_id = "shell.dev.example"   # trailing comment
422display_name = "Dev Sandbox"
423
424[runtime]
425runtime_max = "0.15"
426runtime_current = "0.13"
427
428[audit]
429pii_cipher = "xchacha20-poly1305"
430dek_backend = "software-kek"
431kms_auto_promote = "manual"
432signature_class = "ed25519"
433compliance_tier = 0
434"#;
435        let (_, da) = ManifestLoader::load(a.as_bytes()).unwrap();
436        let (_, db) = ManifestLoader::load(b.as_bytes()).unwrap();
437        assert_eq!(da, db, "comments / whitespace must not affect digest");
438    }
439
440    #[test]
441    fn digest_differs_for_semantically_different_manifest() {
442        let (_, d0) = ManifestLoader::load(TIER0_DEV_TOML.as_bytes()).unwrap();
443        let (_, d1) = ManifestLoader::load(TIER1_PROD_TOML.as_bytes()).unwrap();
444        assert_ne!(d0, d1);
445    }
446
447    #[test]
448    fn print_tier0_dev_digest_for_pin() {
449        // One-shot helper to discover the sentinel bytes; left in the
450        // suite so maintainers can re-run it after a deliberate schema
451        // bump and copy the new bytes into
452        // `digest_invariant::TIER0_DEV_DIGEST_V0_11`.
453        let (_, d) = ManifestLoader::load(TIER0_DEV_TOML.as_bytes()).unwrap();
454        eprintln!(
455            "tier0_dev digest = {:?}",
456            d.iter().map(|b| format!("0x{b:02x}")).collect::<Vec<_>>()
457        );
458    }
459
460    #[test]
461    fn tier0_with_kms_backend_rejected() {
462        let bad = TIER0_DEV_TOML.replace("software-kek", "aws-kms");
463        let err = ManifestLoader::load(bad.as_bytes()).unwrap_err();
464        assert!(matches!(
465            err,
466            ManifestError::TierBackendMismatch { tier: 0, .. }
467        ));
468    }
469
470    #[test]
471    fn tier1_with_software_kek_rejected() {
472        let bad = TIER1_PROD_TOML.replace("aws-kms", "software-kek");
473        let err = ManifestLoader::load(bad.as_bytes()).unwrap_err();
474        assert!(matches!(
475            err,
476            ManifestError::TierBackendMismatch { tier: 1, .. }
477        ));
478    }
479
480    #[test]
481    fn software_kek_past_0_15_refused() {
482        let bad = TIER0_DEV_TOML
483            .replace("runtime_current = \"0.13\"", "runtime_current = \"0.16\"")
484            // runtime_max also must be >= current for version ordering to pass.
485            .replace("runtime_max = \"0.15\"", "runtime_max = \"0.20\"");
486        let err = ManifestLoader::load(bad.as_bytes()).unwrap_err();
487        assert!(matches!(
488            err,
489            ManifestError::SoftwareKekProductionRefused { .. }
490        ));
491    }
492
493    #[test]
494    fn runtime_max_less_than_current_rejected() {
495        let bad = TIER0_DEV_TOML.replace("runtime_max = \"0.15\"", "runtime_max = \"0.5\"");
496        let err = ManifestLoader::load(bad.as_bytes()).unwrap_err();
497        assert!(matches!(err, ManifestError::VersionMismatch));
498    }
499
500    #[test]
501    fn parse_error_on_malformed_toml() {
502        let err = ManifestLoader::load(b"this is not toml == invalid").unwrap_err();
503        assert!(matches!(err, ManifestError::ParseError(_)));
504    }
505
506    #[test]
507    fn unknown_top_level_field_rejected() {
508        let bad = format!("{}\nunknown_field = 42\n", TIER0_DEV_TOML);
509        let err = ManifestLoader::load(bad.as_bytes()).unwrap_err();
510        // `deny_unknown_fields` surfaces the serde error inside ParseError.
511        assert!(matches!(err, ManifestError::ParseError(_)));
512    }
513
514    #[test]
515    fn bad_semver_rejected() {
516        let bad = TIER0_DEV_TOML.replace(
517            "runtime_current = \"0.13\"",
518            "runtime_current = \"not-a-version\"",
519        );
520        let err = ManifestLoader::load(bad.as_bytes()).unwrap_err();
521        assert!(matches!(
522            err,
523            ManifestError::InvalidValue {
524                field: "runtime.runtime_current",
525                ..
526            }
527        ));
528    }
529
530    #[test]
531    fn manifest_rejects_unknown_signature_class() {
532        let bad = TIER0_DEV_TOML.replace(
533            "signature_class = \"ed25519\"",
534            "signature_class = \"dilithium\"",
535        );
536        let err = ManifestLoader::load(bad.as_bytes()).unwrap_err();
537        assert!(matches!(
538            err,
539            ManifestError::InvalidValue {
540                field: "audit.signature_class",
541                ..
542            }
543        ));
544    }
545
546    #[test]
547    fn manifest_accepts_each_valid_signature_class() {
548        for class in ["none", "ed25519", "ml-dsa-65", "hybrid"] {
549            let good = TIER0_DEV_TOML.replace(
550                "signature_class = \"ed25519\"",
551                &format!("signature_class = \"{class}\""),
552            );
553            assert!(
554                ManifestLoader::load(good.as_bytes()).is_ok(),
555                "signature_class {class} must be accepted"
556            );
557        }
558    }
559
560    #[test]
561    fn shell_id_over_32_bytes_rejected() {
562        let long_id = "a".repeat(33);
563        let bad = TIER0_DEV_TOML.replace("shell.dev.example", &long_id);
564        let err = ManifestLoader::load(bad.as_bytes()).unwrap_err();
565        assert!(matches!(
566            err,
567            ManifestError::InvalidValue {
568                field: "shell.shell_id",
569                ..
570            }
571        ));
572    }
573
574    #[test]
575    fn emit_runtime_bootstrap_appends_event() {
576        let (_, digest) = ManifestLoader::load(TIER0_DEV_TOML.as_bytes()).unwrap();
577        let mut ctx = ActionContext::new(
578            [0x11u8; 32],
579            InstanceId::new(1).unwrap(),
580            Tick(1),
581            Principal::System,
582            CapabilityMask::SYSTEM,
583        );
584        emit_runtime_bootstrap(
585            &digest,
586            SemVer::new(0, 11, 0),
587            SemVer::new(0, 11, 0),
588            vec![TypeCode(0x0003_0001), TypeCode(0x0003_0002)],
589            Tick(1),
590            &mut ctx,
591        )
592        .unwrap();
593        let events = ctx.drain_events();
594        assert_eq!(events.len(), 1);
595        assert_eq!(events[0].type_code, 0x0003_0F01);
596        assert_eq!(events[0].tick, Tick(1));
597
598        // The serialized payload round-trips and carries the supplied digest.
599        let bootstrap: RuntimeBootstrap = postcard::from_bytes(&events[0].payload).unwrap();
600        assert_eq!(bootstrap.manifest_digest, digest);
601        assert_eq!(bootstrap.l0_semver, SemVer::new(0, 11, 0));
602    }
603
604    #[test]
605    fn frontend_defaults_when_omitted() {
606        let (snap, _) = ManifestLoader::load(TIER0_DEV_TOML.as_bytes()).unwrap();
607        // TIER0_DEV_TOML omits [frontend]; defaults apply.
608        assert!(snap.frontend.tls_required);
609        assert!(snap.frontend.alpha_credential_rotation_required);
610    }
611
612    #[test]
613    fn parse_version_accepts_two_or_three_components() {
614        assert_eq!(parse_version("0.13"), Some((0, 13, 0)));
615        assert_eq!(parse_version("0.13.3"), Some((0, 13, 3)));
616        assert_eq!(parse_version(""), None);
617        assert_eq!(parse_version("0.13.3.4"), None); // 3-part splitn leaves "3.4"
618        assert_eq!(parse_version("abc.def"), None);
619    }
620}
621
622/// Manifest digest sentinel pin.
623///
624/// The canonical digest depends on the underlying `toml` crate's serialise
625/// output. `Cargo.lock` pins the toml minor version, so within the pinned
626/// toml-version window the bytes below are stable. A toml major bump
627/// (`1.x → 2.x`) is expected to change these bytes — the test then surfaces
628/// the drift, the operator updates the sentinel and writes a manifest schema
629/// manifest schema micro-patch documenting the re-pin.
630#[cfg(test)]
631#[allow(clippy::unwrap_used, clippy::expect_used)]
632mod digest_invariant {
633    use super::tests::TIER0_DEV_TOML;
634    use super::ManifestLoader;
635
636    /// SHA equivalent: `blake3::keyed_hash(derive_key("arkhe-forge-manifest-digest", &[]),
637    /// toml::to_string(TIER0_DEV_TOML_snapshot))` under the
638    /// `Cargo.lock`-pinned `toml` crate version.
639    const TIER0_DEV_DIGEST_V0_11: [u8; 32] = [
640        0xa1, 0xbf, 0xe8, 0x2a, 0x57, 0xe1, 0xde, 0x55, 0x05, 0x7e, 0x47, 0x51, 0xd0, 0xeb, 0xed,
641        0x3d, 0x48, 0x82, 0xdb, 0xd5, 0x95, 0x2d, 0x83, 0xcd, 0x52, 0x10, 0x6c, 0x0f, 0xae, 0x3b,
642        0xab, 0x3c,
643    ];
644
645    #[test]
646    fn tier0_dev_digest_matches_v0_11_sentinel() {
647        let (_, digest) = ManifestLoader::load(TIER0_DEV_TOML.as_bytes())
648            .expect("TIER0_DEV_TOML must parse cleanly");
649        assert_eq!(
650            digest, TIER0_DEV_DIGEST_V0_11,
651            "manifest canonical_digest drifted from the pinned sentinel — check toml crate \
652             version, ManifestSnapshot field order, and DIGEST_DOMAIN. Update the sentinel + \
653             write a manifest schema micro-patch if the change is intentional."
654        );
655    }
656}