boundgraph-core 0.1.0

Core primitives for the BoundGraph runtime: content-addressed digests, work orders, context, condensates, evidence, and the ledger.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
//! The Ledger Entry (`λ`) — one tamper-evident record.
//!
//! The ledger is event-sourced rather than telemetry, and that single choice
//! buys three features from one mechanism: audit, deterministic replay, and
//! time travel.
//!
//! Tamper-evidence comes from the chain. Each entry commits to the digest of
//! the one before it, so altering entry *N* invalidates every entry after it.
//! Rewriting history therefore means rewriting the whole tail, and the root
//! digest gives that away.
//!
//! This module defines the entry and how the chain links. Writing entries
//! anywhere is US-1001, and the sinks are US-1002 onward.

use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};

use crate::canonical::{Canonical, CanonicalError};
use crate::digest::Digest;
use crate::evidence::Evidence;
use crate::grant::GrantSet;
use crate::id::{RunId, StepId};
use crate::isolation::Isolation;

/// What a step asked for, what it got, and what was taken away.
///
/// Recording all three makes Law I auditable after the fact: "this step asked
/// to write anywhere and was clamped to `/build`" is a fact in the ledger, not
/// something to be reconstructed from logs.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GrantDelta {
    /// What the work order requested.
    pub requested: GrantSet,
    /// What the gate actually granted.
    pub granted: GrantSet,
    /// What was removed between the two.
    pub attenuated: GrantSet,
}

impl GrantDelta {
    /// Record a grant decision.
    pub fn new(requested: GrantSet, granted: GrantSet, attenuated: GrantSet) -> Self {
        Self {
            requested,
            granted,
            attenuated,
        }
    }

    /// Whether anything was taken away.
    pub fn was_attenuated(&self) -> bool {
        !self.attenuated.is_empty()
    }
}

/// What a step cost.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct CostRecord {
    /// Prompt tokens consumed.
    pub tokens_in: u64,
    /// Completion tokens produced.
    pub tokens_out: u64,
    /// Monetary cost.
    ///
    /// A decimal, never a float: money that does not add up exactly is worse
    /// than no figure at all.
    pub usd: Decimal,
    /// Wall-clock time in milliseconds, including waiting.
    pub wall_ms: u64,
    /// Time spent inside tools, in milliseconds.
    pub tool_ms: u64,
}

impl CostRecord {
    /// Total tokens, in and out.
    pub fn total_tokens(&self) -> u64 {
        self.tokens_in.saturating_add(self.tokens_out)
    }
}

/// Points at the journalled nondeterministic inputs for a step.
///
/// Replay substitutes these for the outside world. Without one, a step is not
/// reproducible, which the determinism audit (US-0407) flags before the run.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JournalRef {
    /// Content address of the journal record.
    pub digest: Digest,
    /// How many nondeterministic inputs it captures.
    pub entries: u32,
}

/// `λ` — one append-only, hash-chained ledger record.
///
/// # Example
///
/// ```
/// use boundgraph_core::{Canonical, CostRecord, Digest, GrantDelta, GrantSet,
///                  LedgerEntry, RunId, StepId};
///
/// let run = RunId::new();
/// let step = StepId::new(Digest::of(b"step-1"));
/// let delta = GrantDelta::new(GrantSet::EMPTY, GrantSet::EMPTY, GrantSet::EMPTY);
///
/// let first = LedgerEntry::genesis(run, step, delta.clone(), CostRecord::default(), None, None);
/// let second = LedgerEntry::following(&first, step, delta, CostRecord::default(), None, None)?;
///
/// // Each entry commits to the one before it.
/// assert_eq!(second.prev, first.digest()?);
/// assert!(second.follows(&first)?);
/// # Ok::<(), boundgraph_core::CanonicalError>(())
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LedgerEntry {
    /// Position in the chain, starting at zero and gapless.
    pub seq: u64,
    /// Digest of entry `seq - 1`. The genesis entry links to all-zero.
    pub prev: Digest,
    /// Which run this belongs to.
    pub run: RunId,
    /// Which step this records.
    pub step: StepId,
    /// What was requested, granted, and attenuated.
    pub grant: GrantDelta,
    /// What it cost.
    pub cost: CostRecord,
    /// The verdict, where the step reached one.
    pub evidence: Option<Evidence>,
    /// Where the journalled nondeterministic inputs live.
    pub journal: Option<JournalRef>,
    /// What actually confined the step, where the runner said (US-0508).
    ///
    /// In the entry rather than beside it, because it is a fact about the
    /// step's *authority* and belongs where [`GrantDelta`] is: the chain
    /// commits to it, so a confinement record cannot be edited afterwards
    /// without giving itself away. That is the whole difference between "this
    /// ran confined" being checkable and being trusted.
    ///
    /// `None` means the runner did not say, and is deliberately distinct from
    /// [`Confinement::NONE`](crate::Confinement::NONE), which means it ran with
    /// nothing enforced. Skipped when absent, so an entry from a runner that
    /// says nothing encodes exactly as it did before this field existed and
    /// every digest already written stays valid.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub isolation: Option<Isolation>,
}

