Skip to main content

agent_frame/
unit.rs

1//! Units: what a derived thing is, what it owes its source, and what it takes
2//! to get one into the system.
3//!
4//! # The admission boundary
5//!
6//! [`Unit`] deliberately does **not** derive [`serde::Deserialize`]. An earlier
7//! revision did, and that made [`Unit::seal`] one construction path among two:
8//! a caller could decode `{"op":"generate","depth":0,...}` straight past every
9//! check the constructor performs. The claim that `depth <= 1` was "unreachable"
10//! was false for exactly that reason — it held for the mint and not for the
11//! decoder, which is the half an attacker uses.
12//!
13//! Decoding therefore lands in [`RawUnit`], a transparent DTO that asserts
14//! nothing, and the only way from there to a `Unit` is the fallible
15//! [`TryFrom`] below, which enforces the whole v0 contract:
16//!
17//! | rule | why |
18//! |---|---|
19//! | `op` must be [`Op::Elide`] | v0 mints only what it can check; the others' verification classes have no implementation |
20//! | `depth == op.depth_after(0)` | rejects a decoded depth 2, and any other declared depth the operation does not imply |
21//! | `span.start < span.end` | a range covering no bytes addresses nothing — this is what `addressed: bool` was pretending to be |
22//!
23//! Whether the span lies inside the *actual* source is not knowable here — it
24//! needs the bytes — so it is checked by [`crate::verify`], and the negative
25//! case is tested there.
26//!
27//! # What admission does NOT prove about `root`
28//!
29//! Stated because the omission is easy to read as an oversight, and because a
30//! boundary whose limits are undocumented gets trusted for more than it checks.
31//!
32//! Admission establishes that `root` is **present and is a syntactically valid
33//! dag-cbor-profile [`ContentId`]**. It establishes nothing else. In
34//! particular it does not establish that the id resolves to a stored
35//! [`crate::RootEvent`], and a `RawUnit` naming an id nothing ever minted
36//! admits — and verifies — exactly like one that names a real event.
37//!
38//! That is not a gap to be closed here. Resolving an id needs a store, and this
39//! crate has none; giving it one to answer a structural question is v0 growing
40//! the storage service its scope refuses. It is the same shape as the span
41//! rule one paragraph up: the check exists, and it lives where the data does.
42//!
43//! **The layer that owns it is the store.** `newt frame`'s `FrameStore`
44//! resolves the root and reports `root_kind` / `root_seq` / `root_content`
45//! when it can and `root_unresolved` when it cannot — never a silent absence,
46//! which is the same discipline `parents` applies to a missing link.
47//!
48//! Read together with the Lean referent this is a **weakening paired with a
49//! strengthening**. `fabricated u := u.root.isNone` is unrepresentable in Rust,
50//! because `root` is mandatory rather than an `Option`. What replaces the
51//! orphan is a root that *dangles*, which the Lean model has no way to express
52//! — its `Root` is an inhabitant, not a reference. `agent-frame` is stricter
53//! about presence and cannot be strict about resolution.
54//!
55//! # Identity is the derivation, not the lifecycle
56//!
57//! [`Unit::id`] is the id of the [`Derivation`]. `life` is deliberately outside
58//! it: superseding a root is a fact *about* a derivation, not a different
59//! derivation, and if it changed the id then marking a unit superseded would
60//! fork every reference to it.
61
62use content_addressable::{ContentAddressable, ContentError, ContentId, RawContentId};
63use serde::{Deserialize, Serialize};
64
65use crate::derivation::{Derivation, Span};
66use crate::op::Op;
67
68/// Lifecycle of a unit's root.
69///
70/// Operator intent drifts ("do Y instead"), and tracking that per-unit is
71/// intractable; tracking it at the ROOT is not, and everything downstream
72/// inherits it.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
74#[serde(rename_all = "snake_case")]
75pub enum Life {
76    /// The root still stands. Nothing downstream is freely evictable.
77    Live,
78    /// The operator replaced this intent ("do Y instead").
79    Superseded,
80    /// The operator retracted this intent outright.
81    Withdrawn,
82}
83
84/// Why a mint was refused.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
86pub enum SealError {
87    /// v0 mints elision only. The operation exists in the type; the constructor
88    /// refuses it until its verification class has an implementation.
89    #[error(
90        "v0 mints elision only: `{op:?}` has verification class `{check:?}`, which has \
91         no implementation and no calibrated threshold. Minting it would produce a unit \
92         whose check nobody can run."
93    )]
94    NotMintableInV0 {
95        /// The operation the caller asked to seal.
96        op: Op,
97        /// Its verification class — the reason the refusal is principled rather
98        /// than a not-implemented-yet.
99        check: crate::op::Check,
100    },
101    /// The span does not lie within the source it claims to address.
102    #[error("span [{start}, {end}) does not lie within a source of {source_len} bytes")]
103    SpanOutOfSource {
104        /// Start of the offending span.
105        start: u64,
106        /// End of the offending span.
107        end: u64,
108        /// Length of the source actually supplied.
109        source_len: usize,
110    },
111    /// A span covering no bytes addresses nothing.
112    #[error(
113        "span [{start}, {end}) covers no bytes: a unit that addresses nothing cannot be re-derived"
114    )]
115    UnaddressedSpan {
116        /// Start of the offending span.
117        start: u64,
118        /// End of the offending span.
119        end: u64,
120    },
121}
122
123/// Why a decoded unit was refused admission.
124///
125/// Distinct from [`SealError`] on purpose: sealing is *this process minting*,
126/// admission is *foreign bytes arriving*. They fail for overlapping reasons and
127/// a reader of a log needs to know which side produced the refusal.
128#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
129pub enum AdmitError {
130    /// The decoded operation is not mintable in v0.
131    #[error(
132        "decoded unit declares `{op:?}` (verification class `{check:?}`), which v0 does not \
133         admit: its check has no implementation, so the unit would be unverifiable evidence"
134    )]
135    NotMintableInV0 {
136        /// The operation the decoded unit declared.
137        op: Op,
138        /// Its verification class.
139        check: crate::op::Check,
140    },
141    /// The declared depth is not the depth the operation implies.
142    #[error(
143        "decoded unit declares depth {declared}, but `{op:?}` over source material implies \
144         depth {implied}. This is the check the old `Deserialize` derive bypassed."
145    )]
146    DepthMismatch {
147        /// The operation declared.
148        op: Op,
149        /// The depth the bytes claimed.
150        declared: u32,
151        /// The depth the operation actually implies.
152        implied: u32,
153    },
154    /// A span covering no bytes addresses nothing.
155    #[error("decoded unit's span [{start}, {end}) covers no bytes and so addresses nothing")]
156    UnaddressedSpan {
157        /// Start of the offending span.
158        start: u64,
159        /// End of the offending span.
160        end: u64,
161    },
162}
163
164/// The transparent decode target: **asserts nothing**.
165///
166/// Every field is public and plain. This type is what foreign bytes become, and
167/// it is not usable as a unit until it has been through
168/// [`TryFrom<RawUnit> for Unit`].
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
170#[serde(deny_unknown_fields)]
171pub struct RawUnit {
172    /// What the bytes claim was done.
173    pub op: Op,
174    /// The source byte string claimed.
175    pub source: RawContentId,
176    /// The byte range claimed.
177    pub span: Span,
178    /// The addressed material claimed.
179    pub elided: RawContentId,
180    /// The root event claimed.
181    pub root: ContentId,
182    /// The generation depth claimed.
183    pub depth: u32,
184    /// The lifecycle claimed.
185    pub life: Life,
186}
187
188/// A single unit of compacted context, addressed by its derivation.
189///
190/// Constructed only by [`Unit::seal`] or by admitting a [`RawUnit`]. Serialises
191/// (so it can be stored and hashed) but does not deserialise (so the admission
192/// boundary cannot be walked around).
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub struct Unit {
195    derivation: Derivation,
196    depth: u32,
197    life: Life,
198}
199
200/// A `Unit` encodes exactly as its [`RawUnit`], so what this process writes is
201/// what another process decodes — and what it decodes must then pass admission.
202/// Written by hand rather than derived: `#[serde(flatten)]` over the derivation
203/// would be a second, silently different layout, and `deny_unknown_fields` does
204/// not compose with it.
205impl Serialize for Unit {
206    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
207        self.to_raw().serialize(s)
208    }
209}
210
211impl Unit {
212    /// Seal one unit over SOURCE material.
213    ///
214    /// The source bytes are required — not a convenience, the whole point. A
215    /// unit cannot be minted without the material it addresses, so there is no
216    /// way to produce one whose `elided` claim was never computed from anything.
217    ///
218    /// # Errors
219    ///
220    /// [`SealError::NotMintableInV0`] for any operation other than [`Op::Elide`];
221    /// [`SealError::UnaddressedSpan`] for a span covering no bytes;
222    /// [`SealError::SpanOutOfSource`] when the span runs past the source.
223    pub fn seal(op: Op, source: &[u8], span: Span, root: ContentId) -> Result<Self, SealError> {
224        if op != Op::Elide {
225            return Err(SealError::NotMintableInV0 {
226                op,
227                check: op.check(),
228            });
229        }
230        if span.is_empty() {
231            return Err(SealError::UnaddressedSpan {
232                start: span.start,
233                end: span.end,
234            });
235        }
236        let derivation =
237            Derivation::over(op, source, span, root).ok_or(SealError::SpanOutOfSource {
238                start: span.start,
239                end: span.end,
240                source_len: source.len(),
241            })?;
242        Ok(Self {
243            derivation,
244            depth: op.depth_after(0),
245            life: Life::Live,
246        })
247    }
248
249    /// The addressed fact this unit's identity is computed over.
250    #[must_use]
251    pub const fn derivation(&self) -> &Derivation {
252        &self.derivation
253    }
254
255    /// This unit's address: the id of its [`Derivation`].
256    ///
257    /// # Errors
258    ///
259    /// Propagates an encoding failure from the canonical form.
260    pub fn id(&self) -> Result<crate::UnitId, ContentError> {
261        self.derivation.id().map(crate::UnitId::from)
262    }
263
264    /// What was done to the source material.
265    #[must_use]
266    pub const fn op(&self) -> Op {
267        self.derivation.op
268    }
269
270    /// Generation provenance depth: 0 for source material, one more per generation.
271    #[must_use]
272    pub const fn depth(&self) -> u32 {
273        self.depth
274    }
275
276    /// The root event this unit is caused by.
277    #[must_use]
278    pub const fn root(&self) -> ContentId {
279        self.derivation.root
280    }
281
282    /// The lifecycle of this unit's root.
283    #[must_use]
284    pub const fn life(&self) -> Life {
285        self.life
286    }
287
288    /// The depth bound: `depth <= 1`.
289    #[must_use]
290    pub const fn is_well_formed(&self) -> bool {
291        self.depth <= 1
292    }
293
294    /// A unit is freely evictable when its root is no longer live.
295    ///
296    /// Deterministic: no grounding check, no model call, no judgement.
297    #[must_use]
298    pub const fn evictable(&self) -> bool {
299        match self.life {
300            Life::Live => false,
301            Life::Superseded | Life::Withdrawn => true,
302        }
303    }
304
305    /// Mark this unit's root as no longer live.
306    ///
307    /// Deliberately does **not** change [`Unit::id`]: lifecycle is a fact about
308    /// a derivation, not a different derivation.
309    #[must_use]
310    pub const fn with_life(mut self, life: Life) -> Self {
311        self.life = life;
312        self
313    }
314
315    /// The transparent form, for encoding to bytes another process will decode.
316    #[must_use]
317    pub const fn to_raw(&self) -> RawUnit {
318        RawUnit {
319            op: self.derivation.op,
320            source: self.derivation.source,
321            span: self.derivation.span,
322            elided: self.derivation.elided,
323            root: self.derivation.root,
324            depth: self.depth,
325            life: self.life,
326        }
327    }
328}
329
330impl TryFrom<RawUnit> for Unit {
331    type Error = AdmitError;
332
333    /// **The only way in from foreign bytes.**
334    ///
335    /// # Errors
336    ///
337    /// See [`AdmitError`]. Span-versus-source is checked by [`crate::verify`],
338    /// which needs the bytes this function does not have.
339    fn try_from(raw: RawUnit) -> Result<Self, Self::Error> {
340        if raw.op != Op::Elide {
341            return Err(AdmitError::NotMintableInV0 {
342                op: raw.op,
343                check: raw.op.check(),
344            });
345        }
346        let implied = raw.op.depth_after(0);
347        if raw.depth != implied {
348            return Err(AdmitError::DepthMismatch {
349                op: raw.op,
350                declared: raw.depth,
351                implied,
352            });
353        }
354        if raw.span.is_empty() {
355            return Err(AdmitError::UnaddressedSpan {
356                start: raw.span.start,
357                end: raw.span.end,
358            });
359        }
360        Ok(Self {
361            derivation: Derivation {
362                op: raw.op,
363                source: raw.source,
364                span: raw.span,
365                elided: raw.elided,
366                root: raw.root,
367            },
368            depth: raw.depth,
369            life: raw.life,
370        })
371    }
372}
373
374impl ContentAddressable for Unit {
375    /// The canonical form is the **derivation's**, so a unit's bytes and its id
376    /// agree with [`Unit::id`].
377    fn canonical_form(&self) -> Result<Vec<u8>, ContentError> {
378        self.derivation.canonical_form()
379    }
380}