cellos-core 0.7.3

CellOS domain types and ports — typed authority, formation DAG, CloudEvent envelopes, RBAC primitives. No I/O.
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
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
//! Typed authority validator — the three variants and their validation logic.
//!
//! See parent module [`crate::authority`] for the doctrine narrative and
//! status notes. This file holds the mechanical truth.
//!
//! # Mechanical separation
//!
//! [`ObservedAuthority`], [`ProvenAuthority`], and [`ImposedAuthority`] are
//! three **distinct, non-convertible** structs. There are no `From`/`Into`
//! impls between them, no shared trait, no common base type. Merging is a
//! compile error.
//!
//! Each variant carries its own validated [`AuthorityDerivation`] (the ADG
//! sub-object). Construction goes through the type's `try_new` method which
//! invokes [`AuthorityDerivation::validate`] plus type-class compatibility
//! checks. Once constructed, fields are immutable (no public setters; ADG
//! and output are exposed read-only).

use std::collections::BTreeSet;

use serde::{Deserialize, Serialize};

// ---------------------------------------------------------------------------
// Enums shared across the three typed-authority variants
// ---------------------------------------------------------------------------

/// Capability-tier and confidence semantics from
/// [docs/authority-model.md §5b](../../../../docs/authority-model.md).
///
/// `STATISTICALLY_BOUNDED` is reserved per §5a but not yet emitted by any
/// rule-class; it does not appear in the canonical mapping.
///
/// `DECLARED` is the [ADR-0006] `DeclaredAuthority` variant — guest-agent
/// declarations forwarded over the host-bound vsock channel. It is the
/// **floor** of the precedence ladder: any co-occurring class outranks it.
///
/// [ADR-0006]: ../../../../docs/adr/0006-in-vm-observability-runner-evidence.md
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum EpistemicStatus {
    /// Raw signal seen on the wire; no structural cross-check applied.
    DirectObservation,
    /// Two or more signals align in the way the protocol mandates.
    StructurallyBound,
    /// Verified via signature or chain-of-trust the workload cannot forge.
    CryptographicProof,
    /// A structural signal we expected to see was deliberately encrypted/withheld.
    Opaque,
    /// Authority asserted via in-line interception (Stack 3 / M2 quarantine only).
    Imposed,
    /// Guest-agent declaration forwarded over a host-bound vsock channel
    /// (ADR-0006). Strictly weaker than [`Self::DirectObservation`] — any
    /// co-occurring class outranks it in the canonical mapping.
    Declared,
}

/// Rule-class drives [`EpistemicStatus`]; this is the canonical enum from
/// [docs/authority-derivation-graph.md](../../../../docs/authority-derivation-graph.md).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RuleClass {
    /// Lone-signal sighting. Tier ceiling 2.
    RawObservation,
    /// Two protocol-mandated signals align. Tier ceiling 3.
    StructuralAlignment,
    /// Cryptographic verification (JWS, DANE-TLSA, SVID). Tier ceiling 4.
    CryptographicProof,
    /// Expected signal deliberately withheld (ECH, encrypted inner SNI).
    /// Tier ceiling 1.
    Opacity,
    /// In-line termination observed plaintext post-MITM. Tier ceiling 4
    /// (with named structural residuals; emission must carry
    /// `non_authoritative_enforcement: true`).
    ImposedInterception,
    /// Guest-agent declaration forwarded over the per-cell vsock channel
    /// (ADR-0006). Tier ceiling 1; floor of the precedence ladder. Backs
    /// [`DeclaredAuthority`] exclusively.
    GuestAgentDeclaration,
}

/// Named rules per
/// [docs/authority-derivation-graph.md](../../../../docs/authority-derivation-graph.md).
/// Adding a new rule requires updating that document AND
/// [`canonical_class_for_rule`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Rule {
    /// SNI parsed from ClientHello, no cross-check.
    RawSniObserved,
    /// Host header parsed, no cross-check.
    RawHostHeaderObserved,
    /// h2 `:authority` parsed.
    RawH2AuthorityObserved,
    /// SNI value byte-equals Host value (post-IDN, post-port-strip).
    SniHostMatch,
    /// h2 `:authority` matches HTTP/1.x Host on the same flow.
    H2AuthorityHostMatch,
    /// SNI matches a declared `cdnAuthority.providers[].hostnamePattern`.
    SniCdnProviderMatch,
    /// Workload-signed authority claim verified against cell-ephemeral key.
    JwsSignatureVerified,
    /// Upstream cert chain matches DNSSEC-validated TLSA record.
    DaneTlsaBound,
    /// SPIFFE SVID chain verified (future).
    SvidChainVerified,
    /// `encrypted_client_hello` extension present.
    EchDetected,
    /// Successor to ECH_DETECTED when more state available.
    EncryptedInnerSni,
    /// Stack 3 / M2 only.
    MitmHandshakeTerminated,
    /// In-VM agent declared a process spawn (ADR-0006).
    GuestProcSpawnObserved,
    /// In-VM agent declared a process exit (ADR-0006).
    GuestProcExitObserved,
    /// In-VM agent declared an inotify watch firing (ADR-0006).
    GuestFsInotifyFired,
    /// In-VM agent declared a capability denial (ADR-0006).
    GuestCapDenialObserved,
    /// In-VM agent declared a `connect()` attempt (ADR-0006).
    GuestNetConnectAttempted,
}