/// The digest a genesis entry links back to: no predecessor.
pub const GENESIS_PREV: Digest = Digest::from_bytes([0u8; 32]);

impl LedgerEntry {
    /// The first entry in a chain.
    pub fn genesis(
        run: RunId,
        step: StepId,
        grant: GrantDelta,
        cost: CostRecord,
        evidence: Option<Evidence>,
        journal: Option<JournalRef>,
    ) -> Self {
        Self {
            seq: 0,
            prev: GENESIS_PREV,
            run,
            step,
            grant,
            cost,
            evidence,
            journal,
            isolation: None,
        }
    }

    /// The next entry after `previous`, linked to it by digest.
    ///
    /// # Errors
    ///
    /// Returns [`CanonicalError`] if `previous` cannot be canonically encoded,
    /// which would make the link uncomputable.
    pub fn following(
        previous: &Self,
        step: StepId,
        grant: GrantDelta,
        cost: CostRecord,
        evidence: Option<Evidence>,
        journal: Option<JournalRef>,
    ) -> Result<Self, CanonicalError> {
        Ok(Self {
            seq: previous.seq + 1,
            prev: previous.digest()?,
            run: previous.run,
            step,
            grant,
            cost,
            evidence,
            journal,
            isolation: None,
        })
    }

    /// Record what actually confined the step (US-0508).
    ///
    /// A builder rather than a seventh constructor argument: the two
    /// constructors are what every sink calls, most transitions are not an
    /// execution, and a parameter that was `None` on five appends out of six
    /// would be noise at every call site that never had one to give.
    ///
    /// Setting it changes the entry's digest, so this belongs before the entry
    /// is handed to a chain rather than after.
    #[must_use]
    pub fn confined_by(mut self, isolation: Isolation) -> Self {
        self.isolation = Some(isolation);
        self
    }

    /// What confined this step, where the runner said.
    ///
    /// `None` means unrecorded. See [`isolation`](Self::isolation): a step that
    /// ran with nothing enforced returns `Some`.
    pub const fn confinement(&self) -> Option<Isolation> {
        self.isolation
    }

    /// Whether this entry records what confined the step at all.
    pub const fn is_confinement_recorded(&self) -> bool {
        self.isolation.is_some()
    }

    /// Whether this step ran in a virtualising tier — what Profile C asks for.
    ///
    /// `false` for a step whose confinement was never recorded, because an
    /// unrecorded step is not evidence that anything virtualised it. A caller
    /// that needs to tell the two apart reads
    /// [`is_confinement_recorded`](Self::is_confinement_recorded).
    pub const fn ran_virtualised(&self) -> bool {
        match self.isolation {
            Some(isolation) => isolation.is_virtualising(),
            None => false,
        }
    }

    /// Whether this entry genuinely follows `previous`.
    ///
    /// Checks both the sequence number and the digest link. A gap or a broken
    /// link means the chain has been altered.
    ///
    /// # Errors
    ///
    /// Returns [`CanonicalError`] if `previous` cannot be canonically encoded.
    pub fn follows(&self, previous: &Self) -> Result<bool, CanonicalError> {
        Ok(self.seq == previous.seq + 1 && self.prev == previous.digest()?)
    }

