Skip to main content

cortex_runtime/
claims.rs

1//! Runtime claim compiler for truth-ceiling enforcement.
2
3use cortex_core::{
4    AuthorityClass, ClaimCeiling, ClaimProofState, PolicyDecision, PolicyOutcome, ReportableClaim,
5    RuntimeMode,
6};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10/// Runtime claim class with a minimum authority ceiling.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum RuntimeClaimKind {
14    /// Diagnostic claim about a local failure or unknown.
15    Diagnostic,
16    /// Advisory local claim.
17    Advisory,
18    /// Trusted run-history claim.
19    TrustedHistory,
20    /// Export authority claim.
21    Export,
22    /// Promotion or durable authority claim.
23    Promotion,
24    /// Release-readiness claim.
25    ReleaseReadiness,
26    /// Compliance evidence claim.
27    ComplianceEvidence,
28    /// Cross-system trust decision claim.
29    CrossSystemTrust,
30}
31
32impl RuntimeClaimKind {
33    /// Minimum effective ceiling needed to emit this claim affirmatively.
34    #[must_use]
35    pub const fn required_ceiling(self) -> ClaimCeiling {
36        match self {
37            Self::Diagnostic | Self::Advisory => ClaimCeiling::DevOnly,
38            Self::TrustedHistory => ClaimCeiling::SignedLocalLedger,
39            Self::Export => ClaimCeiling::ExternallyAnchored,
40            Self::Promotion
41            | Self::ReleaseReadiness
42            | Self::ComplianceEvidence
43            | Self::CrossSystemTrust => ClaimCeiling::AuthorityGrade,
44        }
45    }
46}
47
48/// Compiled runtime claim with weakest-link ceiling and allow/deny decision.
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct CompiledRuntimeClaim {
51    /// Claim text.
52    pub claim: String,
53    /// Runtime claim kind.
54    pub kind: RuntimeClaimKind,
55    /// Runtime mode supplied by the caller.
56    pub runtime_mode: RuntimeMode,
57    /// Authority class supplied by the caller.
58    pub authority_class: AuthorityClass,
59    /// Proof state supplied by proof closure.
60    pub proof_state: ClaimProofState,
61    /// Requested claim ceiling.
62    pub requested_ceiling: ClaimCeiling,
63    /// Effective weakest-link ceiling.
64    pub effective_ceiling: ClaimCeiling,
65    /// Minimum ceiling for this claim kind.
66    pub required_ceiling: ClaimCeiling,
67    /// Whether the claim may be emitted affirmatively.
68    pub allowed: bool,
69    /// Downgrade or denial reasons.
70    pub reasons: Vec<String>,
71}
72
73/// Use requested for development-ledger data.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(rename_all = "snake_case")]
76pub enum DevelopmentLedgerUse {
77    /// Local diagnostic inspection by the operator.
78    LocalDiagnostic,
79    /// Audit export artifact.
80    AuditExport,
81    /// Compliance evidence generation.
82    ComplianceEvidence,
83    /// Cross-system trust decision.
84    CrossSystemTrustDecision,
85    /// External reporting.
86    ExternalReporting,
87}
88
89impl DevelopmentLedgerUse {
90    /// Stable payload string for this use.
91    #[must_use]
92    pub const fn wire_str(self) -> &'static str {
93        match self {
94            Self::LocalDiagnostic => "local_diagnostic",
95            Self::AuditExport => "audit_export",
96            Self::ComplianceEvidence => "compliance_evidence",
97            Self::CrossSystemTrustDecision => "cross_system_trust_decision",
98            Self::ExternalReporting => "external_reporting",
99        }
100    }
101}
102
103/// Decision for using one event payload in a requested authority surface.
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub struct DevelopmentLedgerUseDecision {
106    /// Requested use.
107    pub requested_use: DevelopmentLedgerUse,
108    /// Whether this event payload may be used for that surface.
109    pub allowed: bool,
110    /// Decision reason.
111    pub reason: String,
112}
113
114/// Fail-closed decision for a proof-bearing runtime claim surface.
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116pub struct RuntimeClaimPreflight {
117    /// Compiled claim evidence and weakest-link ceiling.
118    pub claim: CompiledRuntimeClaim,
119    /// Whether the requested surface may proceed.
120    pub allowed: bool,
121    /// Operator-facing decision reason.
122    pub reason: String,
123}
124
125/// Decides whether a run-event payload may be used for the requested surface.
126#[must_use]
127pub fn development_ledger_use_decision(
128    payload: &Value,
129    requested_use: DevelopmentLedgerUse,
130) -> DevelopmentLedgerUseDecision {
131    let ledger_authority = payload.get("ledger_authority").and_then(Value::as_str);
132    let signed_ledger_authority = payload
133        .get("signed_ledger_authority")
134        .and_then(Value::as_bool);
135    let forbidden = payload
136        .get("forbidden_uses")
137        .and_then(Value::as_array)
138        .is_some_and(|uses| {
139            uses.iter()
140                .any(|value| value.as_str() == Some(requested_use.wire_str()))
141        });
142    if requested_use == DevelopmentLedgerUse::LocalDiagnostic {
143        return DevelopmentLedgerUseDecision {
144            requested_use,
145            allowed: true,
146            reason: "local diagnostics do not upgrade development-ledger authority".into(),
147        };
148    }
149
150    if forbidden {
151        DevelopmentLedgerUseDecision {
152            requested_use,
153            allowed: false,
154            reason: format!("ledger data is forbidden for {}", requested_use.wire_str()),
155        }
156    } else if ledger_authority == Some("development") || signed_ledger_authority == Some(false) {
157        DevelopmentLedgerUseDecision {
158            requested_use,
159            allowed: false,
160            reason: format!(
161                "development-ledger data is forbidden for {}",
162                requested_use.wire_str()
163            ),
164        }
165    } else if ledger_authority == Some("signed_local")
166        || (signed_ledger_authority == Some(true)
167            && !matches!(
168                ledger_authority,
169                Some("externally_anchored" | "authority_grade")
170            ))
171    {
172        DevelopmentLedgerUseDecision {
173            requested_use,
174            allowed: false,
175            reason: format!(
176                "signed-local ledger data is forbidden for {} without external authority",
177                requested_use.wire_str()
178            ),
179        }
180    } else {
181        DevelopmentLedgerUseDecision {
182            requested_use,
183            allowed: true,
184            reason: "payload does not declare this requested use as forbidden".into(),
185        }
186    }
187}
188
189/// Compile a runtime claim and decide whether it may be emitted affirmatively.
190#[must_use]
191pub fn compile_runtime_claim(
192    claim: impl Into<String>,
193    kind: RuntimeClaimKind,
194    runtime_mode: RuntimeMode,
195    authority_class: AuthorityClass,
196    proof_state: ClaimProofState,
197    requested_ceiling: ClaimCeiling,
198) -> CompiledRuntimeClaim {
199    let reportable = ReportableClaim::new(
200        claim,
201        runtime_mode,
202        authority_class,
203        proof_state,
204        requested_ceiling,
205    );
206    let required_ceiling = kind.required_ceiling();
207    let effective_ceiling = reportable.effective_ceiling();
208    let allowed = effective_ceiling >= required_ceiling;
209    let mut reasons = reportable.downgrade_reasons().to_vec();
210    if !allowed {
211        reasons.push(format!(
212            "{kind:?} requires {required_ceiling:?}, but effective ceiling is {effective_ceiling:?}"
213        ));
214    }
215
216    CompiledRuntimeClaim {
217        claim: reportable.claim().to_string(),
218        kind,
219        runtime_mode: reportable.runtime_mode(),
220        authority_class: reportable.authority_class(),
221        proof_state: reportable.proof_state(),
222        requested_ceiling: reportable.requested_ceiling(),
223        effective_ceiling,
224        required_ceiling,
225        allowed,
226        reasons,
227    }
228}
229
230/// Preflight a proof-bearing runtime claim before a command emits, exports, or
231/// persists an authority-bearing result.
232#[must_use]
233pub fn runtime_claim_preflight(
234    claim: impl Into<String>,
235    kind: RuntimeClaimKind,
236    runtime_mode: RuntimeMode,
237    authority_class: AuthorityClass,
238    proof_state: ClaimProofState,
239    requested_ceiling: ClaimCeiling,
240) -> RuntimeClaimPreflight {
241    let compiled = compile_runtime_claim(
242        claim,
243        kind,
244        runtime_mode,
245        authority_class,
246        proof_state,
247        requested_ceiling,
248    );
249    let reason = if compiled.allowed {
250        let effective_ceiling = compiled.effective_ceiling;
251        format!("{kind:?} allowed at effective ceiling {effective_ceiling:?}")
252    } else {
253        compiled.reasons.last().cloned().unwrap_or_else(|| {
254            let effective_ceiling = compiled.effective_ceiling;
255            let required_ceiling = compiled.required_ceiling;
256            format!("{kind:?} denied because effective ceiling {effective_ceiling:?} is below required {required_ceiling:?}")
257        })
258    };
259
260    RuntimeClaimPreflight {
261        allowed: compiled.allowed,
262        claim: compiled,
263        reason,
264    }
265}
266
267/// Preflight a proof-bearing runtime claim and include an ADR 0026 policy
268/// decision in the ceiling computation.
269#[must_use]
270pub fn runtime_claim_preflight_with_policy(
271    claim: impl Into<String>,
272    kind: RuntimeClaimKind,
273    runtime_mode: RuntimeMode,
274    authority_class: AuthorityClass,
275    proof_state: ClaimProofState,
276    requested_ceiling: ClaimCeiling,
277    policy: &PolicyDecision,
278) -> RuntimeClaimPreflight {
279    let mut preflight = runtime_claim_preflight(
280        claim,
281        kind,
282        runtime_mode,
283        authority_class,
284        proof_state,
285        requested_ceiling,
286    );
287    let policy_ceiling = policy.final_outcome.claim_ceiling();
288    if policy_ceiling < preflight.claim.effective_ceiling {
289        preflight.claim.effective_ceiling = policy_ceiling;
290        let final_outcome = policy.final_outcome;
291        preflight.claim.reasons.push(format!(
292            "policy outcome {final_outcome:?} limits authority claims"
293        ));
294    }
295    if matches!(
296        policy.final_outcome,
297        PolicyOutcome::Reject | PolicyOutcome::Quarantine
298    ) {
299        preflight.allowed = false;
300        let final_outcome = policy.final_outcome;
301        preflight.reason = format!("policy outcome {final_outcome:?} fails closed for {kind:?}");
302    } else {
303        preflight.allowed = preflight.claim.effective_ceiling >= preflight.claim.required_ceiling;
304        if !preflight.allowed {
305            let required_ceiling = preflight.claim.required_ceiling;
306            let effective_ceiling = preflight.claim.effective_ceiling;
307            preflight.reason = format!(
308                "{kind:?} requires {required_ceiling:?}, but effective ceiling is {effective_ceiling:?}"
309            );
310        }
311    }
312    preflight
313}
314
315/// Return an error message when a proof-bearing claim cannot satisfy its
316/// requested authority surface.
317pub fn require_runtime_claim(
318    claim: impl Into<String>,
319    kind: RuntimeClaimKind,
320    runtime_mode: RuntimeMode,
321    authority_class: AuthorityClass,
322    proof_state: ClaimProofState,
323    requested_ceiling: ClaimCeiling,
324) -> Result<RuntimeClaimPreflight, RuntimeClaimPreflight> {
325    let preflight = runtime_claim_preflight(
326        claim,
327        kind,
328        runtime_mode,
329        authority_class,
330        proof_state,
331        requested_ceiling,
332    );
333    if preflight.allowed {
334        Ok(preflight)
335    } else {
336        Err(preflight)
337    }
338}
339
340/// Return an error when policy or proof/runtime ceilings cannot satisfy the
341/// requested authority surface.
342pub fn require_runtime_claim_with_policy(
343    claim: impl Into<String>,
344    kind: RuntimeClaimKind,
345    runtime_mode: RuntimeMode,
346    authority_class: AuthorityClass,
347    proof_state: ClaimProofState,
348    requested_ceiling: ClaimCeiling,
349    policy: &PolicyDecision,
350) -> Result<RuntimeClaimPreflight, RuntimeClaimPreflight> {
351    let preflight = runtime_claim_preflight_with_policy(
352        claim,
353        kind,
354        runtime_mode,
355        authority_class,
356        proof_state,
357        requested_ceiling,
358        policy,
359    );
360    if preflight.allowed {
361        Ok(preflight)
362    } else {
363        Err(preflight)
364    }
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    #[test]
372    fn dev_fixture_cannot_emit_release_claim() {
373        let claim = compile_runtime_claim(
374            "ready for release",
375            RuntimeClaimKind::ReleaseReadiness,
376            RuntimeMode::Dev,
377            AuthorityClass::Operator,
378            ClaimProofState::FullChainVerified,
379            ClaimCeiling::AuthorityGrade,
380        );
381
382        assert!(!claim.allowed);
383        assert_eq!(claim.effective_ceiling, ClaimCeiling::DevOnly);
384        assert!(claim
385            .reasons
386            .iter()
387            .any(|reason| reason.contains("ReleaseReadiness requires AuthorityGrade")));
388    }
389
390    #[test]
391    fn local_unsigned_run_cannot_emit_trusted_history() {
392        let claim = compile_runtime_claim(
393            "trusted run history",
394            RuntimeClaimKind::TrustedHistory,
395            RuntimeMode::LocalUnsigned,
396            AuthorityClass::Operator,
397            ClaimProofState::FullChainVerified,
398            ClaimCeiling::AuthorityGrade,
399        );
400
401        assert!(!claim.allowed);
402        assert_eq!(claim.effective_ceiling, ClaimCeiling::LocalUnsigned);
403    }
404
405    #[test]
406    fn unknown_runtime_cannot_emit_authority_claims() {
407        for kind in [
408            RuntimeClaimKind::Promotion,
409            RuntimeClaimKind::TrustedHistory,
410            RuntimeClaimKind::Export,
411            RuntimeClaimKind::ComplianceEvidence,
412            RuntimeClaimKind::ReleaseReadiness,
413            RuntimeClaimKind::CrossSystemTrust,
414        ] {
415            let claim = compile_runtime_claim(
416                "authority claim",
417                kind,
418                RuntimeMode::Unknown,
419                AuthorityClass::Operator,
420                ClaimProofState::FullChainVerified,
421                ClaimCeiling::AuthorityGrade,
422            );
423
424            assert!(!claim.allowed, "{kind:?} must fail in unknown runtime");
425            assert_eq!(claim.effective_ceiling, ClaimCeiling::DevOnly);
426        }
427    }
428
429    #[test]
430    fn signed_full_chain_can_emit_trusted_history() {
431        let claim = compile_runtime_claim(
432            "trusted run history",
433            RuntimeClaimKind::TrustedHistory,
434            RuntimeMode::SignedLocalLedger,
435            AuthorityClass::Verified,
436            ClaimProofState::FullChainVerified,
437            ClaimCeiling::SignedLocalLedger,
438        );
439
440        assert!(claim.allowed);
441        assert_eq!(claim.effective_ceiling, ClaimCeiling::SignedLocalLedger);
442    }
443
444    #[test]
445    fn preflight_fails_closed_for_authority_surfaces_above_ceiling() {
446        for kind in [
447            RuntimeClaimKind::ReleaseReadiness,
448            RuntimeClaimKind::Export,
449            RuntimeClaimKind::Promotion,
450            RuntimeClaimKind::ComplianceEvidence,
451        ] {
452            let preflight = runtime_claim_preflight(
453                "authority surface",
454                kind,
455                RuntimeMode::LocalUnsigned,
456                AuthorityClass::Observed,
457                ClaimProofState::Partial,
458                ClaimCeiling::AuthorityGrade,
459            );
460
461            assert!(!preflight.allowed, "{kind:?} must fail closed");
462            assert_eq!(
463                preflight.claim.effective_ceiling,
464                ClaimCeiling::LocalUnsigned
465            );
466            assert!(
467                preflight.reason.contains("requires")
468                    || preflight.reason.contains("below required"),
469                "preflight reason should explain the ceiling failure"
470            );
471        }
472    }
473
474    #[test]
475    fn preflight_allows_promotion_only_at_authority_grade() {
476        let preflight = runtime_claim_preflight(
477            "attested doctrine promotion",
478            RuntimeClaimKind::Promotion,
479            RuntimeMode::AuthorityGrade,
480            AuthorityClass::Operator,
481            ClaimProofState::FullChainVerified,
482            ClaimCeiling::AuthorityGrade,
483        );
484
485        assert!(preflight.allowed);
486        assert_eq!(
487            preflight.claim.effective_ceiling,
488            ClaimCeiling::AuthorityGrade
489        );
490    }
491
492    #[test]
493    fn policy_reject_and_quarantine_downgrade_claim_preflight() {
494        for outcome in [PolicyOutcome::Reject, PolicyOutcome::Quarantine] {
495            let policy = PolicyDecision {
496                final_outcome: outcome,
497                contributing: Vec::new(),
498                discarded: Vec::new(),
499                break_glass: None,
500            };
501            let preflight = runtime_claim_preflight_with_policy(
502                "trusted export",
503                RuntimeClaimKind::Export,
504                RuntimeMode::ExternallyAnchored,
505                AuthorityClass::Operator,
506                ClaimProofState::FullChainVerified,
507                ClaimCeiling::ExternallyAnchored,
508                &policy,
509            );
510
511            assert!(!preflight.allowed);
512            assert_eq!(preflight.claim.effective_ceiling, ClaimCeiling::DevOnly);
513            assert!(preflight.reason.contains("policy outcome"));
514        }
515    }
516
517    #[test]
518    fn policy_warn_does_not_soften_ceiling_failure() {
519        let policy = PolicyDecision {
520            final_outcome: PolicyOutcome::Warn,
521            contributing: Vec::new(),
522            discarded: Vec::new(),
523            break_glass: None,
524        };
525        let preflight = runtime_claim_preflight_with_policy(
526            "trusted history",
527            RuntimeClaimKind::TrustedHistory,
528            RuntimeMode::LocalUnsigned,
529            AuthorityClass::Observed,
530            ClaimProofState::Partial,
531            ClaimCeiling::AuthorityGrade,
532            &policy,
533        );
534
535        assert!(!preflight.allowed);
536        assert_eq!(
537            preflight.claim.effective_ceiling,
538            ClaimCeiling::LocalUnsigned
539        );
540    }
541
542    #[test]
543    fn development_ledger_denies_all_forbidden_external_uses() {
544        let payload = serde_json::json!({
545            "ledger_authority": "development",
546            "signed_ledger_authority": false,
547            "forbidden_uses": [
548                "audit_export",
549                "compliance_evidence",
550                "cross_system_trust_decision",
551                "external_reporting"
552            ]
553        });
554
555        for requested_use in [
556            DevelopmentLedgerUse::AuditExport,
557            DevelopmentLedgerUse::ComplianceEvidence,
558            DevelopmentLedgerUse::CrossSystemTrustDecision,
559            DevelopmentLedgerUse::ExternalReporting,
560        ] {
561            let decision = development_ledger_use_decision(&payload, requested_use);
562            assert!(
563                !decision.allowed,
564                "{requested_use:?} should be forbidden for development ledger"
565            );
566        }
567    }
568
569    #[test]
570    fn development_ledger_allows_local_diagnostic_only() {
571        let payload = serde_json::json!({
572            "ledger_authority": "development",
573            "signed_ledger_authority": false,
574            "forbidden_uses": ["audit_export"]
575        });
576
577        let decision =
578            development_ledger_use_decision(&payload, DevelopmentLedgerUse::LocalDiagnostic);
579
580        assert!(decision.allowed);
581        assert!(decision.reason.contains("do not upgrade"));
582    }
583
584    #[test]
585    fn externally_anchored_payload_is_not_blocked_by_ledger_gate() {
586        let payload = serde_json::json!({
587            "ledger_authority": "externally_anchored",
588            "signed_ledger_authority": true,
589            "forbidden_uses": []
590        });
591
592        let decision = development_ledger_use_decision(&payload, DevelopmentLedgerUse::AuditExport);
593
594        assert!(decision.allowed);
595    }
596
597    #[test]
598    fn signed_local_payload_still_honors_explicit_forbidden_uses() {
599        let payload = serde_json::json!({
600            "ledger_authority": "signed_local",
601            "signed_ledger_authority": true,
602            "trusted_run_history": true,
603            "forbidden_uses": ["audit_export", "compliance_evidence"]
604        });
605
606        let audit = development_ledger_use_decision(&payload, DevelopmentLedgerUse::AuditExport);
607        let compliance =
608            development_ledger_use_decision(&payload, DevelopmentLedgerUse::ComplianceEvidence);
609        let local =
610            development_ledger_use_decision(&payload, DevelopmentLedgerUse::LocalDiagnostic);
611
612        assert!(!audit.allowed);
613        assert!(!compliance.allowed);
614        assert!(local.allowed);
615    }
616
617    #[test]
618    fn signed_local_payload_cannot_be_used_for_external_surfaces_without_forbidden_uses() {
619        let payload = serde_json::json!({
620            "ledger_authority": "signed_local",
621            "signed_ledger_authority": true,
622            "trusted_run_history": true
623        });
624
625        for requested_use in [
626            DevelopmentLedgerUse::AuditExport,
627            DevelopmentLedgerUse::ComplianceEvidence,
628            DevelopmentLedgerUse::CrossSystemTrustDecision,
629            DevelopmentLedgerUse::ExternalReporting,
630        ] {
631            let decision = development_ledger_use_decision(&payload, requested_use);
632            assert!(
633                !decision.allowed,
634                "{requested_use:?} should be forbidden for signed-local ledger"
635            );
636            assert!(
637                decision.reason.contains("signed-local ledger"),
638                "reason should name signed-local boundary: {}",
639                decision.reason
640            );
641        }
642    }
643}