/// Binding status per
/// [docs/authority-model.md §6](../../../../docs/authority-model.md).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum BindingStatus {
    /// At least one structural-alignment or cryptographic-proof rule fired
    /// AND no opacity/imposed rule fired.
    Bound,
    /// Any opacity rule fired, OR no alignment/proof rule fired against a
    /// multi-input set.
    Unknown,
    /// Imposed-interception rule fired (Stack 3 only).
    ImposedBound,
    /// Single-input emission where binding is undefined.
    NotApplicable,
}

/// Canonical input types — a strict subset of those listed in the ADG doc.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuthorityInputType {
    /// Bytes from ClientHello SNI extension.
    Sni,
    /// Bytes from HTTP/1.x `Host:` header.
    HostHeader,
    /// Bytes from h2 HEADERS `:authority` pseudo-header.
    H2Authority,
    /// Subject Alternative Name from a verified upstream cert.
    TlsCertSan,
    /// DNSSEC-validated TLSA record content.
    DaneTlsaRecord,
    /// Verified body of a workload-signed JWS authority claim.
    JwsPayload,
    /// DNSSEC-validated A/AAAA answer (links to upstream Phase 3h emission).
    DnssecValidatedARecord,
    /// Stack 3 / `IMPOSED` only; bytes seen post-termination.
    MitmTerminatedObservation,
}

// ---------------------------------------------------------------------------
// ADG sub-object types
// ---------------------------------------------------------------------------

/// One evidence atom consumed by a derivation. Mirrors the ADG `inputs[]`
/// schema; see [docs/authority-derivation-graph.md](../../../../docs/authority-derivation-graph.md).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AuthorityInput {
    /// Canonical input type.
    #[serde(rename = "type")]
    pub input_type: AuthorityInputType,
    /// Observed value (redacted per `RedactingEventSink` policy if applicable).
    pub value: String,
    /// Coverage score for this input alone, ∈ \[0, 1\].
    pub confidence: f64,
    /// Optional UUID — when this input itself was the output of a prior
    /// emission, links the chain.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub source_event_id: Option<String>,
}

/// One rule that fired during derivation.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct AppliedRule {
    /// Named rule.
    pub rule: Rule,
    /// Class — must match [`canonical_class_for_rule`] for `rule`.
    pub class: RuleClass,
}

/// The derived `(tier, confidence, epistemic_status, binding_status)` tuple.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct AuthorityOutput {
    /// Capability tier, ∈ \[0, 4\].
    pub tier: u8,
    /// Coverage-derived confidence, ∈ \[0, 1\].
    pub confidence: f64,
    /// Deterministically derived from the rule-class set.
    pub epistemic_status: EpistemicStatus,
    /// Per [`BindingStatus`] semantics.
    pub binding_status: BindingStatus,
}

/// The complete ADG sub-object. Serializable for downstream events.
///
/// Construction does not validate; callers must invoke [`Self::validate`] (or
/// go through one of the typed-authority `try_new` constructors which call
/// `validate` for them).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AuthorityDerivation {
    /// Inputs consumed by the derivation. Must be non-empty.
    pub inputs: Vec<AuthorityInput>,
    /// Rules applied to those inputs. Must be non-empty.
    pub rules_applied: Vec<AppliedRule>,
    /// The derivation's output tuple.
    pub output: AuthorityOutput,
}

// ---------------------------------------------------------------------------
// Validation errors
// ---------------------------------------------------------------------------