    /// Whether this is the first entry in a chain.
    pub fn is_genesis(&self) -> bool {
        self.seq == 0 && self.prev == GENESIS_PREV
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::evidence::{CheckerId, EvidenceKind, Signature, SigningKeyId, Verdict};

    fn delta() -> GrantDelta {
        GrantDelta::new(GrantSet::EMPTY, GrantSet::EMPTY, GrantSet::EMPTY)
    }

    fn step(tag: &[u8]) -> StepId {
        StepId::new(Digest::of(tag))
    }

    fn chain(len: usize) -> Vec<LedgerEntry> {
        let run = RunId::new();
        let mut out = vec![LedgerEntry::genesis(
            run,
            step(b"0"),
            delta(),
            CostRecord::default(),
            None,
            None,
        )];
        for i in 1..len {
            let prev = out.last().unwrap();
            out.push(
                LedgerEntry::following(
                    prev,
                    step(format!("{i}").as_bytes()),
                    delta(),
                    CostRecord::default(),
                    None,
                    None,
                )
                .unwrap(),
            );
        }
        out
    }

    #[test]
    fn carries_every_documented_field() {
        let e = LedgerEntry::genesis(
            RunId::new(),
            step(b"s"),
            delta(),
            CostRecord {
                tokens_in: 100,
                tokens_out: 20,
                usd: Decimal::new(41, 2),
                wall_ms: 1_200,
                tool_ms: 300,
            },
            Some(Evidence::new(
                Verdict::Pass,
                CheckerId::new("c", 1),
                [Digest::of(b"x")],
                Signature::from_bytes([0u8; 64]),
                SigningKeyId("k".into()),
                EvidenceKind::Deterministic,
            )),
            Some(JournalRef {
                digest: Digest::of(b"j"),
                entries: 3,
            }),
        );
        assert_eq!(e.seq, 0);
        assert_eq!(e.cost.total_tokens(), 120);
        assert_eq!(e.cost.usd, Decimal::new(41, 2));
        assert!(e.evidence.is_some());
        assert!(e.journal.is_some());
    }

    #[test]
    fn genesis_links_to_nothing() {
        let g = &chain(1)[0];
        assert!(g.is_genesis());
        assert_eq!(g.prev, GENESIS_PREV);
    }

    #[test]
    fn each_entry_commits_to_its_predecessor() {
        let c = chain(5);
        for pair in c.windows(2) {
            assert!(pair[1].follows(&pair[0]).unwrap());
        }
    }

    #[test]
    fn sequence_numbers_are_gapless() {
        let c = chain(5);
        for (i, e) in c.iter().enumerate() {
            assert_eq!(e.seq, i as u64);
        }
    }

    #[test]
    fn altering_an_entry_breaks_every_link_after_it() {
        let mut c = chain(5);

        // Tamper with entry 2: make it look cheaper than it was.
        c[2].cost.tokens_in = 999_999;

        // Its own link backwards still checks out...
        assert!(c[2].follows(&c[1]).unwrap());
        // ...but entry 3 no longer follows the altered entry 2.
        assert!(
            !c[3].follows(&c[2]).unwrap(),
            "tampering must invalidate the rest of the chain"
        );
    }

    #[test]
    fn a_reordered_chain_does_not_verify() {
        let c = chain(4);
        assert!(
            !c[3].follows(&c[1]).unwrap(),
            "entries cannot be resequenced"
        );
    }

    #[test]
    fn grant_delta_records_attenuation() {
        use crate::grant::GrantAtom;
        let d = GrantDelta::new(
            GrantSet::new([GrantAtom::new("fs", "write", "/**")]),
            GrantSet::new([GrantAtom::new("fs", "write", "/build/**")]),
            GrantSet::new([GrantAtom::new("fs", "write", "/**")]),
        );
        assert!(d.was_attenuated());
        assert!(!delta().was_attenuated());
    }

    #[test]
    fn cost_is_decimal_not_float() {
        // 0.1 + 0.2 must be exactly 0.3, which f64 cannot promise.
        let a = Decimal::new(1, 1);
        let b = Decimal::new(2, 1);
        assert_eq!(a + b, Decimal::new(3, 1));
    }

    #[test]
    fn round_trips_through_canonical_cbor() {
        let e = chain(2).pop().unwrap();
        let bytes = crate::to_canonical_cbor(&e).unwrap();
        let back: LedgerEntry = crate::from_canonical_cbor(&bytes).unwrap();
        assert_eq!(e, back);
    }
}