Skip to main content

pr4xis_runtime/
load.rs

1//! The fail-closed load gate — admit a `.prx` only after re-deriving its root
2//! and checking it against the trusted root.
3//!
4//! This is the runtime's verify-before-interpret kernel primitive. The runtime
5//! never trusts a self-asserted identity: it decodes the bytes, re-derives the
6//! archive's Merkle root from the content it is about to admit, and accepts the
7//! archive only if that root equals the externally-trusted root (the pin a peer
8//! or the lock supplies). A tampered, stale, or mis-addressed archive is
9//! refused, not loaded.
10//!
11//! The loaded [`Archive`] is the OPEN form — generators + relations + connections
12//! as data, the free-category presentation. Rebinding it into the closed-world
13//! compiled `Category` (via `FreeExtension`, driven by each connection's
14//! action-on-generators) is the next layer.
15
16use crate::address::ContentAddress;
17use crate::archive::Archive;
18use crate::codec::{self, CodecError};
19
20/// Why a `.prx` failed to load.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum LoadError {
23    /// The bytes could not be decoded as a `.prx` archive.
24    Decode(CodecError),
25    /// The archive decoded, but its re-derived Merkle root does not match the
26    /// trusted root — a tampered, stale, or wrong archive. Refused.
27    RootMismatch {
28        expected: ContentAddress,
29        actual: ContentAddress,
30    },
31}
32
33impl core::fmt::Display for LoadError {
34    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
35        match self {
36            LoadError::Decode(e) => write!(f, "decode .prx: {e}"),
37            LoadError::RootMismatch { expected, actual } => write!(
38                f,
39                ".prx root mismatch: expected {}, got {} — refused",
40                expected.to_hex(),
41                actual.to_hex()
42            ),
43        }
44    }
45}
46
47impl std::error::Error for LoadError {}
48
49/// Emit an [`Archive`] to its canonical `.prx` bytes (DAG-CBOR). The archive's
50/// identity is its [`Archive::root`], derived from the content — NOT these bytes
51/// — so the gate re-derives the root on load rather than hashing the wire.
52pub fn emit(archive: &Archive) -> Result<Vec<u8>, CodecError> {
53    codec::canonical_encode(archive)
54}
55
56/// Load a `.prx` archive from its canonical bytes, FAIL-CLOSED against
57/// `trusted_root`: decode, re-derive the Merkle root, and admit the archive only
58/// if it matches. The trusted root comes from OUTSIDE the bytes (a peer's claim,
59/// a lock pin) — that is what makes the check meaningful.
60pub fn load(bytes: &[u8], trusted_root: ContentAddress) -> Result<Archive, LoadError> {
61    let archive: Archive = codec::canonical_decode(bytes).map_err(LoadError::Decode)?;
62    let actual = archive.root().map_err(LoadError::Decode)?;
63    if actual != trusted_root {
64        return Err(LoadError::RootMismatch {
65            expected: trusted_root,
66            actual,
67        });
68    }
69    Ok(archive)
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use crate::connection::{Connection, GeneratorAction};
76    use crate::definition::Definition;
77
78    fn sample() -> Archive {
79        Archive {
80            nodes: vec![Definition {
81                kind: "Concept".into(),
82                name: "Employer".into(),
83                edges: vec![("Subsumption".into(), "Agent".into())],
84                axioms: vec!["EmployerIsAgent".into()],
85                lexical: Some("employer".into()),
86            }],
87            connections: vec![Connection {
88                kind: "FullyFaithful".into(),
89                source: "A".into(),
90                target: "B".into(),
91                action: GeneratorAction::Functor {
92                    map_object: vec![("A".into(), "B".into())],
93                    map_morphism: vec![],
94                },
95                laws: vec!["PreservesComposition".into()],
96            }],
97        }
98    }
99
100    #[test]
101    fn emit_then_load_round_trips() {
102        let a = sample();
103        let bytes = emit(&a).unwrap();
104        let loaded = load(&bytes, a.root().unwrap()).unwrap();
105        assert_eq!(loaded, a);
106    }
107
108    #[test]
109    fn load_refuses_a_wrong_root() {
110        let a = sample();
111        let bytes = emit(&a).unwrap();
112        let err = load(&bytes, ContentAddress::of(b"not the root")).unwrap_err();
113        assert!(matches!(err, LoadError::RootMismatch { .. }));
114    }
115
116    #[test]
117    fn load_refuses_tampered_bytes() {
118        let a = sample();
119        let trusted = a.root().unwrap();
120        let mut bytes = emit(&a).unwrap();
121        // Flip a byte: either decode fails, or it decodes to a different archive
122        // whose re-derived root no longer matches. Either way — refused.
123        *bytes.last_mut().unwrap() ^= 0xff;
124        assert!(load(&bytes, trusted).is_err());
125    }
126
127    #[test]
128    fn load_refuses_garbage() {
129        let err = load(b"not dag-cbor at all", ContentAddress::of(b"x")).unwrap_err();
130        assert!(matches!(err, LoadError::Decode(_)));
131    }
132}