/// Errors the validator can return. Each variant names the specific §9
/// invariant it violates so debug logs and tests can distinguish them.
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum ValidationError {
    /// `inputs` is empty (Property 4 — ADG presence).
    #[error("authority derivation has no inputs")]
    NoInputs,
    /// `rules_applied` is empty (Property 4 — ADG presence).
    #[error("authority derivation has no applied rules")]
    NoRulesApplied,
    /// An input's `confidence` is outside \[0, 1\].
    #[error("input confidence {0} out of range [0, 1]")]
    InputConfidenceOutOfRange(f64),
    /// An input's `confidence` is NaN.
    #[error("input confidence is NaN")]
    InputConfidenceNaN,
    /// `output.confidence` is outside \[0, 1\] or NaN.
    #[error("output confidence {0} out of range [0, 1] or NaN")]
    OutputConfidenceInvalid(f64),
    /// `output.tier` is outside \[0, 4\].
    #[error("output tier {0} out of range [0, 4]")]
    TierOutOfRange(u8),
    /// Property 1 — `output.confidence > max(inputs.confidence)`.
    #[error("confidence inflation: output {output} exceeds max input {max_input}")]
    ConfidenceInflation {
        /// Declared output confidence.
        output: f64,
        /// Maximum input confidence observed.
        max_input: f64,
    },
    /// Property 2 — `output.tier > min(class_tier_ceiling)`.
    #[error("tier inflation: output tier {output} exceeds ceiling {ceiling} (limited by {limiting_class:?})")]
    TierInflation {
        /// Declared output tier.
        output: u8,
        /// Maximum tier permitted by the rule-class set.
        ceiling: u8,
        /// The class whose ceiling was violated.
        limiting_class: RuleClass,
    },
    /// Property 3 — `output.epistemic_status` is not the deterministic image
    /// of the rule-class set.
    #[error("epistemic status mismatch: declared {declared:?}, expected {expected:?}")]
    EpistemicMismatch {
        /// Declared `epistemic_status`.
        declared: EpistemicStatus,
        /// Status the canonical mapping demands.
        expected: EpistemicStatus,
    },
    /// Property 5 — a rule's declared class does not match the canonical
    /// `rule -> class` mapping.
    #[error("rule {rule:?} declared class {declared:?}, canonical class is {canonical:?}")]
    RuleClassMismatch {
        /// Rule whose class was declared.
        rule: Rule,
        /// Class the caller declared.
        declared: RuleClass,
        /// Class the canonical mapping demands.
        canonical: RuleClass,
    },
    /// `binding_status` does not match the rules' implications.
    #[error("binding status mismatch: declared {declared:?}, expected {expected:?}")]
    BindingStatusMismatch {
        /// Declared `binding_status`.
        declared: BindingStatus,
        /// Status the canonical mapping demands.
        expected: BindingStatus,
    },
    /// Type-class compatibility violation — the typed authority's permitted
    /// rule-class envelope was exceeded.
    #[error(
        "type-class incompatible: {type_name} requires rule-classes within {permitted:?}, got {rejected:?}"
    )]
    TypeClassIncompatible {
        /// `"ObservedAuthority"` / `"ProvenAuthority"` / `"ImposedAuthority"`.
        type_name: &'static str,
        /// The permitted set for this type.
        permitted: &'static [RuleClass],
        /// The class that triggered the rejection.
        rejected: RuleClass,
    },
    /// `ProvenAuthority` was constructed without a verified-signature artifact.
    #[error("ProvenAuthority requires a verified-signature artifact")]
    ProvenAuthorityMissingArtifact,
    /// `ImposedAuthority` requires the IMPOSED_INTERCEPTION rule-class to
    /// have fired in its derivation.
    #[error("ImposedAuthority requires the IMPOSED_INTERCEPTION rule-class in its derivation")]
    ImposedAuthorityMissingInterception,
    /// The empty-class-set case from the ADG mapping table.
    #[error("rule-class set is empty after deduplication; refusing emission")]
    EmptyClassSet,
}

// ---------------------------------------------------------------------------
// Canonical mappings
// ---------------------------------------------------------------------------

