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
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
//! Which isolation tier a tool runs in, what it enforced, and the record of
//! what actually confined a step.
//!
//! Named here, in the crate with no execution in it, because four layers need
//! the same vocabulary and none of them should have to depend on the others to
//! get it: a tool manifest *declares* a tier, the compiler *checks* it against
//! the profile a flow claims, the sandbox *is* one and reports what it
//! [`Confinement`] amounts to on this host, and the ledger records the
//! [`Isolation`] a step actually ran under (US-0508).
//!
//! The last of those is what turns "this ran confined" from a claim into a
//! fact: `bg plan` says which tier a tool *needs* and a tier says what it
//! *can* do here, and neither says what happened.
//!
//! `ARCHITECTURE.md` D1 schedules all four — in-process now, subprocess as the
//! Layer 3 target through M2 to M6, Wasmtime at M7 for Profile C, Firecracker
//! later — so the enum is complete from the start even though two of them are
//! not built yet. A tier arriving later should be a new implementation, not a
//! new word.

use std::fmt;

use serde::{Deserialize, Serialize};

use crate::grant::{GrantAtom, GrantSet};

/// How strongly a tool is isolated from the machine it runs on.
///
/// Ordered by strength, weakest first, and that ordering is load-bearing:
/// [`Tier::weakest`] is how a flow's overall isolation is summarised, and
/// summarising it any other way would let one unconfined tool hide behind
/// several confined ones.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum Tier {
    /// In this process, sharing its address space. Trusted built-in tools only.
    InProcess,
    /// A jailed subprocess. The safe default for anything not written here.
    Subprocess,
    /// Wasmtime with WASI preopens. The Profile C tier.
    Wasm,
    /// A microVM. Scheduled after Wasm; named so the vocabulary is complete.
    MicroVm,
}

impl Tier {
    /// Every tier, weakest first.
    pub const ALL: [Self; 4] = [Self::InProcess, Self::Subprocess, Self::Wasm, Self::MicroVm];

    /// The name used in a manifest, a plan, and a ledger entry.
    pub const fn name(self) -> &'static str {
        match self {
            Self::InProcess => "inproc",
            Self::Subprocess => "process",
            Self::Wasm => "wasm",
            Self::MicroVm => "microvm",
        }
    }

    /// Whether this tier virtualises the tool's view of the machine.
    ///
    /// What Profile C asks for. In-process and subprocess do not: the first
    /// shares the address space, and the second shares the kernel's namespaces
    /// unless something outside the tier has separated them.
    pub const fn is_virtualising(self) -> bool {
        matches!(self, Self::Wasm | Self::MicroVm)
    }

    /// Whether a tool has to be trusted for this tier to be safe.
    ///
    /// True only in-process, and it is the whole reason that tier refuses
    /// anything not built in: a tool sharing this address space is not confined
    /// by anything at all, so the only control left is who wrote it.
    pub const fn requires_trust(self) -> bool {
        matches!(self, Self::InProcess)
    }

    /// The weaker of two tiers.
    pub const fn weaker(self, other: Self) -> Self {
        if (self as u8) <= (other as u8) {
            self
        } else {
            other
        }
    }

    /// The weakest tier in a collection, if there is one.
    ///
    /// How a flow's isolation is reported. A flow is only as isolated as its
    /// least isolated tool, and saying anything else would let one in-process
    /// tool hide behind a dozen sandboxed ones.
    pub fn weakest(tiers: impl IntoIterator<Item = Self>) -> Option<Self> {
        tiers.into_iter().reduce(Self::weaker)
    }
}

/// The safe default: anything that did not say gets a subprocess.
///
/// D1 calls process isolation "the safe default for untrusted code", and a
/// manifest that forgot to declare is exactly the case that needs one. Defaulting
/// to in-process would mean a forgotten field silently bought full trust.
impl Default for Tier {
    fn default() -> Self {
        Self::Subprocess
    }
}

impl fmt::Display for Tier {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.name())
    }
}

