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}