/// The canonical rule-name → rule-class mapping. Mirrors the table in
/// [docs/authority-derivation-graph.md](../../../../docs/authority-derivation-graph.md).
///
/// **This function is the source of truth for Property 5 (rule-class
/// consistency).** Adding a new rule requires:
///
/// 1. Adding the variant to [`Rule`].
/// 2. Adding the entry here.
/// 3. Updating the ADG doc.
/// 4. Updating any tests that enumerate `Rule` exhaustively.
///
/// The compiler's exhaustiveness check catches step 2 if step 1 lands first.
#[must_use]
pub const fn canonical_class_for_rule(rule: Rule) -> RuleClass {
    match rule {
        Rule::RawSniObserved | Rule::RawHostHeaderObserved | Rule::RawH2AuthorityObserved => {
            RuleClass::RawObservation
        }
        Rule::SniHostMatch | Rule::H2AuthorityHostMatch | Rule::SniCdnProviderMatch => {
            RuleClass::StructuralAlignment
        }
        Rule::JwsSignatureVerified | Rule::DaneTlsaBound | Rule::SvidChainVerified => {
            RuleClass::CryptographicProof
        }
        Rule::EchDetected | Rule::EncryptedInnerSni => RuleClass::Opacity,
        Rule::MitmHandshakeTerminated => RuleClass::ImposedInterception,
        Rule::GuestProcSpawnObserved
        | Rule::GuestProcExitObserved
        | Rule::GuestFsInotifyFired
        | Rule::GuestCapDenialObserved
        | Rule::GuestNetConnectAttempted => RuleClass::GuestAgentDeclaration,
    }
}

/// Tier ceiling per rule-class — the canonical mapping. The ADG doc and
/// [docs/authority-model.md §5b](../../../../docs/authority-model.md) pin
/// these numbers; do not change them without an ADR amendment.
#[must_use]
pub const fn max_tier_for_class(class: RuleClass) -> u8 {
    match class {
        RuleClass::RawObservation => 2,
        RuleClass::StructuralAlignment => 3,
        RuleClass::CryptographicProof => 4,
        RuleClass::Opacity => 1,
        RuleClass::ImposedInterception => 4,
        RuleClass::GuestAgentDeclaration => 1,
    }
}

/// The deterministic class-set → epistemic-status mapping per
/// [docs/authority-derivation-graph.md](../../../../docs/authority-derivation-graph.md):
///
/// ```text
/// {ImposedInterception, ...}                                   → Imposed
/// {Opacity, ...except Imposed}                                  → Opaque
/// {CryptographicProof, ...except above}                         → CryptographicProof
/// {StructuralAlignment, ...except above}                        → StructurallyBound
/// {RawObservation, ...except above}                             → DirectObservation
/// {GuestAgentDeclaration only}                                  → Declared (ADR-0006)
/// empty                                                         → returns None
/// ```
///
/// Per [ADR-0006] §2 the precedence ladder is
/// `Imposed > Opaque > CryptographicProof > StructurallyBound > DirectObservation > Declared`
/// — `Declared` is the floor, so any host-side class co-occurring with
/// `GuestAgentDeclaration` outranks it.
///
/// Returns `None` for the empty set; callers translate to
/// [`ValidationError::EmptyClassSet`].
///
/// [ADR-0006]: ../../../../docs/adr/0006-in-vm-observability-runner-evidence.md
#[must_use]
pub fn epistemic_for_class_set(classes: &BTreeSet<RuleClass>) -> Option<EpistemicStatus> {
    if classes.is_empty() {
        return None;
    }
    if classes.contains(&RuleClass::ImposedInterception) {
        return Some(EpistemicStatus::Imposed);
    }
    if classes.contains(&RuleClass::Opacity) {
        return Some(EpistemicStatus::Opaque);
    }
    if classes.contains(&RuleClass::CryptographicProof) {
        return Some(EpistemicStatus::CryptographicProof);
    }
    if classes.contains(&RuleClass::StructuralAlignment) {
        return Some(EpistemicStatus::StructurallyBound);
    }
    if classes.contains(&RuleClass::RawObservation) {
        return Some(EpistemicStatus::DirectObservation);
    }
    // Floor of the precedence ladder — only GuestAgentDeclaration left.
    if classes.contains(&RuleClass::GuestAgentDeclaration) {
        return Some(EpistemicStatus::Declared);
    }
    // Unreachable given enum exhaustiveness above, but kept defensive.
    None
}

/// Compute the expected [`BindingStatus`] given the inputs and rule-class
/// set, per [`BindingStatus`] doc-comments.
fn expected_binding_status(
    inputs: &[AuthorityInput],
    classes: &BTreeSet<RuleClass>,
) -> BindingStatus {
    if classes.contains(&RuleClass::ImposedInterception) {
        return BindingStatus::ImposedBound;
    }
    let opacity_or_imposed = classes.contains(&RuleClass::Opacity);
    let alignment_or_proof = classes.contains(&RuleClass::StructuralAlignment)
        || classes.contains(&RuleClass::CryptographicProof);
    if opacity_or_imposed {
        return BindingStatus::Unknown;
    }
    if alignment_or_proof {
        return BindingStatus::Bound;
    }
    if inputs.len() <= 1 {
        // single-input derivation; binding is undefined.
        BindingStatus::NotApplicable
    } else {
        // multi-input set with no alignment/proof — explicit UNKNOWN per ADG §4.
        BindingStatus::Unknown
    }
}