/// How strongly a tier confines one resource domain.
///
/// Named here for the same reason [`Tier`] is: a tier *reports* one, the
/// runtime *carries* it, and a ledger entry *records* it, so the vocabulary
/// belongs to the crate none of the three can avoid depending on.
///
/// # Why the honest answer has three values
///
/// [`Enforcement::Kernel`] and [`Enforcement::None`] are the easy cases. The
/// interesting one is [`Enforcement::Interposed`]: the host stands between the
/// tool and the resource, which is real enforcement for everything that goes
/// through the handle it was given, and no enforcement at all against a tool
/// that makes its own syscall. An in-process tool is in exactly that position.
///
/// Collapsing that into "enforced" would be a lie and collapsing it into "not
/// enforced" would make the in-process tier unusable, so it is its own value
/// and the caller decides what it is worth. Profile C decides it is worth
/// nothing.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Enforcement {
    /// The kernel refuses. A tool cannot exceed it whatever it does.
    Kernel,
    /// The host stands in the way.
    ///
    /// Real for everything reached through the handle the tool was given, and
    /// worth nothing against a tool that makes its own syscall. See the type
    /// documentation on why this is its own value rather than rounded to one of
    /// the other two.
    Interposed,
    /// Not enforced. A tier reporting this for a domain must refuse any
    /// capability set that touches it.
    None,
}

impl Enforcement {
    /// Every grade, strongest first.
    pub const ALL: [Self; 3] = [Self::Kernel, Self::Interposed, Self::None];

    /// Whether a grant in this domain can be handed out at all.
    pub const fn permits_grants(self) -> bool {
        !matches!(self, Self::None)
    }

    /// The name used in a report and in a ledger entry.
    pub const fn name(self) -> &'static str {
        match self {
            Self::Kernel => "kernel",
            Self::Interposed => "interposed",
            Self::None => "none",
        }
    }

    /// How it reads in a diagnostic.
    pub const fn describe(self) -> &'static str {
        match self {
            Self::Kernel => "enforced by the kernel",
            Self::Interposed => "enforced by the host, which a tool making its own syscalls evades",
            Self::None => "not enforced",
        }
    }
}

impl fmt::Display for Enforcement {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.name())
    }
}

/// What a tier actually enforces, per domain, on this host.
///
/// Per host, not per tier in the abstract: the subprocess tier confines a
/// filesystem grant with a preopened directory everywhere, and confines a
/// network grant only where the operating system will give it a network
/// namespace. Reporting the abstract answer would be reporting the answer for
/// somebody else's machine.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Confinement {
    /// `fs:*` — reading, listing, writing files.
    pub filesystem: Enforcement,
    /// `net:*` — reaching anything off this machine.
    pub network: Enforcement,
    /// `proc:*` — starting other programs.
    pub process: Enforcement,
    /// `env:*` — reading the environment.
    pub environment: Enforcement,
    /// `secret:*` — using a credential.
    pub secret: Enforcement,
}

impl Confinement {
    /// A tier that enforces nothing. The floor to build from, so a tier claims
    /// enforcement only by saying so.
    pub const NONE: Self = Self {
        filesystem: Enforcement::None,
        network: Enforcement::None,
        process: Enforcement::None,
        environment: Enforcement::None,
        secret: Enforcement::None,
    };

    /// How this tier confines the given grant domain.
    ///
    /// An unknown domain is [`Enforcement::None`]. The grant vocabulary is
    /// meant to grow ([`KNOWN_DOMAINS`](crate::grant::KNOWN_DOMAINS) says so),
    /// and a tier that treated a domain it had never heard of as confined would
    /// be guessing in the one direction that is unsafe.
    pub const fn of(&self, domain: &str) -> Enforcement {
        // `match` on a `&str` in a const fn needs bytes.
        match domain.as_bytes() {
            b"fs" => self.filesystem,
            b"net" => self.network,
            b"proc" => self.process,
            b"env" => self.environment,
            b"secret" => self.secret,
            _ => Enforcement::None,
        }
    }

    /// Each domain and how it is confined, weakest-enforced domains included.
    pub const fn domains(&self) -> [(&'static str, Enforcement); 5] {
        [
            ("fs", self.filesystem),
            ("net", self.network),
            ("proc", self.process),
            ("env", self.environment),
            ("secret", self.secret),
        ]
    }

    /// Whether nothing at all is enforced.
    ///
    /// Not the same as saying nothing: a tier that reports this has been asked
    /// and has answered "none", which is exactly the fact a reviewer of a
    /// Profile C run came looking for.
    pub fn enforces_nothing(&self) -> bool {
        *self == Self::NONE
    }

    /// The first atom of `grants` this tier cannot enforce, if there is one.
    ///
    /// What a tier refuses a capability set on: a grant nothing enforces is an
    /// advisory permission, and handing one over would make the compiler's
    /// proof false at run time.
    pub fn unenforceable<'g>(&self, grants: &'g GrantSet) -> Option<&'g GrantAtom> {
        grants
            .iter()
            .find(|atom| !self.of(&atom.domain).permits_grants())
    }
}

