1use cortex_core::{
4 AuthorityClass, ClaimCeiling, ClaimProofState, PolicyDecision, PolicyOutcome, ReportableClaim,
5 RuntimeMode,
6};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum RuntimeClaimKind {
14 Diagnostic,
16 Advisory,
18 TrustedHistory,
20 Export,
22 Promotion,
24 ReleaseReadiness,
26 ComplianceEvidence,
28 CrossSystemTrust,
30}
31
32impl RuntimeClaimKind {
33 #[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct CompiledRuntimeClaim {
51 pub claim: String,
53 pub kind: RuntimeClaimKind,
55 pub runtime_mode: RuntimeMode,
57 pub authority_class: AuthorityClass,
59 pub proof_state: ClaimProofState,
61 pub requested_ceiling: ClaimCeiling,
63 pub effective_ceiling: ClaimCeiling,
65 pub required_ceiling: ClaimCeiling,
67 pub allowed: bool,
69 pub reasons: Vec<String>,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(rename_all = "snake_case")]
76pub enum DevelopmentLedgerUse {
77 LocalDiagnostic,
79 AuditExport,
81 ComplianceEvidence,
83 CrossSystemTrustDecision,
85 ExternalReporting,
87}
88
89impl DevelopmentLedgerUse {
90 #[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub struct DevelopmentLedgerUseDecision {
106 pub requested_use: DevelopmentLedgerUse,
108 pub allowed: bool,
110 pub reason: String,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116pub struct RuntimeClaimPreflight {
117 pub claim: CompiledRuntimeClaim,
119 pub allowed: bool,
121 pub reason: String,
123}
124
125#[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#[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#[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#[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
315pub 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
340pub 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}