impl AuthorityDerivation {
    /// Run all six §9 invariants over this derivation. Returns `Ok(())` on
    /// success or the first violation as a [`ValidationError`].
    ///
    /// This is a pure function — no I/O, no allocations beyond the
    /// rule-class set deduplication.
    pub fn validate(&self) -> Result<(), ValidationError> {
        // Property 4 — ADG presence
        if self.inputs.is_empty() {
            return Err(ValidationError::NoInputs);
        }
        if self.rules_applied.is_empty() {
            return Err(ValidationError::NoRulesApplied);
        }

        // Range checks on numeric outputs and inputs
        for input in &self.inputs {
            if input.confidence.is_nan() {
                return Err(ValidationError::InputConfidenceNaN);
            }
            if !(0.0..=1.0).contains(&input.confidence) {
                return Err(ValidationError::InputConfidenceOutOfRange(input.confidence));
            }
        }
        if self.output.confidence.is_nan() || !(0.0..=1.0).contains(&self.output.confidence) {
            return Err(ValidationError::OutputConfidenceInvalid(
                self.output.confidence,
            ));
        }
        if self.output.tier > 4 {
            return Err(ValidationError::TierOutOfRange(self.output.tier));
        }

        // Property 5 — rule-name → class consistency
        for ar in &self.rules_applied {
            let canonical = canonical_class_for_rule(ar.rule);
            if canonical != ar.class {
                return Err(ValidationError::RuleClassMismatch {
                    rule: ar.rule,
                    declared: ar.class,
                    canonical,
                });
            }
        }

        let class_set: BTreeSet<RuleClass> = self.rules_applied.iter().map(|ar| ar.class).collect();

        // Property 3 — epistemic determinism
        let expected_epistemic =
            epistemic_for_class_set(&class_set).ok_or(ValidationError::EmptyClassSet)?;
        if self.output.epistemic_status != expected_epistemic {
            return Err(ValidationError::EpistemicMismatch {
                declared: self.output.epistemic_status,
                expected: expected_epistemic,
            });
        }

        // Property 1 — confidence non-inflation
        let max_input = self
            .inputs
            .iter()
            .map(|i| i.confidence)
            .fold(f64::NEG_INFINITY, f64::max);
        if self.output.confidence > max_input {
            return Err(ValidationError::ConfidenceInflation {
                output: self.output.confidence,
                max_input,
            });
        }

        // Property 2 — tier ceiling
        let (ceiling, limiting_class) = class_set
            .iter()
            .map(|c| (max_tier_for_class(*c), *c))
            .min_by_key(|(t, _)| *t)
            .expect("class_set non-empty (checked above)");
        if self.output.tier > ceiling {
            return Err(ValidationError::TierInflation {
                output: self.output.tier,
                ceiling,
                limiting_class,
            });
        }

        // Binding status determinism
        let expected_binding = expected_binding_status(&self.inputs, &class_set);
        if self.output.binding_status != expected_binding {
            return Err(ValidationError::BindingStatusMismatch {
                declared: self.output.binding_status,
                expected: expected_binding,
            });
        }

        Ok(())
    }

    /// Returns the deduplicated rule-class set this derivation applied.
    /// Exposed for tests and downstream consumers (e.g. taudit) that want to
    /// reason about class composition without re-walking `rules_applied`.
    #[must_use]
    pub fn class_set(&self) -> BTreeSet<RuleClass> {
        self.rules_applied.iter().map(|ar| ar.class).collect()
    }
}

// ---------------------------------------------------------------------------
// Typed authority — the three variants
// ---------------------------------------------------------------------------

/// Authority derived from CellOS's own observation of bytes inside the
/// cell's network namespace.
///
/// **Permitted rule-classes:** `RawObservation`, `StructuralAlignment`,
/// `Opacity`. Tier ceiling: 2 (or up to 3 via `StructuralAlignment`, capped
/// at 2 by the type-class envelope per Authority Model §14.1).
///
/// **Forbidden rule-classes:** `CryptographicProof` (no signature was
/// verified), `ImposedInterception` (no termination occurred),
/// `GuestAgentDeclaration` (reserved for `DeclaredAuthority`).
///
/// **Type-system discipline:** No `From`/`Into` impls to or from any other
/// authority variant. The only public constructor is [`Self::try_new`].
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ObservedAuthority {
    derivation: AuthorityDerivation,
}