impl fmt::Display for Confinement {
    /// `fs=kernel net=none proc=interposed env=kernel secret=interposed`.
    ///
    /// Every domain, every time, including the ones enforcing nothing. A
    /// rendering that omitted them would make "not confined" and "not
    /// mentioned" look the same, which is the distinction this type exists to
    /// keep.
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for (index, (domain, enforcement)) in self.domains().into_iter().enumerate() {
            if index > 0 {
                f.write_str(" ")?;
            }
            write!(f, "{domain}={enforcement}")?;
        }
        Ok(())
    }
}

/// What actually confined one step (US-0508).
///
/// # Why this is recorded rather than inferred
///
/// A plan says which tier a tool *needs*, and a tier says what it *can*
/// enforce on this host. Neither says what happened. A host that could not
/// deliver what the plan assumed is exactly the case worth catching — a
/// Profile C flow that silently ran in-process is a flow whose reproducibility
/// claim is false — so the tier that ran the step writes down what it was and
/// what it enforced, and the ledger keeps that beside the grant delta.
///
/// # Absent is not the same as none
///
/// The ledger holds an `Option<Isolation>`, and the two cases are different
/// facts. `None` means the runner did not say: nothing is claimed, and a
/// reviewer knows to go and ask. `Some` carrying [`Confinement::NONE`] means
/// the step ran with nothing enforced, which is a claim, and one somebody may
/// have to act on. Rounding either into the other would lose the difference
/// between an unaudited step and an unconfined one.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Isolation {
    /// The tier that actually ran the step.
    pub tier: Tier,
    /// What that tier actually enforced, per domain, on the host it ran on.
    pub confinement: Confinement,
    /// The reused worker that served it, where one did (US-0909).
    ///
    /// `None` is a child of its own: one process, one call, gone afterwards.
    /// `Some` means a warm worker served it, and names which — because "the
    /// tool ran in a subprocess" stops being the whole answer once a
    /// subprocess serves more than one step. Two steps confined identically
    /// and served by the *same* worker are a different fact from two steps
    /// that each had a process to themselves, and only one of them can be
    /// investigated by asking what else that worker did.
    ///
    /// Skipped when absent, so a cold child's entry encodes exactly as it did
    /// before warm workers existed.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub worker: Option<Worker>,
}

/// Which reused worker served a step, and how much it had already done
/// (US-0909).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Worker {
    /// Which worker, counting from one over the pool's whole life.
    ///
    /// A generation rather than an index: a worker that dies is replaced, and
    /// the replacement occupying the same slot is a *different* process. Reusing
    /// the number would make two workers' histories look like one's.
    pub generation: u64,
    /// How many calls it had already served when this one arrived.
    ///
    /// Zero for the first. What turns "worker 3 served this" into "worker 3 had
    /// already served nine other steps when it served this", which is the
    /// question an auditor looking at a warm pool actually has.
    pub served: u32,
}

impl Worker {
    /// The `served`-th call on worker `generation`.
    pub const fn new(generation: u64, served: u32) -> Self {
        Self { generation, served }
    }

    /// Whether this was the worker's first call, so nothing preceded it there.
    pub const fn is_first(&self) -> bool {
        self.served == 0
    }
}

impl fmt::Display for Worker {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "worker {} after {} call(s)",
            self.generation, self.served
        )
    }
}

impl Isolation {
    /// A step that ran in `tier`, which enforced `confinement`.
    ///
    /// In a child of its own: [`served_by`](Self::served_by) is what says a
    /// reused worker ran it instead.
    pub const fn new(tier: Tier, confinement: Confinement) -> Self {
        Self {
            tier,
            confinement,
            worker: None,
        }
    }

    /// Record that a reused worker served this step (US-0909).
    #[must_use]
    pub const fn served_by(mut self, worker: Worker) -> Self {
        self.worker = Some(worker);
        self
    }

    /// Whether a reused worker served it, rather than a child of its own.
    pub const fn was_warm(&self) -> bool {
        self.worker.is_some()
    }

    /// A step that ran in `tier` with nothing enforced at all.
    ///
    /// The record a trusted in-process tool leaves when nothing stands between
    /// it and the machine. Written down rather than omitted: see the type
    /// documentation.
    pub const fn unconfined(tier: Tier) -> Self {
        Self::new(tier, Confinement::NONE)
    }

    /// Whether the tier that ran it virtualises the machine — what Profile C
    /// asks for.
    pub const fn is_virtualising(&self) -> bool {
        self.tier.is_virtualising()
    }

    /// Whether nothing at all was enforced.
    pub fn enforces_nothing(&self) -> bool {
        self.confinement.enforces_nothing()
    }