impl ObservedAuthority {
    /// Permitted rule-classes for this variant. Public so tests and
    /// downstream consumers can introspect the envelope.
    pub const PERMITTED_CLASSES: &'static [RuleClass] = &[
        RuleClass::RawObservation,
        RuleClass::StructuralAlignment,
        RuleClass::Opacity,
    ];

    /// Construct an `ObservedAuthority` from a validated derivation.
    ///
    /// Runs [`AuthorityDerivation::validate`] plus the type-class envelope
    /// check (no `CryptographicProof`, no `ImposedInterception`, no
    /// `GuestAgentDeclaration`).
    ///
    /// # Errors
    ///
    /// Returns the first [`ValidationError`] encountered.
    pub fn try_new(derivation: AuthorityDerivation) -> Result<Self, ValidationError> {
        derivation.validate()?;
        for class in derivation.class_set() {
            if !Self::PERMITTED_CLASSES.contains(&class) {
                return Err(ValidationError::TypeClassIncompatible {
                    type_name: "ObservedAuthority",
                    permitted: Self::PERMITTED_CLASSES,
                    rejected: class,
                });
            }
        }
        Ok(Self { derivation })
    }

    /// Read-only access to the validated derivation.
    #[must_use]
    pub fn derivation(&self) -> &AuthorityDerivation {
        &self.derivation
    }

    /// Convenience accessor for the output tuple.
    #[must_use]
    pub fn output(&self) -> AuthorityOutput {
        self.derivation.output
    }
}

/// Verified-signature artifact backing a [`ProvenAuthority`]. Storing the
/// artifact alongside the derivation prevents `ProvenAuthority` from being
/// constructed from a derivation that *claims* `CryptographicProof` without
/// actually carrying the proof material.
///
/// The verifier path that produced this artifact is upstream of
/// `cellos-core` (e.g., supervisor's JWS verifier, DNSSEC validator); this
/// crate holds no signing/verification keys per D11.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ProvenAuthorityArtifact {
    /// JWS authority claim verified against a workload-bound key.
    JwsVerified {
        /// `kid` of the key that verified the signature.
        kid: String,
    },
    /// Upstream cert chain matched a DNSSEC-validated TLSA record.
    DaneTlsaBound {
        /// TLSA record selector / matching-type the chain matched.
        tlsa_record_id: String,
    },
    /// SPIFFE SVID chain verified.
    SvidChainVerified {
        /// SPIFFE ID this chain bound to.
        spiffe_id: String,
    },
}

/// Authority derived from cryptographic verification of a claim CellOS did
/// not author.
///
/// **Permitted rule-classes:** `CryptographicProof` (REQUIRED — at least one
/// rule from this class must fire). `StructuralAlignment` and
/// `RawObservation` may co-occur (e.g. SNI parsed alongside a verified JWS),
/// but `Opacity` and `ImposedInterception` are forbidden.
///
/// **Forbidden:** `Opacity` (would invalidate the proof claim);
/// `ImposedInterception` (different stack); `GuestAgentDeclaration`.
///
/// **Constructor discipline:** [`Self::try_new`] requires a
/// [`ProvenAuthorityArtifact`]. Without the artifact, construction fails —
/// preventing a non-cryptographic emission from minting a `ProvenAuthority`
/// even if its derivation declares `CryptographicProof`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProvenAuthority {
    derivation: AuthorityDerivation,
    artifact: ProvenAuthorityArtifact,
}

impl ProvenAuthority {
    /// Permitted rule-classes for this variant.
    pub const PERMITTED_CLASSES: &'static [RuleClass] = &[
        RuleClass::CryptographicProof,
        RuleClass::StructuralAlignment,
        RuleClass::RawObservation,
    ];

    /// Construct a `ProvenAuthority`.
    ///
    /// Runs [`AuthorityDerivation::validate`] plus the type-class envelope
    /// check, and additionally requires:
    ///
    /// 1. At least one rule with class `CryptographicProof` (otherwise we
    ///    have nothing to prove).
    /// 2. A non-trivial [`ProvenAuthorityArtifact`].
    ///
    /// # Errors
    ///
    /// Returns the first [`ValidationError`] encountered.
    pub fn try_new(
        derivation: AuthorityDerivation,
        artifact: ProvenAuthorityArtifact,
    ) -> Result<Self, ValidationError> {
        derivation.validate()?;
        let class_set = derivation.class_set();
        if !class_set.contains(&RuleClass::CryptographicProof) {
            return Err(ValidationError::ProvenAuthorityMissingArtifact);
        }
        for class in &class_set {
            if !Self::PERMITTED_CLASSES.contains(class) {
                return Err(ValidationError::TypeClassIncompatible {
                    type_name: "ProvenAuthority",
                    permitted: Self::PERMITTED_CLASSES,
                    rejected: *class,
                });
            }
        }
        Ok(Self {
            derivation,
            artifact,
        })
    }

    /// Read-only access to the derivation.
    #[must_use]
    pub fn derivation(&self) -> &AuthorityDerivation {
        &self.derivation
    }

    /// Read-only access to the verification artifact.
    #[must_use]
    pub fn artifact(&self) -> &ProvenAuthorityArtifact {
        &self.artifact
    }

    /// Convenience accessor for the output tuple.
    #[must_use]
    pub fn output(&self) -> AuthorityOutput {
        self.derivation.output
    }
}