    /// Whether every domain was refused by the kernel rather than by the host.
    ///
    /// The question a reviewer asking "was this confined, or merely asked
    /// nicely?" is actually asking.
    pub fn is_kernel_enforced(&self) -> bool {
        self.confinement
            .domains()
            .into_iter()
            .all(|(_, enforcement)| enforcement == Enforcement::Kernel)
    }
}

impl fmt::Display for Isolation {
    /// `process (fs=kernel net=none proc=interposed env=kernel secret=interposed)`,
    /// and where a warm worker served it, which one.
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} ({})", self.tier, self.confinement)?;
        match self.worker {
            Some(worker) => write!(f, ", {worker}"),
            None => Ok(()),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn every_tier_has_a_stable_name() {
        for (tier, name) in [
            (Tier::InProcess, "inproc"),
            (Tier::Subprocess, "process"),
            (Tier::Wasm, "wasm"),
            (Tier::MicroVm, "microvm"),
        ] {
            assert_eq!(tier.name(), name);
            assert_eq!(tier.to_string(), name);
        }
    }

    /// Profile C asks for a virtualising sandbox, and only two tiers are one.
    #[test]
    fn only_the_virtualising_tiers_say_they_are() {
        assert!(!Tier::InProcess.is_virtualising());
        assert!(!Tier::Subprocess.is_virtualising());
        assert!(Tier::Wasm.is_virtualising());
        assert!(Tier::MicroVm.is_virtualising());
    }

    /// The ordering is strength, and the rest of the system relies on it.
    #[test]
    fn tiers_are_ordered_weakest_first() {
        assert!(Tier::InProcess < Tier::Subprocess);
        assert!(Tier::Subprocess < Tier::Wasm);
        assert!(Tier::Wasm < Tier::MicroVm);

        let mut shuffled = [Tier::Wasm, Tier::InProcess, Tier::MicroVm, Tier::Subprocess];
        shuffled.sort();
        assert_eq!(shuffled, Tier::ALL);
    }

    /// A flow is only as isolated as its least isolated tool.
    #[test]
    fn the_weakest_tier_is_the_one_reported() {
        assert_eq!(
            Tier::weakest([Tier::Wasm, Tier::InProcess, Tier::Subprocess]),
            Some(Tier::InProcess)
        );
        assert_eq!(Tier::weakest([Tier::Wasm, Tier::MicroVm]), Some(Tier::Wasm));
        assert_eq!(Tier::weakest([]), None);
    }

    /// A forgotten field must not buy full trust.
    #[test]
    fn the_default_is_the_safe_one_rather_than_the_convenient_one() {
        assert_eq!(Tier::default(), Tier::Subprocess);
        assert!(!Tier::default().requires_trust());
    }

    #[test]
    fn only_the_in_process_tier_depends_on_trusting_the_tool() {
        assert!(Tier::InProcess.requires_trust());
        for tier in [Tier::Subprocess, Tier::Wasm, Tier::MicroVm] {
            assert!(!tier.requires_trust(), "{tier}");
        }
    }

    #[test]
    fn a_tier_round_trips_through_its_serialised_name() {
        for tier in Tier::ALL {
            let json = serde_json::to_string(&tier).unwrap();
            assert_eq!(serde_json::from_str::<Tier>(&json).unwrap(), tier);
        }
        assert_eq!(
            serde_json::to_string(&Tier::InProcess).unwrap(),
            "\"in_process\""
        );
    }

    // ── what a tier enforced (US-0508) ──────────────────────────────────────

    fn kernel_everywhere() -> Confinement {
        Confinement {
            filesystem: Enforcement::Kernel,
            network: Enforcement::Kernel,
            process: Enforcement::Kernel,
            environment: Enforcement::Kernel,
            secret: Enforcement::Kernel,
        }
    }

    /// The floor claims nothing, which is what makes claiming something mean
    /// something.
    #[test]
    fn the_confinement_floor_enforces_nothing_in_every_domain() {
        assert!(Confinement::NONE.enforces_nothing());
        for (domain, enforcement) in Confinement::NONE.domains() {
            assert_eq!(enforcement, Enforcement::None, "{domain}");
            assert!(!Confinement::NONE.of(domain).permits_grants());
        }
    }

    /// A domain nobody has heard of yet is not confined, and the answer errs in
    /// the safe direction rather than the convenient one.
    #[test]
    fn an_unknown_domain_is_never_reported_as_confined() {
        assert_eq!(kernel_everywhere().of("quantum"), Enforcement::None);
        assert!(!kernel_everywhere().of("quantum").permits_grants());
    }

    /// The distinction the whole grade exists for: interposed is enforcement,
    /// and it is not the kernel's.
    #[test]
    fn interposed_permits_grants_and_is_still_not_the_kernel() {
        assert!(Enforcement::Interposed.permits_grants());
        assert!(Enforcement::Kernel.permits_grants());
        assert!(!Enforcement::None.permits_grants());
        assert_ne!(Enforcement::Interposed, Enforcement::Kernel);
    }

    /// A step that ran with nothing enforced says so, and that is different
    /// from a runner that did not say.
    #[test]
    fn running_unconfined_is_a_claim_rather_than_a_silence() {
        let ran_unconfined = Isolation::unconfined(Tier::InProcess);
        let recorded: Option<Isolation> = Some(ran_unconfined);
        let unrecorded: Option<Isolation> = None;

        assert_ne!(recorded, unrecorded);
        assert!(ran_unconfined.enforces_nothing());
        assert!(!ran_unconfined.is_kernel_enforced());
    }

    #[test]
    fn only_a_virtualising_tier_satisfies_profile_c_after_the_fact() {
        assert!(!Isolation::unconfined(Tier::InProcess).is_virtualising());
        assert!(!Isolation::new(Tier::Subprocess, kernel_everywhere()).is_virtualising());
        assert!(Isolation::new(Tier::Wasm, kernel_everywhere()).is_virtualising());
    }

    /// Kernel enforcement is claimed for the whole step or not at all: one
    /// interposed domain is enough to make the claim false.
    #[test]
    fn one_interposed_domain_defeats_a_kernel_claim() {
        let mut confinement = kernel_everywhere();
        assert!(Isolation::new(Tier::Subprocess, confinement).is_kernel_enforced());

        confinement.secret = Enforcement::Interposed;
        assert!(!Isolation::new(Tier::Subprocess, confinement).is_kernel_enforced());
    }

    /// The rendering names every domain, so "not confined" never reads as "not
    /// mentioned".
    #[test]
    fn the_rendering_names_every_domain_including_the_unenforced_ones() {
        let rendered = Isolation::unconfined(Tier::InProcess).to_string();
        assert_eq!(
            rendered,
            "inproc (fs=none net=none proc=none env=none secret=none)"
        );
        for domain in ["fs", "net", "proc", "env", "secret"] {
            assert!(
                rendered.contains(domain),
                "{domain} missing from {rendered}"
            );
        }
    }

    // ── which worker served it (US-0909) ────────────────────────────────────

    /// A cold child and a warm worker are different facts about a step, and the
    /// record keeps them apart.
    #[test]
    fn a_reused_worker_is_named_and_a_child_of_its_own_is_not() {
        let cold = Isolation::new(Tier::Subprocess, kernel_everywhere());
        let warm = cold.served_by(Worker::new(3, 9));

        assert!(!cold.was_warm());
        assert!(warm.was_warm());
        assert_ne!(cold, warm, "same confinement, different history");

        let worker = warm.worker.expect("a warm step names its worker");
        assert_eq!(worker.generation, 3);
        assert_eq!(worker.served, 9);
        assert!(!worker.is_first());
        assert!(Worker::new(1, 0).is_first());
    }

    /// A dead worker's replacement is a different process, so it gets a
    /// different number rather than inheriting one.
    #[test]
    fn a_generation_is_not_a_slot() {
        assert_ne!(Worker::new(1, 0), Worker::new(2, 0));
        assert!(Worker::new(1, 5) < Worker::new(2, 0), "ordered by age");
    }

    #[test]
    fn the_rendering_names_the_worker_only_when_there_was_one() {
        let cold = Isolation::new(Tier::Subprocess, Confinement::NONE);
        assert!(!cold.to_string().contains("worker"));
        assert!(
            cold.served_by(Worker::new(2, 4))
                .to_string()
                .ends_with("worker 2 after 4 call(s)")
        );
    }

    #[test]
    fn an_isolation_record_round_trips_through_json() {
        for isolation in [
            Isolation::unconfined(Tier::InProcess),
            Isolation::new(Tier::Subprocess, kernel_everywhere()),
            Isolation::new(Tier::Subprocess, kernel_everywhere()).served_by(Worker::new(7, 2)),
            Isolation::new(Tier::Wasm, Confinement::NONE),
        ] {
            let json = serde_json::to_string(&isolation).unwrap();
            assert_eq!(serde_json::from_str::<Isolation>(&json).unwrap(), isolation);
        }
    }
}