/// Authority asserted by CellOS having terminated TLS in-line and observed
/// plaintext that the workload would not have shown without termination.
///
/// **Sources:** Stack 3 / M2 only — lives architecturally in the quarantined
/// `cellos-tls-term` crate (post-1.0). The type lives in `cellos-core` so
/// downstream code can refer to it without depending on the quarantined
/// crate, but no `cellos-core` code path constructs one today.
///
/// **Permitted rule-classes:** `ImposedInterception` (REQUIRED).
/// Co-occurrence with other classes is permitted only insofar as the ADG
/// mapping demands `Imposed` epistemic status anyway.
///
/// **Doctrine:** `ImposedAuthority` MUST NOT be emitted on the
/// `cell.observability.v1.l7_authority_evidence` event type. It uses
/// `cell.observability.v1.l7_imposed_enforcement` exclusively, with the
/// `non_authoritative_enforcement: true` flag. This module enforces the
/// type-system half of that contract; the event-builder half lands in B3.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ImposedAuthority {
    derivation: AuthorityDerivation,
}

impl ImposedAuthority {
    /// Permitted rule-classes for this variant. `ImposedInterception` is
    /// REQUIRED to appear; co-occurrence with other classes is permitted but
    /// the deterministic mapping forces output epistemic status to `Imposed`.
    pub const PERMITTED_CLASSES: &'static [RuleClass] = &[
        RuleClass::ImposedInterception,
        RuleClass::RawObservation,
        RuleClass::StructuralAlignment,
    ];

    /// Construct an `ImposedAuthority`.
    ///
    /// Runs [`AuthorityDerivation::validate`] plus the type-class envelope
    /// check, and additionally requires the `ImposedInterception`
    /// rule-class to be present.
    ///
    /// # Errors
    ///
    /// Returns the first [`ValidationError`] encountered.
    pub fn try_new(derivation: AuthorityDerivation) -> Result<Self, ValidationError> {
        derivation.validate()?;
        let class_set = derivation.class_set();
        if !class_set.contains(&RuleClass::ImposedInterception) {
            return Err(ValidationError::ImposedAuthorityMissingInterception);
        }
        for class in &class_set {
            if !Self::PERMITTED_CLASSES.contains(class) {
                return Err(ValidationError::TypeClassIncompatible {
                    type_name: "ImposedAuthority",
                    permitted: Self::PERMITTED_CLASSES,
                    rejected: *class,
                });
            }
        }
        Ok(Self { derivation })
    }

    /// Read-only access to the derivation.
    #[must_use]
    pub fn derivation(&self) -> &AuthorityDerivation {
        &self.derivation
    }

    /// Convenience accessor for the output tuple.
    #[must_use]
    pub fn output(&self) -> AuthorityOutput {
        self.derivation.output
    }
}

/// Authority asserted by a guest-side declaration (in-VM `cellos-telemetry`
/// agent over the per-cell vsock channel) per [ADR-0006].
///
/// **Permitted rule-classes:** [`RuleClass::GuestAgentDeclaration`] only —
/// the envelope is mono-class per ADR-0006 §3. Any other class violates the
/// type-system gate that prevents guest declarations from being silently
/// upgraded to host-side epistemic statuses.
///
/// **Tier ceiling:** 1. **Epistemic status:** [`EpistemicStatus::Declared`].
/// **Binding status:** [`BindingStatus::NotApplicable`] for single-input
/// derivations; multi-input declarations remain [`BindingStatus::Unknown`]
/// because no host-side alignment/proof rule can fire alongside.
///
/// **Type-system discipline:** No `From`/`Into` impls to or from any other
/// authority variant. Construction goes only through [`Self::try_new`].
///
/// [ADR-0006]: ../../../../docs/adr/0006-in-vm-observability-runner-evidence.md
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DeclaredAuthority {
    derivation: AuthorityDerivation,
}

impl DeclaredAuthority {
    /// Permitted rule-classes for this variant. Mono-class envelope per
    /// ADR-0006 §3 — `GuestAgentDeclaration` is the only legal class.
    pub const PERMITTED_CLASSES: &'static [RuleClass] = &[RuleClass::GuestAgentDeclaration];

    /// Construct a `DeclaredAuthority`.
    ///
    /// Runs [`AuthorityDerivation::validate`] plus the mono-class envelope
    /// check (any class other than `GuestAgentDeclaration` is rejected with
    /// [`ValidationError::TypeClassIncompatible`]).
    ///
    /// # Errors
    ///
    /// Returns the first [`ValidationError`] encountered.
    pub fn try_new(derivation: AuthorityDerivation) -> Result<Self, ValidationError> {
        derivation.validate()?;
        for class in derivation.class_set() {
            if !Self::PERMITTED_CLASSES.contains(&class) {
                return Err(ValidationError::TypeClassIncompatible {
                    type_name: "DeclaredAuthority",
                    permitted: Self::PERMITTED_CLASSES,
                    rejected: class,
                });
            }
        }
        Ok(Self { derivation })
    }

    /// Read-only access to the validated derivation.
    #[must_use]
    pub fn derivation(&self) -> &AuthorityDerivation {
        &self.derivation
    }

    /// Convenience accessor for the output tuple.
    #[must_use]
    pub fn output(&self) -> AuthorityOutput {
        self.derivation.output
    }
}

/// Compile-fail gate for guest event-builders (ADR-0006 / O2 doctrine
/// red-line). Implementors declare their canonical [`RuleClass`] as an
/// associated constant; a `const`-asserted check in [`Self::rule_class`]
/// makes any class other than [`RuleClass::GuestAgentDeclaration`] a
/// **compilation error** at the call site.
///
/// This is the type-system half of the F-tier admission gate: a guest
/// event-builder cannot accidentally route through `RawObservation`
/// (Tier 2 / `DirectObservation`) and silently launder a guest declaration
/// into a host-side epistemic status.
///
/// # Compile-fail example
///
/// Pointing `RULE_CLASS` at a non-guest class fails to compile when
/// `rule_class()` is invoked:
///
/// ```compile_fail
/// use cellos_core::authority::{GuestEventBuilder, RuleClass};
///
/// struct BadBuilder;
/// impl GuestEventBuilder for BadBuilder {
///     const RULE_CLASS: RuleClass = RuleClass::RawObservation;
/// }
///
/// let _ = BadBuilder.rule_class();
/// ```
pub trait GuestEventBuilder {
    /// The canonical [`RuleClass`] this builder emits. **MUST** be
    /// [`RuleClass::GuestAgentDeclaration`].
    const RULE_CLASS: RuleClass;

    /// Returns [`Self::RULE_CLASS`]. The default implementation is a
    /// `const`-asserted compile-time check that the class is
    /// `GuestAgentDeclaration`; overriding is allowed but the assertion
    /// still fires on any path that calls the trait method.
    fn rule_class(&self) -> RuleClass {
        const {
            assert!(
                matches!(Self::RULE_CLASS, RuleClass::GuestAgentDeclaration),
                "GuestEventBuilder::RULE_CLASS must be RuleClass::GuestAgentDeclaration",
            );
        }
        Self::RULE_CLASS
    }
}

// ---------------------------------------------------------------------------
// Doctrine red-line: D9 — no Into/From between types.
// ---------------------------------------------------------------------------
//
// The doctrine gate D9 (mechanical separation, Authority Model §14 +
// ADR-0006 §1) requires that none of the four typed-authority variants
// (`ObservedAuthority`, `ProvenAuthority`, `ImposedAuthority`,
// `DeclaredAuthority`) can be converted into another. Rust's coherence
// rules enforce this trivially: we simply do NOT implement
// `From<ObservedAuthority> for DeclaredAuthority` and so on.
//
// `tests::no_cross_type_conversion_compiles` (in the sibling `tests` module)
// does the soft confirmation: trait-object-style `is::<TypeA>()` checks on
// stored values plus an `assert_not_impl_all!`-style negative trait test.
// A genuine compile-fail test would require `trybuild`, which is not in our
// dev-dep set; the §14 contract is upheld by review discipline plus the
// run-time test that asserts the four types are distinct via `TypeId`.