1use std::collections::{BTreeMap, BTreeSet};
10
11use serde::{Deserialize, Serialize, Serializer, ser::SerializeStruct};
12use serde_json::Value;
13use sha2::{Digest, Sha256};
14use thiserror::Error;
15
16use crate::event::{
17 Event, EventError, EventKind, EventStore, SessionReplacementRecord, history_sha256,
18};
19use crate::genui::{
20 ActionCatalog, Component, ComponentKind, EvidenceItem, GenUiError, StatusLevel, Surface,
21 TimelineEntry, TimelineState, default_negotiated_capabilities,
22 render_surface_with_capabilities_and_catalog_with_store, sanitize_text,
23};
24use crate::session::{Session, SessionError, SessionState};
25
26const MAX_DISPLAY_EVIDENCE: usize = 100;
27const MAX_EVIDENCE_ITEMS: usize = 100_000;
28const MAX_EVIDENCE_VALUE_BYTES: usize = 4_096;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum AuthorityClass {
33 AuthoritativeFalseGreenState,
34 AuthoritativeAgentSessionState,
35 DiagnosticNonAuthoritativeMetadata,
36}
37
38impl AuthorityClass {
39 #[must_use]
40 pub const fn label(self) -> &'static str {
41 match self {
42 Self::AuthoritativeFalseGreenState => "authoritative FalseGreen state",
43 Self::AuthoritativeAgentSessionState => "authoritative Agent/session state",
44 Self::DiagnosticNonAuthoritativeMetadata => "diagnostic/non-authoritative metadata",
45 }
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
52#[non_exhaustive]
53pub struct NativeField {
54 value: Option<String>,
55 authority: AuthorityClass,
56}
57
58impl Serialize for NativeField {
59 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
60 where
61 S: Serializer,
62 {
63 let mut state = serializer.serialize_struct("NativeField", 2)?;
64 state.serialize_field("value", &self.value)?;
65 state.serialize_field("authority", &self.authority)?;
66 state.end()
67 }
68}
69
70impl NativeField {
71 #[must_use]
72 pub(crate) fn falsegreen(value: impl Into<String>) -> Self {
73 let value = value.into();
74 Self {
75 value: Some(safe_text(&value)),
76 authority: AuthorityClass::AuthoritativeFalseGreenState,
77 }
78 }
79
80 #[must_use]
81 pub(crate) fn agent(value: impl Into<String>) -> Self {
82 let value = value.into();
83 Self {
84 value: Some(safe_text(&value)),
85 authority: AuthorityClass::AuthoritativeAgentSessionState,
86 }
87 }
88
89 #[must_use]
90 pub(crate) fn diagnostic(value: impl Into<String>) -> Self {
91 let value = value.into();
92 Self {
93 value: Some(safe_text(&value)),
94 authority: AuthorityClass::DiagnosticNonAuthoritativeMetadata,
95 }
96 }
97
98 #[must_use]
99 pub(crate) fn unavailable(authority: AuthorityClass) -> Self {
100 Self {
101 value: None,
102 authority,
103 }
104 }
105
106 #[must_use]
107 pub fn display(&self, unknown: bool) -> String {
108 sanitize_text(self.value.as_deref().unwrap_or(if unknown {
109 "Unknown"
110 } else {
111 "Unavailable"
112 }))
113 }
114
115 #[must_use]
116 pub fn value(&self) -> Option<&str> {
117 self.value.as_deref()
118 }
119
120 #[must_use]
121 pub const fn authority(&self) -> AuthorityClass {
122 self.authority
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
128pub enum NativeStatus {
129 Accepted,
130 Failed,
131 InsufficientEvidence,
132 Queued,
133 Claimed,
134 Running,
135 Executing,
136 Terminal,
137 Recoverable,
138 Unknown,
139}
140
141impl NativeStatus {
142 #[must_use]
143 pub const fn label(self) -> &'static str {
144 match self {
145 Self::Accepted => "ACCEPTED",
146 Self::Failed => "FAILED",
147 Self::InsufficientEvidence => "INSUFFICIENT EVIDENCE",
148 Self::Queued => "QUEUED",
149 Self::Claimed => "CLAIMED",
150 Self::Running => "RUNNING",
151 Self::Executing => "EXECUTING",
152 Self::Terminal => "TERMINAL",
153 Self::Recoverable => "RECOVERABLE",
154 Self::Unknown => "UNKNOWN",
155 }
156 }
157
158 #[must_use]
159 const fn level(self) -> StatusLevel {
160 match self {
161 Self::Accepted => StatusLevel::Success,
162 Self::Failed => StatusLevel::Error,
163 Self::InsufficientEvidence | Self::Recoverable | Self::Unknown => StatusLevel::Warning,
164 _ => StatusLevel::Info,
165 }
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
170#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
171pub enum NativeVerdict {
172 Accepted,
173 Failed,
174 InsufficientEvidence,
175 Incomplete,
176 Invalid,
177 Unknown,
178}
179
180impl NativeVerdict {
181 #[must_use]
182 pub const fn label(self) -> &'static str {
183 match self {
184 Self::Accepted => "ACCEPTED",
185 Self::Failed => "FAILED",
186 Self::InsufficientEvidence => "INSUFFICIENT EVIDENCE",
187 Self::Incomplete => "INCOMPLETE",
188 Self::Invalid => "INVALID",
189 Self::Unknown => "UNKNOWN",
190 }
191 }
192
193 #[must_use]
194 const fn status(self) -> NativeStatus {
195 match self {
196 Self::Accepted => NativeStatus::Accepted,
197 Self::Failed => NativeStatus::Failed,
198 Self::InsufficientEvidence => NativeStatus::InsufficientEvidence,
199 Self::Incomplete | Self::Invalid | Self::Unknown => NativeStatus::Unknown,
200 }
201 }
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
205#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
206pub enum EvidenceState {
207 Passed,
208 Failed,
209 Insufficient,
210 Unknown,
211}
212
213impl EvidenceState {
214 #[must_use]
215 const fn label(self) -> &'static str {
216 match self {
217 Self::Passed => "PASS",
218 Self::Failed => "FAIL",
219 Self::Insufficient => "INSUFFICIENT",
220 Self::Unknown => "UNKNOWN",
221 }
222 }
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
226pub struct NativeSnapshotIdentity {
227 pub source_session_id: String,
228 pub state_generation: u64,
229 pub digest: String,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
233pub struct NativeTask {
234 pub task_id: NativeField,
235 pub lifecycle: NativeField,
236 pub frozen: NativeField,
237 pub scope_digest: NativeField,
238 pub scope_summary: NativeField,
239 pub source_sha256: NativeField,
240 pub candidate_sha256: NativeField,
241 pub current_job_id: NativeField,
242}
243
244#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
245pub struct NativeScopeObligation {
246 pub id: NativeField,
247 pub requirement: NativeField,
248}
249
250#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
251pub struct NativeFrozenScope {
252 pub scope_digest: NativeField,
253 pub frozen: NativeField,
254 pub obligations: Vec<NativeScopeObligation>,
255 pub distinction: String,
256}
257
258#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
259pub struct NativeJob {
260 pub id: NativeField,
261 pub status: NativeStatus,
262 pub status_raw: NativeField,
263 pub task_id: NativeField,
264 pub source_sha256: NativeField,
265 pub candidate_sha256: NativeField,
266 pub entitlement_consumption: NativeField,
267 pub run_ids: Vec<NativeField>,
268 pub authority_decision: NativeField,
269}
270
271#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
272pub struct NativeRun {
273 pub id: NativeField,
274 pub worker_id: NativeField,
275 pub job_id: NativeField,
276 pub assignment_generation: NativeField,
277 pub status: NativeStatus,
278 pub status_raw: NativeField,
279 pub execution_started_at: NativeField,
280 pub package_runtime_identity: NativeField,
281 pub recovery_lease: NativeField,
282}
283
284#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
285pub struct NativeObligationOutcome {
286 pub obligation_id: NativeField,
287 pub outcome: EvidenceState,
288 pub detail: NativeField,
289 pub evidence_count: usize,
290}
291
292#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
293pub struct NativeVerification {
294 pub verdict: NativeVerdict,
295 pub raw_verdict: NativeField,
296 pub source_bound: bool,
297 pub candidate_sha256: NativeField,
298 pub authoritative_source_sha256: NativeField,
299 pub result_digest: NativeField,
300 pub report_reference: NativeField,
301 pub completion_authority: NativeField,
302 pub obligation_outcomes: Vec<NativeObligationOutcome>,
303 pub certification_note: String,
304 pub binding_error: Option<String>,
305}
306
307#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
308pub struct NativeEvidenceItem {
309 pub evidence_id: NativeField,
310 pub obligation: NativeField,
311 pub source: NativeField,
312 pub evidence_type: NativeField,
313 pub reference: NativeField,
314 pub state: EvidenceState,
315 pub detail: NativeField,
316}
317
318#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
319pub struct NativeEvidence {
320 pub items: Vec<NativeEvidenceItem>,
321 pub complete: bool,
322 pub duplicate_ids: Vec<String>,
323 pub unavailable_reason: Option<String>,
324 pub total: usize,
325 pub passed: usize,
326 pub failed: usize,
327 pub insufficient: usize,
328 pub unknown: usize,
329 pub observed_rows: usize,
330 pub invalid_rows: usize,
331 pub declared_rows: Option<usize>,
332}
333
334#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
335pub struct NativeReport {
336 pub id: NativeField,
337 pub report_digest: NativeField,
338 pub candidate_sha256: NativeField,
339 pub source_sha256: NativeField,
340 pub verdict: NativeVerdict,
341 pub key_evidence_summary: NativeField,
342 pub created_at: NativeField,
343 pub provenance: NativeField,
344}
345
346#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
347#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
348pub enum RecoveryState {
349 NotApplicable,
350 Recoverable,
351 Blocked,
352 Terminal,
353 Unknown,
354}
355
356#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
357pub struct NativeRecovery {
358 pub state: RecoveryState,
359 pub reason: NativeField,
360 pub pre_begin_claim: NativeField,
361 pub action_available: bool,
362 pub distinction: String,
363}
364
365#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
366pub struct NativeReplacement {
367 pub predecessor_session: NativeField,
368 pub replacement_session: NativeField,
369 pub predecessor_state: NativeField,
370 pub continuity: NativeField,
371 pub candidate_sha256: NativeField,
372 pub workspace_state: NativeField,
373 pub terminal_outcome: NativeField,
374}
375
376#[derive(Debug, Clone, PartialEq, Eq)]
379#[non_exhaustive]
380pub struct AuthoritativeTaskView {
381 task_id: String,
382 lifecycle: Option<String>,
383 scope_id: String,
384 scope_digest: String,
385 source_sha256: Option<String>,
386 candidate_sha256: Option<String>,
387 current_job_id: Option<String>,
388}
389
390#[derive(Debug, Clone, PartialEq, Eq)]
391#[non_exhaustive]
392pub struct AuthoritativeFrozenScopeView {
393 scope_id: String,
394 scope_digest: String,
395 obligations: Vec<(String, String)>,
396}
397
398#[derive(Debug, Clone, PartialEq, Eq)]
399#[non_exhaustive]
400pub struct AuthoritativeJobView {
401 id: String,
402 status: NativeStatus,
403 task_id: String,
404 source_sha256: Option<String>,
405 candidate_sha256: Option<String>,
406 run_ids: Vec<String>,
407 entitlement_consumption: Option<String>,
408}
409
410#[derive(Debug, Clone, PartialEq, Eq)]
411#[non_exhaustive]
412pub struct AuthoritativeRunView {
413 id: String,
414 status: NativeStatus,
415 job_id: String,
416 worker_id: Option<String>,
417 assignment_generation: Option<String>,
418 execution_started_at: Option<String>,
419 package_runtime_identity: Option<String>,
420 recovery_lease: Option<String>,
421}
422
423#[derive(Debug, Clone, PartialEq, Eq)]
424#[non_exhaustive]
425pub struct AuthoritativeRecoveryView {
426 state: RecoveryState,
427 reason: Option<String>,
428 action_available: bool,
429 task_id: String,
430 job_id: String,
431 run_id: String,
432 worker_id: String,
433 assignment_generation: String,
434 source_sha256: String,
435 candidate_sha256: Option<String>,
436 provenance: String,
437}
438
439#[derive(Debug, Clone, PartialEq, Eq)]
440#[non_exhaustive]
441pub struct AuthoritativeResultView {
442 verdict: NativeVerdict,
443 candidate_sha256: String,
444 source_sha256: String,
445 run_id: String,
446 job_id: String,
447 worker_id: Option<String>,
448 assignment_generation: Option<String>,
449 result_digest: String,
450}
451
452#[derive(Debug, Clone, PartialEq, Eq)]
453#[non_exhaustive]
454pub struct AuthoritativeEvidenceView {
455 items: Vec<NativeEvidenceItem>,
456 total: usize,
457 passed: usize,
458 failed: usize,
459 insufficient: usize,
460 unknown: usize,
461}
462
463#[derive(Debug, Clone, PartialEq, Eq)]
464#[non_exhaustive]
465pub struct AuthoritativeReportView {
466 id: String,
467 report_digest: String,
468 candidate_sha256: String,
469 source_sha256: String,
470 verdict: NativeVerdict,
471 job_id: String,
472 run_id: String,
473 result_digest: String,
474}
475
476impl AuthoritativeTaskView {
477 #[must_use]
478 pub fn task_id(&self) -> &str {
479 &self.task_id
480 }
481 #[must_use]
482 pub fn lifecycle(&self) -> Option<&str> {
483 self.lifecycle.as_deref()
484 }
485 #[must_use]
486 pub fn scope_id(&self) -> &str {
487 &self.scope_id
488 }
489 #[must_use]
490 pub fn scope_digest(&self) -> &str {
491 &self.scope_digest
492 }
493 #[must_use]
494 pub fn source_sha256(&self) -> Option<&str> {
495 self.source_sha256.as_deref()
496 }
497 #[must_use]
498 pub fn candidate_sha256(&self) -> Option<&str> {
499 self.candidate_sha256.as_deref()
500 }
501 #[must_use]
502 pub fn current_job_id(&self) -> Option<&str> {
503 self.current_job_id.as_deref()
504 }
505}
506
507impl AuthoritativeFrozenScopeView {
508 #[must_use]
509 pub fn scope_id(&self) -> &str {
510 &self.scope_id
511 }
512 #[must_use]
513 pub fn scope_digest(&self) -> &str {
514 &self.scope_digest
515 }
516 #[must_use]
517 pub fn obligations(&self) -> &[(String, String)] {
518 &self.obligations
519 }
520}
521
522impl AuthoritativeJobView {
523 #[must_use]
524 pub fn id(&self) -> &str {
525 &self.id
526 }
527 #[must_use]
528 pub const fn status(&self) -> NativeStatus {
529 self.status
530 }
531 #[must_use]
532 pub fn task_id(&self) -> &str {
533 &self.task_id
534 }
535 #[must_use]
536 pub fn source_sha256(&self) -> Option<&str> {
537 self.source_sha256.as_deref()
538 }
539 #[must_use]
540 pub fn candidate_sha256(&self) -> Option<&str> {
541 self.candidate_sha256.as_deref()
542 }
543 #[must_use]
544 pub fn run_ids(&self) -> &[String] {
545 &self.run_ids
546 }
547 #[must_use]
548 pub fn entitlement_consumption(&self) -> Option<&str> {
549 self.entitlement_consumption.as_deref()
550 }
551}
552
553impl AuthoritativeRunView {
554 #[must_use]
555 pub fn id(&self) -> &str {
556 &self.id
557 }
558 #[must_use]
559 pub const fn status(&self) -> NativeStatus {
560 self.status
561 }
562 #[must_use]
563 pub fn job_id(&self) -> &str {
564 &self.job_id
565 }
566 #[must_use]
567 pub fn worker_id(&self) -> Option<&str> {
568 self.worker_id.as_deref()
569 }
570 #[must_use]
571 pub fn assignment_generation(&self) -> Option<&str> {
572 self.assignment_generation.as_deref()
573 }
574 #[must_use]
575 pub fn execution_started_at(&self) -> Option<&str> {
576 self.execution_started_at.as_deref()
577 }
578 #[must_use]
579 pub fn package_runtime_identity(&self) -> Option<&str> {
580 self.package_runtime_identity.as_deref()
581 }
582 #[must_use]
583 pub fn recovery_lease(&self) -> Option<&str> {
584 self.recovery_lease.as_deref()
585 }
586}
587
588impl AuthoritativeRecoveryView {
589 #[must_use]
590 pub const fn state(&self) -> RecoveryState {
591 self.state
592 }
593 #[must_use]
594 pub fn reason(&self) -> Option<&str> {
595 self.reason.as_deref()
596 }
597 #[must_use]
598 pub const fn action_available(&self) -> bool {
599 self.action_available
600 }
601 #[must_use]
602 pub fn task_id(&self) -> &str {
603 &self.task_id
604 }
605 #[must_use]
606 pub fn job_id(&self) -> &str {
607 &self.job_id
608 }
609 #[must_use]
610 pub fn run_id(&self) -> &str {
611 &self.run_id
612 }
613 #[must_use]
614 pub fn worker_id(&self) -> &str {
615 &self.worker_id
616 }
617 #[must_use]
618 pub fn assignment_generation(&self) -> &str {
619 &self.assignment_generation
620 }
621 #[must_use]
622 pub fn source_sha256(&self) -> &str {
623 &self.source_sha256
624 }
625 #[must_use]
626 pub fn candidate_sha256(&self) -> Option<&str> {
627 self.candidate_sha256.as_deref()
628 }
629 #[must_use]
630 pub fn provenance(&self) -> &str {
631 &self.provenance
632 }
633}
634
635impl AuthoritativeResultView {
636 #[must_use]
637 pub const fn verdict(&self) -> NativeVerdict {
638 self.verdict
639 }
640 #[must_use]
641 pub fn candidate_sha256(&self) -> &str {
642 &self.candidate_sha256
643 }
644 #[must_use]
645 pub fn source_sha256(&self) -> &str {
646 &self.source_sha256
647 }
648 #[must_use]
649 pub fn run_id(&self) -> &str {
650 &self.run_id
651 }
652 #[must_use]
653 pub fn job_id(&self) -> &str {
654 &self.job_id
655 }
656 #[must_use]
657 pub fn worker_id(&self) -> Option<&str> {
658 self.worker_id.as_deref()
659 }
660 #[must_use]
661 pub fn assignment_generation(&self) -> Option<&str> {
662 self.assignment_generation.as_deref()
663 }
664 #[must_use]
665 pub fn result_digest(&self) -> &str {
666 &self.result_digest
667 }
668}
669
670impl AuthoritativeEvidenceView {
671 #[must_use]
672 pub fn items(&self) -> &[NativeEvidenceItem] {
673 &self.items
674 }
675 #[must_use]
676 pub const fn total(&self) -> usize {
677 self.total
678 }
679 #[must_use]
680 pub const fn passed(&self) -> usize {
681 self.passed
682 }
683 #[must_use]
684 pub const fn failed(&self) -> usize {
685 self.failed
686 }
687 #[must_use]
688 pub const fn insufficient(&self) -> usize {
689 self.insufficient
690 }
691 #[must_use]
692 pub const fn unknown(&self) -> usize {
693 self.unknown
694 }
695}
696
697impl AuthoritativeReportView {
698 #[must_use]
699 pub fn id(&self) -> &str {
700 &self.id
701 }
702 #[must_use]
703 pub fn report_digest(&self) -> &str {
704 &self.report_digest
705 }
706 #[must_use]
707 pub fn candidate_sha256(&self) -> &str {
708 &self.candidate_sha256
709 }
710 #[must_use]
711 pub fn source_sha256(&self) -> &str {
712 &self.source_sha256
713 }
714 #[must_use]
715 pub const fn verdict(&self) -> NativeVerdict {
716 self.verdict
717 }
718 #[must_use]
719 pub fn job_id(&self) -> &str {
720 &self.job_id
721 }
722 #[must_use]
723 pub fn run_id(&self) -> &str {
724 &self.run_id
725 }
726 #[must_use]
727 pub fn result_digest(&self) -> &str {
728 &self.result_digest
729 }
730}
731
732impl AuthoritativeTaskView {
733 pub fn try_from_value(value: &Value) -> Result<Self, String> {
734 parse_task_view(value)
735 }
736 pub fn from_value(value: &Value) -> Result<Self, String> {
737 Self::try_from_value(value)
738 }
739}
740impl AuthoritativeFrozenScopeView {
741 pub fn try_from_value(value: &Value) -> Result<Self, String> {
742 parse_scope_view(value)
743 }
744 pub fn from_value(value: &Value) -> Result<Self, String> {
745 Self::try_from_value(value)
746 }
747}
748impl AuthoritativeJobView {
749 pub fn try_from_value(value: &Value) -> Result<Self, String> {
750 parse_job_view(value)
751 }
752 pub fn from_value(value: &Value) -> Result<Self, String> {
753 Self::try_from_value(value)
754 }
755}
756impl AuthoritativeRunView {
757 pub fn try_from_value(value: &Value) -> Result<Self, String> {
758 parse_run_view(value)
759 }
760 pub fn from_value(value: &Value) -> Result<Self, String> {
761 Self::try_from_value(value)
762 }
763}
764impl AuthoritativeRecoveryView {
765 #[allow(clippy::too_many_arguments)]
766 pub fn try_from_value(
767 value: &Value,
768 task: &AuthoritativeTaskView,
769 job: &AuthoritativeJobView,
770 run: &AuthoritativeRunView,
771 source: &str,
772 ) -> Result<Self, String> {
773 parse_recovery_view(value, Some(task), Some(job), Some(run), Some(source))
774 }
775 #[allow(clippy::too_many_arguments)]
776 pub fn from_value(
777 value: &Value,
778 task: &AuthoritativeTaskView,
779 job: &AuthoritativeJobView,
780 run: &AuthoritativeRunView,
781 source: &str,
782 ) -> Result<Self, String> {
783 Self::try_from_value(value, task, job, run, source)
784 }
785}
786
787impl AuthoritativeEvidenceView {
788 pub fn try_from_value(
789 _value: &Value,
790 _scope: &AuthoritativeFrozenScopeView,
791 ) -> Result<Self, String> {
792 Err("evidence authority requires a validated parent Result and source context".to_owned())
793 }
794
795 pub fn try_from_value_with_result(
796 value: &Value,
797 scope: &AuthoritativeFrozenScopeView,
798 result: &AuthoritativeResultView,
799 ) -> Result<Self, String> {
800 let values = strict_evidence_array(value)?.ok_or("evidence array is missing")?;
801 if values.len() > MAX_EVIDENCE_ITEMS {
802 return Err("evidence set exceeds validation bound".to_owned());
803 }
804 let mut ids = BTreeSet::new();
805 let mut items = Vec::with_capacity(values.len());
806 let mut counts = [0usize; 4];
807 for value in values {
808 let (id, item, state) = parse_evidence_item(
809 value,
810 scope,
811 Some(result.job_id()),
812 Some(result.run_id()),
813 Some(result.source_sha256()),
814 Some(result.result_digest()),
815 )?;
816 if !ids.insert(id) {
817 return Err("duplicate evidence ID".to_owned());
818 }
819 match state {
820 EvidenceState::Passed => counts[0] += 1,
821 EvidenceState::Failed => counts[1] += 1,
822 EvidenceState::Insufficient => counts[2] += 1,
823 EvidenceState::Unknown => counts[3] += 1,
824 }
825 items.push(item);
826 }
827 Ok(Self {
828 items,
829 total: values.len(),
830 passed: counts[0],
831 failed: counts[1],
832 insufficient: counts[2],
833 unknown: counts[3],
834 })
835 }
836
837 pub fn from_value(value: &Value, scope: &AuthoritativeFrozenScopeView) -> Result<Self, String> {
838 Self::try_from_value(value, scope)
839 }
840
841 pub fn from_value_with_result(
842 value: &Value,
843 scope: &AuthoritativeFrozenScopeView,
844 result: &AuthoritativeResultView,
845 ) -> Result<Self, String> {
846 Self::try_from_value_with_result(value, scope, result)
847 }
848}
849
850#[derive(Debug, Clone, PartialEq, Eq)]
851#[non_exhaustive]
852pub struct ValidatedReplacementSessionView {
853 predecessor_session_id: String,
854 replacement_session_id: String,
855 predecessor_state: String,
856 candidate_sha256: String,
857 workspace_state: String,
858 source_git_state_sequence: u64,
859}
860
861impl ValidatedReplacementSessionView {
862 #[must_use]
863 pub fn predecessor_session_id(&self) -> &str {
864 &self.predecessor_session_id
865 }
866 #[must_use]
867 pub fn replacement_session_id(&self) -> &str {
868 &self.replacement_session_id
869 }
870 #[must_use]
871 pub fn predecessor_state(&self) -> &str {
872 &self.predecessor_state
873 }
874 #[must_use]
875 pub fn candidate_sha256(&self) -> &str {
876 &self.candidate_sha256
877 }
878 #[must_use]
879 pub fn workspace_state(&self) -> &str {
880 &self.workspace_state
881 }
882 #[must_use]
883 pub const fn source_git_state_sequence(&self) -> u64 {
884 self.source_git_state_sequence
885 }
886}
887
888impl ValidatedReplacementSessionView {
889 fn try_from_record_predecessor(
890 record: &SessionReplacementRecord,
891 predecessor_events: &[Event],
892 ) -> Result<Self, String> {
893 validate_replacement_predecessor(record, predecessor_events)?;
894 let git = predecessor_events
895 .iter()
896 .find(|event| {
897 event.sequence == record.source_git_state_sequence
898 && event.kind == EventKind::GitState
899 })
900 .ok_or("replacement GitState sequence is not bound")?;
901 let workspace_state = git
902 .payload
903 .get("workspace_state_sha256")
904 .and_then(Value::as_str)
905 .filter(|value| valid_digest(value))
906 .ok_or("replacement workspace identity is unavailable")?;
907 Ok(Self {
908 predecessor_session_id: record.predecessor_session_id.clone(),
909 replacement_session_id: record.replacement_session_id.clone(),
910 predecessor_state: record.predecessor_state.clone(),
911 candidate_sha256: record.candidate_sha256.clone(),
912 workspace_state: workspace_state.to_owned(),
913 source_git_state_sequence: record.source_git_state_sequence,
914 })
915 }
916
917 pub fn try_from_record_with_related(
921 record: &SessionReplacementRecord,
922 predecessor_events: &[Event],
923 replacement_events: &[Event],
924 ) -> Result<Self, String> {
925 let view = Self::try_from_record_predecessor(record, predecessor_events)?;
926 if replacement_events.is_empty()
927 || replacement_events
928 .iter()
929 .any(|event| event.session_id != record.replacement_session_id)
930 {
931 return Err("replacement events are not bound to the recorded successor".to_owned());
932 }
933 let replaced = replacement_events
934 .iter()
935 .filter(|event| event.kind == EventKind::SessionReplaced)
936 .collect::<Vec<_>>();
937 if replaced.len() != 1 {
938 return Err("replacement relation event is missing or duplicated".to_owned());
939 }
940 let payload = &replaced[0].payload;
941 for (key, expected) in [
942 (
943 "predecessor_session_id",
944 record.predecessor_session_id.as_str(),
945 ),
946 ("predecessor_state", record.predecessor_state.as_str()),
947 (
948 "predecessor_history_sha256",
949 record.predecessor_history_sha256.as_str(),
950 ),
951 ("candidate_sha256", record.candidate_sha256.as_str()),
952 ("falsegreen_task_id", record.falsegreen_task_id.as_str()),
953 ] {
954 if payload.get(key).and_then(Value::as_str) != Some(expected) {
955 return Err(format!("replacement relation field {key} is not exact"));
956 }
957 }
958 if payload
959 .get("candidate_event_sequence")
960 .and_then(Value::as_u64)
961 != Some(record.candidate_event_sequence)
962 || payload
963 .get("source_git_state_sequence")
964 .and_then(Value::as_u64)
965 != Some(record.source_git_state_sequence)
966 {
967 return Err("replacement relation sequence binding is not exact".to_owned());
968 }
969 let authority = predecessor_events
970 .iter()
971 .find(|event| {
972 event.kind == EventKind::Checkpoint
973 && event.payload["checkpoint_kind"] == "acceptance_authority"
974 })
975 .ok_or("predecessor task authority is missing")?;
976 let mut expected_authority = authority.payload.clone();
977 expected_authority["replacement_predecessor_session_id"] =
978 Value::String(record.predecessor_session_id.clone());
979 let replacement_authority = replacement_events
980 .iter()
981 .filter(|event| {
982 event.kind == EventKind::Checkpoint
983 && event.payload["checkpoint_kind"] == "acceptance_authority"
984 })
985 .collect::<Vec<_>>();
986 if replacement_authority.len() != 1
987 || replacement_authority[0].payload != expected_authority
988 {
989 return Err("replacement task authority binding is not exact".to_owned());
990 }
991 let carried = replacement_events
992 .iter()
993 .filter(|event| {
994 event.kind == EventKind::CandidateReady
995 && event.payload["carried_forward"].as_bool() == Some(true)
996 })
997 .collect::<Vec<_>>();
998 if carried.len() != 1
999 || carried[0].payload["candidate_sha256"].as_str()
1000 != Some(record.candidate_sha256.as_str())
1001 || carried[0].payload["predecessor_session_id"].as_str()
1002 != Some(record.predecessor_session_id.as_str())
1003 || carried[0].payload["predecessor_candidate_event_sequence"].as_u64()
1004 != Some(record.candidate_event_sequence)
1005 {
1006 return Err("replacement candidate continuity is incomplete".to_owned());
1007 }
1008 let git_states = replacement_events
1009 .iter()
1010 .filter(|event| event.kind == EventKind::GitState)
1011 .collect::<Vec<_>>();
1012 let source_git = predecessor_events
1013 .iter()
1014 .find(|event| event.sequence == record.source_git_state_sequence)
1015 .ok_or("predecessor GitState is missing")?;
1016 if git_states.len() != 1
1017 || git_states[0].sequence <= carried[0].sequence
1018 || git_states[0].payload != source_git.payload
1019 {
1020 return Err("replacement GitState continuity is incomplete".to_owned());
1021 }
1022 let validations = replacement_events
1023 .iter()
1024 .filter(|event| {
1025 event.kind == EventKind::Checkpoint
1026 && event.payload["checkpoint_kind"] == "replacement_candidate_validation"
1027 })
1028 .collect::<Vec<_>>();
1029 if validations.len() != 1
1030 || validations[0].payload["state"] != "candidate_ready"
1031 || validations[0].payload["predecessor_session_id"].as_str()
1032 != Some(record.predecessor_session_id.as_str())
1033 || validations[0].payload["expected_candidate_sha256"].as_str()
1034 != Some(record.candidate_sha256.as_str())
1035 || validations[0].payload["actual_candidate_sha256"].as_str()
1036 != Some(record.candidate_sha256.as_str())
1037 || validations[0].payload["matched"] != true
1038 {
1039 return Err("replacement candidate validation checkpoint is incomplete".to_owned());
1040 }
1041 Ok(view)
1042 }
1043}
1044
1045fn validate_replacement_predecessor(
1046 record: &SessionReplacementRecord,
1047 events: &[Event],
1048) -> Result<(), String> {
1049 if record.predecessor_session_id.is_empty()
1050 || record.replacement_session_id.is_empty()
1051 || record.predecessor_session_id == record.replacement_session_id
1052 || record.predecessor_state != "failed"
1053 || !valid_digest(&record.predecessor_history_sha256)
1054 || !valid_digest(&record.candidate_sha256)
1055 || record.falsegreen_task_id.is_empty()
1056 {
1057 return Err("replacement record shape is invalid".to_owned());
1058 }
1059 if events.is_empty()
1060 || events
1061 .iter()
1062 .any(|event| event.session_id != record.predecessor_session_id)
1063 {
1064 return Err("predecessor events are not bound to the recorded session".to_owned());
1065 }
1066 if history_sha256(events).map_err(|error| error.to_string())?
1067 != record.predecessor_history_sha256
1068 {
1069 return Err("predecessor history digest is not exact".to_owned());
1070 }
1071 let terminal = events.last().ok_or("predecessor history is empty")?;
1072 if terminal.kind != EventKind::TerminalState
1073 || terminal.payload["state"] != "failed"
1074 || terminal.payload["reason"] != "falsegreen_infrastructure_failure"
1075 {
1076 return Err("predecessor terminal state is not eligible".to_owned());
1077 }
1078 let candidate_index = events
1079 .iter()
1080 .position(|event| event.sequence == record.candidate_event_sequence)
1081 .ok_or("replacement candidate sequence is not bound")?;
1082 let candidate = &events[candidate_index];
1083 if candidate.kind != EventKind::CandidateReady
1084 || candidate.payload["candidate_sha256"].as_str() != Some(record.candidate_sha256.as_str())
1085 || candidate.payload["summary"]
1086 .as_str()
1087 .is_none_or(str::is_empty)
1088 {
1089 return Err("replacement candidate event is incomplete".to_owned());
1090 }
1091 let boundary = events[..candidate_index]
1092 .iter()
1093 .rev()
1094 .find(|event| {
1095 event.kind == EventKind::Checkpoint
1096 && event.payload["checkpoint_kind"] == "turn_boundary"
1097 })
1098 .ok_or("replacement turn boundary is missing")?;
1099 if boundary.payload["candidate_sha256"].as_str() != Some(record.candidate_sha256.as_str()) {
1100 return Err("replacement turn boundary candidate is not exact".to_owned());
1101 }
1102 let git = events
1103 .iter()
1104 .find(|event| {
1105 event.sequence == record.source_git_state_sequence
1106 && event.kind == EventKind::GitState
1107 && event.sequence > candidate.sequence
1108 })
1109 .ok_or("replacement GitState sequence is not bound")?;
1110 if git.payload["workspace_state_sha256"]
1111 .as_str()
1112 .is_none_or(|value| !valid_digest(value))
1113 {
1114 return Err("replacement workspace identity is unavailable".to_owned());
1115 }
1116 let authority = events
1117 .iter()
1118 .filter(|event| {
1119 event.kind == EventKind::Checkpoint
1120 && event.payload["checkpoint_kind"] == "acceptance_authority"
1121 })
1122 .collect::<Vec<_>>();
1123 if authority.len() != 1
1124 || authority[0].payload["falsegreen_task_id"].as_str()
1125 != Some(record.falsegreen_task_id.as_str())
1126 {
1127 return Err("replacement task binding is not exact".to_owned());
1128 }
1129 if events
1130 .iter()
1131 .any(|event| event.kind == EventKind::SessionReplaced)
1132 {
1133 return Err("predecessor contains a forbidden replacement chain".to_owned());
1134 }
1135 Ok(())
1136}
1137
1138#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1139#[serde(rename_all = "snake_case")]
1140pub enum NativeSurfaceFamily {
1141 Task,
1142 FrozenScope,
1143 Job,
1144 Run,
1145 VerificationResult,
1146 Evidence,
1147 Report,
1148 FailureRecovery,
1149 ReplacementSession,
1150}
1151
1152impl NativeSurfaceFamily {
1153 #[must_use]
1154 pub const fn as_str(self) -> &'static str {
1155 match self {
1156 Self::Task => "task",
1157 Self::FrozenScope => "frozen-scope",
1158 Self::Job => "job",
1159 Self::Run => "run",
1160 Self::VerificationResult => "verification-result",
1161 Self::Evidence => "evidence",
1162 Self::Report => "report",
1163 Self::FailureRecovery => "failure-recovery",
1164 Self::ReplacementSession => "replacement-session",
1165 }
1166 }
1167}
1168
1169#[derive(Debug, Clone, PartialEq)]
1170pub struct NativeSurfaceSet {
1171 pub task: Surface,
1172 pub frozen_scope: Surface,
1173 pub job: Surface,
1174 pub run: Surface,
1175 pub verification_result: Surface,
1176 pub evidence: Surface,
1177 pub report: Surface,
1178 pub failure_recovery: Surface,
1179 pub replacement_session: Surface,
1180}
1181
1182impl NativeSurfaceSet {
1183 #[must_use]
1184 pub fn get(&self, family: NativeSurfaceFamily) -> &Surface {
1185 match family {
1186 NativeSurfaceFamily::Task => &self.task,
1187 NativeSurfaceFamily::FrozenScope => &self.frozen_scope,
1188 NativeSurfaceFamily::Job => &self.job,
1189 NativeSurfaceFamily::Run => &self.run,
1190 NativeSurfaceFamily::VerificationResult => &self.verification_result,
1191 NativeSurfaceFamily::Evidence => &self.evidence,
1192 NativeSurfaceFamily::Report => &self.report,
1193 NativeSurfaceFamily::FailureRecovery => &self.failure_recovery,
1194 NativeSurfaceFamily::ReplacementSession => &self.replacement_session,
1195 }
1196 }
1197
1198 pub fn iter(&self) -> impl Iterator<Item = (NativeSurfaceFamily, &Surface)> {
1199 [
1200 (NativeSurfaceFamily::Task, &self.task),
1201 (NativeSurfaceFamily::FrozenScope, &self.frozen_scope),
1202 (NativeSurfaceFamily::Job, &self.job),
1203 (NativeSurfaceFamily::Run, &self.run),
1204 (
1205 NativeSurfaceFamily::VerificationResult,
1206 &self.verification_result,
1207 ),
1208 (NativeSurfaceFamily::Evidence, &self.evidence),
1209 (NativeSurfaceFamily::Report, &self.report),
1210 (NativeSurfaceFamily::FailureRecovery, &self.failure_recovery),
1211 (
1212 NativeSurfaceFamily::ReplacementSession,
1213 &self.replacement_session,
1214 ),
1215 ]
1216 .into_iter()
1217 }
1218}
1219
1220#[derive(Debug, Error, Clone, PartialEq, Eq)]
1221pub enum NativeProjectionError {
1222 #[error("event-store error while projecting native state: {0}")]
1223 EventStore(String),
1224 #[error("session error while projecting native state: {0}")]
1225 Session(String),
1226 #[error("native state snapshot drifted while it was being read; refresh required")]
1227 SnapshotDrift,
1228 #[error("native state is malformed: {0}")]
1229 Malformed(String),
1230 #[error("native surface rendering failed: {0}")]
1231 Render(String),
1232}
1233
1234impl From<EventError> for NativeProjectionError {
1235 fn from(error: EventError) -> Self {
1236 Self::EventStore(error.to_string())
1237 }
1238}
1239impl From<SessionError> for NativeProjectionError {
1240 fn from(error: SessionError) -> Self {
1241 Self::Session(error.to_string())
1242 }
1243}
1244impl From<GenUiError> for NativeProjectionError {
1245 fn from(error: GenUiError) -> Self {
1246 Self::Render(error.to_string())
1247 }
1248}
1249
1250#[derive(Debug, Clone)]
1251struct Capture {
1252 session: Session,
1253 events: Vec<Event>,
1254 relation: Option<SessionReplacementRecord>,
1255 related_events: Vec<Event>,
1256}
1257
1258#[derive(Debug, Clone, PartialEq)]
1259pub struct NativeStateProjection {
1260 pub snapshot: NativeSnapshotIdentity,
1261 pub task: NativeTask,
1262 pub frozen_scope: NativeFrozenScope,
1263 pub job: NativeJob,
1264 pub run: NativeRun,
1265 pub verification: NativeVerification,
1266 pub evidence: NativeEvidence,
1267 pub report: NativeReport,
1268 pub recovery: NativeRecovery,
1269 pub replacement: NativeReplacement,
1270 surfaces: NativeSurfaceSet,
1271}
1272
1273impl NativeStateProjection {
1274 pub fn from_store(store: &EventStore, session_id: &str) -> Result<Self, NativeProjectionError> {
1275 let first = capture(store, session_id)?;
1276 let first_digest = capture_digest(&first)?;
1277 let second = capture(store, session_id)?;
1278 if first_digest != capture_digest(&second)? || first.relation != second.relation {
1279 return Err(NativeProjectionError::SnapshotDrift);
1280 }
1281 from_capture(&second)
1282 }
1283
1284 pub fn project(store: &EventStore, session_id: &str) -> Result<Self, NativeProjectionError> {
1285 Self::from_store(store, session_id)
1286 }
1287
1288 pub fn from_session(
1289 store: &EventStore,
1290 session: &Session,
1291 ) -> Result<Self, NativeProjectionError> {
1292 let projection = Self::from_store(store, &session.id)?;
1293 if projection.task.lifecycle.value.as_deref() != Some(session_state_label(session.state)) {
1294 return Err(NativeProjectionError::SnapshotDrift);
1295 }
1296 Ok(projection)
1297 }
1298
1299 #[must_use]
1300 pub fn surfaces(&self) -> &NativeSurfaceSet {
1301 &self.surfaces
1302 }
1303
1304 #[must_use]
1305 pub fn surface(&self, family: NativeSurfaceFamily) -> &Surface {
1306 self.surfaces.get(family)
1307 }
1308
1309 pub fn is_stale(&self, store: &EventStore) -> Result<bool, NativeProjectionError> {
1310 Ok(Self::from_store(store, &self.snapshot.source_session_id)?.snapshot != self.snapshot)
1311 }
1312
1313 pub fn refresh(&self, store: &EventStore) -> Result<Self, NativeProjectionError> {
1314 Self::from_store(store, &self.snapshot.source_session_id)
1315 }
1316
1317 pub fn render(
1318 &self,
1319 store: &EventStore,
1320 family: NativeSurfaceFamily,
1321 terminal_width: usize,
1322 ) -> Result<String, NativeProjectionError> {
1323 if self.is_stale(store)? {
1324 return Err(NativeProjectionError::SnapshotDrift);
1325 }
1326 let capabilities = default_negotiated_capabilities(terminal_width.max(1));
1327 let mut catalog = ActionCatalog::default();
1328 render_surface_with_capabilities_and_catalog_with_store(
1329 self.surface(family),
1330 &capabilities,
1331 &mut catalog,
1332 store,
1333 )
1334 .map_err(NativeProjectionError::from)
1335 }
1336}
1337
1338pub fn native_state_surfaces(
1339 store: &EventStore,
1340 session_id: &str,
1341) -> Result<NativeSurfaceSet, NativeProjectionError> {
1342 Ok(NativeStateProjection::from_store(store, session_id)?
1343 .surfaces
1344 .clone())
1345}
1346
1347pub fn project_native_state(
1348 store: &EventStore,
1349 session_id: &str,
1350) -> Result<NativeStateProjection, NativeProjectionError> {
1351 NativeStateProjection::from_store(store, session_id)
1352}
1353
1354pub fn render_native_surface(
1355 store: &EventStore,
1356 session_id: &str,
1357 family: NativeSurfaceFamily,
1358 terminal_width: usize,
1359) -> Result<String, NativeProjectionError> {
1360 NativeStateProjection::from_store(store, session_id)?.render(store, family, terminal_width)
1361}
1362
1363fn capture(store: &EventStore, session_id: &str) -> Result<Capture, NativeProjectionError> {
1364 let session = Session::reconstruct(store, session_id)?;
1365 let events = store.events(session_id)?;
1366 let relation = store
1367 .replacement_for_session(session_id)?
1368 .or(store.replacement_for_predecessor(session_id)?);
1369 let related_events = relation
1370 .as_ref()
1371 .map(|relation| {
1372 let related_id = if relation.predecessor_session_id == session_id {
1373 &relation.replacement_session_id
1374 } else {
1375 &relation.predecessor_session_id
1376 };
1377 store.events(related_id)
1378 })
1379 .transpose()?
1380 .unwrap_or_default();
1381 Ok(Capture {
1382 session,
1383 events,
1384 relation,
1385 related_events,
1386 })
1387}
1388
1389fn capture_digest(capture: &Capture) -> Result<String, NativeProjectionError> {
1390 let own = history_sha256(&capture.events)?;
1391 let related = (!capture.related_events.is_empty())
1392 .then(|| history_sha256(&capture.related_events))
1393 .transpose()?;
1394 serde_json::to_string(&serde_json::json!({
1395 "schema": "falsegreen.agent.native-state-snapshot.v2",
1396 "session": capture.session.id,
1397 "state": session_state_label(capture.session.state),
1398 "own": own,
1399 "related": related,
1400 "relation": capture.relation,
1401 }))
1402 .map(|value| sha256_bytes(value.as_bytes()))
1403 .map_err(|error| NativeProjectionError::Malformed(error.to_string()))
1404}
1405
1406fn snapshot_identity(capture: &Capture) -> Result<NativeSnapshotIdentity, NativeProjectionError> {
1407 let state_generation = capture
1408 .events
1409 .iter()
1410 .chain(capture.related_events.iter())
1411 .map(|event| event.sequence)
1412 .max()
1413 .unwrap_or_default();
1414 Ok(NativeSnapshotIdentity {
1415 source_session_id: capture.session.id.clone(),
1416 state_generation,
1417 digest: capture_digest(capture)?,
1418 })
1419}
1420
1421struct Facts {
1422 task: NativeTask,
1423 frozen_scope: NativeFrozenScope,
1424 job: NativeJob,
1425 run: NativeRun,
1426 verification: NativeVerification,
1427 evidence: NativeEvidence,
1428 report: NativeReport,
1429 recovery: NativeRecovery,
1430 replacement: NativeReplacement,
1431}
1432
1433fn from_capture(capture: &Capture) -> Result<NativeStateProjection, NativeProjectionError> {
1434 let snapshot = snapshot_identity(capture)?;
1435 let facts = derive_facts(capture);
1436 let surfaces = build_surfaces(&snapshot, &facts);
1437 for (_, surface) in surfaces.iter() {
1438 surface.validate().map_err(NativeProjectionError::from)?;
1439 }
1440 Ok(NativeStateProjection {
1441 snapshot,
1442 task: facts.task,
1443 frozen_scope: facts.frozen_scope,
1444 job: facts.job,
1445 run: facts.run,
1446 verification: facts.verification,
1447 evidence: facts.evidence,
1448 report: facts.report,
1449 recovery: facts.recovery,
1450 replacement: facts.replacement,
1451 surfaces,
1452 })
1453}
1454
1455fn derive_facts(capture: &Capture) -> Facts {
1456 let candidate = candidate_context(&capture.events);
1457 let source = source_context(&capture.events);
1458 let task_view = latest_typed_task(&capture.events).filter(|task| {
1459 latest_typed_scope(&capture.events).is_some_and(|scope| {
1460 task.scope_id == scope.scope_id
1461 && task.scope_digest == scope.scope_digest
1462 && task
1463 .candidate_sha256
1464 .as_deref()
1465 .is_none_or(|digest| candidate.as_deref() == Some(digest))
1466 && task
1467 .source_sha256
1468 .as_deref()
1469 .is_none_or(|digest| source.as_deref() == Some(digest))
1470 })
1471 });
1472 let scope_view = latest_typed_scope(&capture.events);
1473 let mut job_view = latest_typed_job(&capture.events).filter(|job| {
1474 task_view.as_ref().is_some_and(|task| {
1475 task.task_id == job.task_id
1476 && task.current_job_id.as_deref().is_none_or(|id| id == job.id)
1477 && task.candidate_sha256.as_deref().is_some_and(|digest| {
1478 job.candidate_sha256.as_deref() == Some(digest)
1479 && candidate.as_deref() == Some(digest)
1480 })
1481 && task.source_sha256.as_deref().is_some_and(|digest| {
1482 job.source_sha256.as_deref() == Some(digest)
1483 && source.as_deref() == Some(digest)
1484 })
1485 })
1486 });
1487 let mut run_view = latest_typed_run(&capture.events).filter(|run| {
1491 job_view.as_ref().is_some_and(|job| {
1492 run.job_id == job.id
1493 && (job.run_ids.is_empty() || job.run_ids.iter().any(|id| id == &run.id))
1494 })
1495 });
1496 if job_view.as_ref().is_some_and(|job| {
1497 run_view
1498 .as_ref()
1499 .is_some_and(|run| !job.run_ids.iter().any(|id| id == &run.id))
1500 }) {
1501 run_view = None;
1502 }
1503 if job_view.as_ref().is_some_and(|job| {
1507 !job.run_ids.is_empty()
1508 && run_view
1509 .as_ref()
1510 .is_none_or(|run| job.run_ids.len() != 1 || job.run_ids[0] != run.id)
1511 }) {
1512 job_view = None;
1513 }
1514 let result_view = latest_typed_result(
1515 &capture.events,
1516 candidate.as_deref(),
1517 source.as_deref(),
1518 scope_view.as_ref(),
1519 job_view.as_ref(),
1520 run_view.as_ref(),
1521 )
1522 .filter(|result| {
1523 job_view.as_ref().is_some_and(|job| job.id == result.job_id)
1524 && run_view.as_ref().is_some_and(|run| run.id == result.run_id)
1525 && result.worker_id.as_deref().is_none_or(|worker| {
1526 run_view.as_ref().and_then(|run| run.worker_id.as_deref()) == Some(worker)
1527 })
1528 && result
1529 .assignment_generation
1530 .as_deref()
1531 .is_none_or(|generation| {
1532 run_view
1533 .as_ref()
1534 .and_then(|run| run.assignment_generation.as_deref())
1535 == Some(generation)
1536 })
1537 });
1538 let report_view = latest_typed_report(
1539 &capture.events,
1540 candidate.as_deref(),
1541 source.as_deref(),
1542 job_view.as_ref(),
1543 run_view.as_ref(),
1544 result_view.as_ref(),
1545 );
1546 let evidence = result_view.as_ref().map_or_else(
1547 || unavailable_evidence("Evidence unavailable: no validated FalseGreen result or frozen scope authority is available."),
1548 |result| {
1549 evidence_from_result(
1550 result,
1551 scope_view.as_ref(),
1552 job_view.as_ref().map(|job| job.id.as_str()),
1553 run_view.as_ref().map(|run| run.id.as_str()),
1554 source.as_deref(),
1555 )
1556 },
1557 );
1558
1559 let task_id = task_view.as_ref().map(|view| view.task_id.clone());
1560 let scope_digest = scope_view.as_ref().map(|view| view.scope_digest.clone());
1561 let task = NativeTask {
1562 task_id: task_id.as_deref().map_or_else(
1563 || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
1564 NativeField::falsegreen,
1565 ),
1566 lifecycle: task_view
1567 .as_ref()
1568 .and_then(|view| view.lifecycle.as_deref())
1569 .map_or_else(
1570 || NativeField::agent(session_state_label(capture.session.state)),
1571 NativeField::falsegreen,
1572 ),
1573 frozen: scope_view.as_ref().map_or_else(
1574 || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
1575 |_| NativeField::falsegreen("FROZEN"),
1576 ),
1577 scope_digest: scope_digest.as_deref().map_or_else(
1578 || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
1579 NativeField::falsegreen,
1580 ),
1581 scope_summary: scope_view.as_ref().map_or_else(
1582 || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
1583 |view| {
1584 NativeField::falsegreen(format!("{} frozen obligation(s)", view.obligations.len()))
1585 },
1586 ),
1587 source_sha256: task_view
1588 .as_ref()
1589 .and_then(|view| view.source_sha256.as_deref())
1590 .map_or_else(
1591 || {
1592 source.as_deref().map_or_else(
1593 || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
1594 NativeField::agent,
1595 )
1596 },
1597 NativeField::falsegreen,
1598 ),
1599 candidate_sha256: task_view
1600 .as_ref()
1601 .and_then(|view| view.candidate_sha256.as_deref())
1602 .or(candidate.as_deref())
1603 .map_or_else(
1604 || NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
1605 NativeField::agent,
1606 ),
1607 current_job_id: task_view
1608 .as_ref()
1609 .and_then(|view| view.current_job_id.as_deref())
1610 .or(job_view.as_ref().map(|view| view.id.as_str()))
1611 .map_or_else(
1612 || NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
1613 NativeField::agent,
1614 ),
1615 };
1616 let frozen_scope = scope_view.as_ref().map_or_else(unavailable_scope, |view| {
1617 NativeFrozenScope {
1618 scope_digest: NativeField::falsegreen(view.scope_digest.clone()),
1619 frozen: NativeField::falsegreen("FROZEN"),
1620 obligations: view
1621 .obligations
1622 .iter()
1623 .map(|(id, requirement)| NativeScopeObligation {
1624 id: NativeField::falsegreen(id),
1625 requirement: NativeField::falsegreen(requirement),
1626 })
1627 .collect(),
1628 distinction: "Requirement text is immutable frozen scope; evidence/result state is separate and cannot rewrite it.".to_owned(),
1629 }
1630 });
1631
1632 let mut verification = result_view
1633 .as_ref()
1634 .map_or_else(unavailable_verification, |result| {
1635 verification_from_result(result, scope_view.as_ref())
1636 });
1637 let job = job_view
1638 .as_ref()
1639 .map_or_else(unavailable_job, |view| NativeJob {
1640 id: NativeField::falsegreen(view.id.clone()),
1641 status: view.status,
1642 status_raw: NativeField::falsegreen(status_wire(view.status)),
1643 task_id: NativeField::falsegreen(view.task_id.clone()),
1644 source_sha256: optional_falsegreen(view.source_sha256.as_deref()),
1645 candidate_sha256: optional_falsegreen(view.candidate_sha256.as_deref()),
1646 entitlement_consumption: optional_agent(view.entitlement_consumption.as_deref()),
1647 run_ids: view.run_ids.iter().map(NativeField::agent).collect(),
1648 authority_decision: NativeField::unavailable(
1649 AuthorityClass::AuthoritativeFalseGreenState,
1650 ),
1651 });
1652 let run = run_view
1653 .as_ref()
1654 .map_or_else(unavailable_run, |view| NativeRun {
1655 id: NativeField::agent(view.id.clone()),
1656 worker_id: optional_agent(view.worker_id.as_deref()),
1657 job_id: NativeField::agent(view.job_id.clone()),
1658 assignment_generation: optional_agent(view.assignment_generation.as_deref()),
1659 status: view.status,
1660 status_raw: NativeField::agent(status_wire(view.status)),
1661 execution_started_at: optional_agent(view.execution_started_at.as_deref()),
1662 package_runtime_identity: optional_agent(view.package_runtime_identity.as_deref()),
1663 recovery_lease: optional_agent(view.recovery_lease.as_deref()),
1664 });
1665 let recovery = latest_typed_recovery(
1666 &capture.events,
1667 task_view.as_ref(),
1668 job_view.as_ref(),
1669 run_view.as_ref(),
1670 source.as_deref(),
1671 )
1672 .map_or_else(|| recovery_unknown(&run), |view| recovery_from_view(&view));
1673 let report = report_view
1674 .as_ref()
1675 .map_or_else(|| unavailable_report(&verification), report_from_view);
1676 if let Some(report_id) = report.id.value.as_deref() {
1677 verification.report_reference = NativeField::falsegreen(report_id);
1678 }
1679 let replacement = replacement_from_capture(capture, &verification);
1680 Facts {
1681 task,
1682 frozen_scope,
1683 job,
1684 run,
1685 verification,
1686 evidence,
1687 report,
1688 recovery,
1689 replacement,
1690 }
1691}
1692
1693fn latest_typed_task(events: &[Event]) -> Option<AuthoritativeTaskView> {
1694 unique_latest(events, |event| {
1695 checkpoint_object(
1696 event,
1697 &["task_authority", "falsegreen_task_authority"],
1698 "task",
1699 )
1700 .or_else(|| {
1701 (event.kind == EventKind::Checkpoint
1702 && event.payload["checkpoint_kind"] == "acceptance_authority")
1703 .then(|| event.payload.get("task"))
1704 .flatten()
1705 .filter(|value| value.is_object())
1706 })
1707 .or_else(|| {
1708 (event.kind == EventKind::Checkpoint
1709 && (event.payload["checkpoint_kind"] == "falsegreen_task_authority"
1710 || (event.payload["checkpoint_kind"] == "acceptance_authority"
1711 && event.payload.get("task_id").is_some()
1712 && event.payload.get("scope_id").is_some())))
1713 .then_some(&event.payload)
1714 })
1715 })
1716 .and_then(|value| parse_task_view(value).ok())
1717}
1718
1719fn latest_typed_scope(events: &[Event]) -> Option<AuthoritativeFrozenScopeView> {
1720 unique_latest(events, |event| {
1721 checkpoint_object(event, &["frozen_scope_authority"], "frozen_scope")
1722 .or_else(|| {
1723 (event.kind == EventKind::Checkpoint
1724 && event.payload["checkpoint_kind"] == "acceptance_authority")
1725 .then(|| event.payload.get("frozen_scope"))
1726 .flatten()
1727 .filter(|value| value.is_object())
1728 })
1729 .or_else(|| {
1730 (event.kind == EventKind::Checkpoint
1731 && (event.payload["checkpoint_kind"] == "frozen_scope_authority"
1732 || (event.payload["checkpoint_kind"] == "acceptance_authority"
1733 && event.payload.get("scope_id").is_some()
1734 && event.payload.get("obligations").is_some())))
1735 .then_some(&event.payload)
1736 })
1737 })
1738 .and_then(|value| parse_scope_view(value).ok())
1739}
1740
1741fn latest_typed_job(events: &[Event]) -> Option<AuthoritativeJobView> {
1742 unique_latest(events, |event| {
1743 checkpoint_object(event, &["job_authority"], "job").or_else(|| {
1744 (event.kind == EventKind::Checkpoint
1745 && event.payload["checkpoint_kind"] == "job_authority"
1746 && event.payload.get("job_id").is_some())
1747 .then_some(&event.payload)
1748 })
1749 })
1750 .and_then(|value| parse_job_view(value).ok())
1751}
1752
1753fn latest_typed_run(events: &[Event]) -> Option<AuthoritativeRunView> {
1754 unique_latest(events, |event| {
1755 checkpoint_object(event, &["run_authority"], "run").or_else(|| {
1756 (event.kind == EventKind::Checkpoint
1757 && event.payload["checkpoint_kind"] == "run_authority"
1758 && event.payload.get("run_id").is_some())
1759 .then_some(&event.payload)
1760 })
1761 })
1762 .and_then(|value| parse_run_view(value).ok())
1763}
1764
1765fn latest_typed_recovery(
1766 events: &[Event],
1767 task: Option<&AuthoritativeTaskView>,
1768 job: Option<&AuthoritativeJobView>,
1769 run: Option<&AuthoritativeRunView>,
1770 source: Option<&str>,
1771) -> Option<AuthoritativeRecoveryView> {
1772 unique_latest(events, |event| {
1773 checkpoint_object(event, &["recovery_authority"], "recovery").or_else(|| {
1774 (event.kind == EventKind::Checkpoint
1775 && event.payload["checkpoint_kind"] == "recovery_authority"
1776 && event.payload.get("state").is_some())
1777 .then_some(&event.payload)
1778 })
1779 })
1780 .and_then(|value| parse_recovery_view(value, task, job, run, source).ok())
1781}
1782
1783fn parse_recovery_view(
1784 value: &Value,
1785 task: Option<&AuthoritativeTaskView>,
1786 job: Option<&AuthoritativeJobView>,
1787 run: Option<&AuthoritativeRunView>,
1788 source: Option<&str>,
1789) -> Result<AuthoritativeRecoveryView, String> {
1790 let object = value
1791 .as_object()
1792 .ok_or("recovery authority is not an object")?;
1793 reject_unknown_keys(
1794 object,
1795 &[
1796 "state",
1797 "reason",
1798 "action_available",
1799 "task_id",
1800 "falsegreen_task_id",
1801 "job_id",
1802 "run_id",
1803 "worker_id",
1804 "worker",
1805 "assignment_generation",
1806 "generation",
1807 "source_sha256",
1808 "authoritative_source_sha256",
1809 "candidate_sha256",
1810 "provenance",
1811 "authority_source",
1812 ],
1813 "recovery authority",
1814 )?;
1815 let state = match required_string(object, "state")?.as_str() {
1816 "RECOVERABLE" => RecoveryState::Recoverable,
1817 "BLOCKED" => RecoveryState::Blocked,
1818 "TERMINAL" => RecoveryState::Terminal,
1819 "UNKNOWN" => RecoveryState::Unknown,
1820 _ => return Err("unknown recovery state".to_owned()),
1821 };
1822 let task = task.ok_or("recovery authority requires Task context")?;
1823 let job = job.ok_or("recovery authority requires Job context")?;
1824 let run = run.ok_or("recovery authority requires Run context")?;
1825 let source = source.ok_or("recovery authority requires source context")?;
1826 let task_id = required_string_any(object, &["task_id", "falsegreen_task_id"])?;
1827 let job_id = required_string(object, "job_id")?;
1828 let run_id = required_string(object, "run_id")?;
1829 let worker_id = required_string_any(object, &["worker_id", "worker"])?;
1830 let assignment_generation =
1831 required_string_any(object, &["assignment_generation", "generation"])?;
1832 let source_sha256 =
1833 required_digest_any(object, &["source_sha256", "authoritative_source_sha256"])?;
1834 let candidate_sha256 = optional_digest_any(object, &["candidate_sha256"])?;
1835 let provenance = required_string_any(object, &["provenance", "authority_source"])?;
1836 if !matches!(
1837 provenance.as_str(),
1838 "canonical" | "falsegreen_core" | "falsegreen"
1839 ) {
1840 return Err("recovery provenance is not a known authority source".to_owned());
1841 }
1842 if task.task_id != task_id
1843 || job.id != job_id
1844 || run.id != run_id
1845 || run.worker_id.as_deref() != Some(worker_id.as_str())
1846 || run.assignment_generation.as_deref() != Some(assignment_generation.as_str())
1847 || source != source_sha256
1848 || candidate_sha256
1849 .as_deref()
1850 .is_some_and(|candidate| task.candidate_sha256.as_deref() != Some(candidate))
1851 {
1852 return Err("recovery authority binding conflicts with current graph".to_owned());
1853 }
1854 Ok(AuthoritativeRecoveryView {
1855 state,
1856 reason: optional_string(object, "reason"),
1857 action_available: object
1858 .get("action_available")
1859 .and_then(Value::as_bool)
1860 .unwrap_or(false),
1861 task_id,
1862 job_id,
1863 run_id,
1864 worker_id,
1865 assignment_generation,
1866 source_sha256,
1867 candidate_sha256,
1868 provenance,
1869 })
1870}
1871
1872fn unique_latest<'a, F>(events: &'a [Event], select: F) -> Option<&'a Value>
1873where
1874 F: FnMut(&'a Event) -> Option<&'a Value>,
1875{
1876 let matches = events.iter().filter_map(select).collect::<Vec<_>>();
1877 (matches.len() == 1).then(|| matches[0])
1878}
1879
1880fn checkpoint_object<'a>(event: &'a Event, kinds: &[&str], key: &str) -> Option<&'a Value> {
1881 if event.kind != EventKind::Checkpoint
1882 || !kinds
1883 .iter()
1884 .any(|kind| event.payload["checkpoint_kind"] == *kind)
1885 {
1886 return None;
1887 }
1888 event.payload.get(key).filter(|value| value.is_object())
1889}
1890
1891fn parse_task_view(value: &Value) -> Result<AuthoritativeTaskView, String> {
1892 let object = value.as_object().ok_or("task authority is not an object")?;
1893 reject_unknown_keys(
1894 object,
1895 &[
1896 "task_id",
1897 "lifecycle",
1898 "scope_id",
1899 "frozen_scope_id",
1900 "scope_digest",
1901 "frozen_scope_digest",
1902 "source_sha256",
1903 "authoritative_source_sha256",
1904 "candidate_sha256",
1905 "current_job_id",
1906 "job_id",
1907 "checkpoint_kind",
1908 "falsegreen_task_id",
1909 "task",
1910 "frozen_scope",
1911 "obligations",
1912 ],
1913 "task authority",
1914 )?;
1915 let task_id = required_string(object, "task_id")?;
1916 if !valid_identity(&task_id) {
1917 return Err("task ID is malformed".to_owned());
1918 }
1919 let lifecycle = optional_string(object, "lifecycle");
1920 if lifecycle
1921 .as_deref()
1922 .is_some_and(|value| !is_known_session_lifecycle(value))
1923 {
1924 return Err("task lifecycle is unknown".to_owned());
1925 }
1926 Ok(AuthoritativeTaskView {
1927 task_id,
1928 lifecycle,
1929 scope_id: required_string_any(object, &["scope_id", "frozen_scope_id"])?,
1930 scope_digest: required_digest_any(object, &["scope_digest", "frozen_scope_digest"])?,
1931 source_sha256: optional_digest_any(
1932 object,
1933 &["source_sha256", "authoritative_source_sha256"],
1934 )?,
1935 candidate_sha256: optional_digest_any(object, &["candidate_sha256"])?,
1936 current_job_id: optional_string_any_checked(object, &["current_job_id", "job_id"])?,
1937 })
1938}
1939
1940fn parse_scope_view(value: &Value) -> Result<AuthoritativeFrozenScopeView, String> {
1941 let object = value
1942 .as_object()
1943 .ok_or("frozen scope authority is not an object")?;
1944 reject_unknown_keys(
1945 object,
1946 &[
1947 "scope_id",
1948 "id",
1949 "scope_digest",
1950 "digest",
1951 "obligations",
1952 "checkpoint_kind",
1953 "falsegreen_task_id",
1954 "task",
1955 "frozen_scope",
1956 "job_id",
1957 ],
1958 "frozen scope authority",
1959 )?;
1960 let scope_id = required_string_any(object, &["scope_id", "id"])?;
1961 if !valid_identity(&scope_id) {
1962 return Err("frozen scope ID is malformed".to_owned());
1963 }
1964 let scope_digest = required_digest_any(object, &["scope_digest", "digest"])?;
1965 let values = object
1966 .get("obligations")
1967 .and_then(Value::as_array)
1968 .ok_or("frozen scope obligations are missing")?;
1969 if values.is_empty() {
1970 return Err("frozen scope obligations are empty".to_owned());
1971 }
1972 let mut ids = BTreeSet::new();
1973 let mut obligations = Vec::with_capacity(values.len());
1974 for value in values {
1975 let object = value
1976 .as_object()
1977 .ok_or("malformed frozen-scope obligation")?;
1978 let id = required_string_any(object, &["id", "obligation_id"])?;
1979 if !valid_identity(&id) {
1980 return Err("frozen-scope obligation ID is malformed".to_owned());
1981 }
1982 let requirement = required_string_any(object, &["requirement", "text", "description"])?;
1983 if !ids.insert(id.clone()) {
1984 return Err(format!("duplicate obligation ID {id}"));
1985 }
1986 obligations.push((id, requirement));
1987 }
1988 if scope_digest != scope_digest_for_obligations(&obligations) {
1989 return Err("frozen scope digest does not match displayed obligations".to_owned());
1990 }
1991 Ok(AuthoritativeFrozenScopeView {
1992 scope_id,
1993 scope_digest,
1994 obligations,
1995 })
1996}
1997
1998fn parse_job_view(value: &Value) -> Result<AuthoritativeJobView, String> {
1999 let object = value.as_object().ok_or("job authority is not an object")?;
2000 reject_unknown_keys(
2001 object,
2002 &[
2003 "job_id",
2004 "id",
2005 "status",
2006 "task_id",
2007 "source_sha256",
2008 "authoritative_source_sha256",
2009 "candidate_sha256",
2010 "run_ids",
2011 "entitlement_consumption",
2012 "consumption",
2013 "checkpoint_kind",
2014 ],
2015 "Job authority",
2016 )?;
2017 let id = required_string_any(object, &["job_id", "id"])?;
2018 if !valid_identity(&id) {
2019 return Err("Job ID is malformed".to_owned());
2020 }
2021 let status = parse_status_exact(required_string(object, "status")?.as_str())
2022 .ok_or("unknown job status")?;
2023 let task_id = required_string(object, "task_id")?;
2024 if !valid_identity(&task_id) {
2025 return Err("Job task ID is malformed".to_owned());
2026 }
2027 let source_sha256 =
2028 optional_digest_any(object, &["source_sha256", "authoritative_source_sha256"])?;
2029 let candidate_sha256 = optional_digest_any(object, &["candidate_sha256"])?;
2030 let run_ids = object.get("run_ids").map_or(Ok(Vec::new()), |value| {
2031 let run_ids: Vec<String> = value
2032 .as_array()
2033 .ok_or("job run_ids are malformed")?
2034 .iter()
2035 .map(|value| {
2036 value
2037 .as_str()
2038 .filter(|value| valid_identity(value))
2039 .map(str::to_owned)
2040 .ok_or_else(|| "job run ID is malformed".to_owned())
2041 })
2042 .collect::<Result<_, _>>()?;
2043 let unique = run_ids.iter().collect::<BTreeSet<_>>().len();
2044 (unique == run_ids.len())
2045 .then_some(run_ids)
2046 .ok_or_else(|| "duplicate Job run ID".to_owned())
2047 })?;
2048 Ok(AuthoritativeJobView {
2049 id,
2050 status,
2051 task_id,
2052 source_sha256,
2053 candidate_sha256,
2054 run_ids,
2055 entitlement_consumption: None,
2060 })
2061}
2062
2063fn parse_run_view(value: &Value) -> Result<AuthoritativeRunView, String> {
2064 let object = value.as_object().ok_or("run authority is not an object")?;
2065 reject_unknown_keys(
2066 object,
2067 &[
2068 "run_id",
2069 "id",
2070 "status",
2071 "job_id",
2072 "worker_id",
2073 "worker",
2074 "assignment_generation",
2075 "generation",
2076 "execution_started_at",
2077 "package_runtime_identity",
2078 "package_identity",
2079 "runtime_identity",
2080 "recovery_lease",
2081 "lease",
2082 "checkpoint_kind",
2083 ],
2084 "Run authority",
2085 )?;
2086 let id = required_string_any(object, &["run_id", "id"])?;
2087 if !valid_identity(&id) {
2088 return Err("Run ID is malformed".to_owned());
2089 }
2090 Ok(AuthoritativeRunView {
2091 id,
2092 status: parse_status_exact(required_string(object, "status")?.as_str())
2093 .ok_or("unknown run status")?,
2094 job_id: {
2095 let job_id = required_string(object, "job_id")?;
2096 if !valid_identity(&job_id) {
2097 return Err("Run Job ID is malformed".to_owned());
2098 }
2099 job_id
2100 },
2101 worker_id: optional_string_any_checked(object, &["worker_id", "worker"])?,
2102 assignment_generation: optional_string_any_checked(
2103 object,
2104 &["assignment_generation", "generation"],
2105 )?,
2106 execution_started_at: optional_string(object, "execution_started_at"),
2107 package_runtime_identity: optional_string_any_checked(
2108 object,
2109 &[
2110 "package_runtime_identity",
2111 "package_identity",
2112 "runtime_identity",
2113 ],
2114 )?,
2115 recovery_lease: optional_string_any_checked(object, &["recovery_lease", "lease"])?,
2116 })
2117}
2118
2119#[derive(Debug, Clone)]
2120struct ResultView {
2121 payload: Value,
2122 verdict: NativeVerdict,
2123 candidate_sha256: String,
2124 source_sha256: String,
2125 run_id: String,
2126 job_id: String,
2127 worker_id: Option<String>,
2128 assignment_generation: Option<String>,
2129 completion_authority: Option<String>,
2130 result_digest: String,
2131}
2132
2133impl AuthoritativeResultView {
2134 pub fn try_from_value(
2135 value: &Value,
2136 candidate: &str,
2137 source: Option<&str>,
2138 scope: Option<&AuthoritativeFrozenScopeView>,
2139 ) -> Result<Self, String> {
2140 if source.is_none() {
2141 return Err("trusted current source digest is required".to_owned());
2142 }
2143 let result = parse_result_view(value, Some(candidate), source, scope)?;
2144 Ok(Self {
2145 verdict: result.verdict,
2146 candidate_sha256: result.candidate_sha256,
2147 source_sha256: result.source_sha256,
2148 run_id: result.run_id,
2149 job_id: result.job_id,
2150 worker_id: result.worker_id,
2151 assignment_generation: result.assignment_generation,
2152 result_digest: result.result_digest,
2153 })
2154 }
2155
2156 pub fn from_value(
2157 value: &Value,
2158 candidate: &str,
2159 source: Option<&str>,
2160 scope: Option<&AuthoritativeFrozenScopeView>,
2161 ) -> Result<Self, String> {
2162 Self::try_from_value(value, candidate, source, scope)
2163 }
2164}
2165
2166fn latest_typed_result(
2167 events: &[Event],
2168 candidate: Option<&str>,
2169 source: Option<&str>,
2170 scope: Option<&AuthoritativeFrozenScopeView>,
2171 job: Option<&AuthoritativeJobView>,
2172 run: Option<&AuthoritativeRunView>,
2173) -> Option<ResultView> {
2174 if events
2175 .iter()
2176 .filter(|event| event.kind == EventKind::FalsegreenResult)
2177 .count()
2178 != 1
2179 {
2180 return None;
2181 }
2182 let event = events
2183 .iter()
2184 .rev()
2185 .find(|event| event.kind == EventKind::FalsegreenResult)?;
2186 let result = parse_result_view(&event.payload, candidate, source, scope).ok()?;
2187 if job.is_some_and(|job| {
2188 job.id == result.job_id && lifecycle_allows_result(job.status, result.verdict)
2189 }) && run.is_some_and(|run| {
2190 run.id == result.run_id && lifecycle_allows_result(run.status, result.verdict)
2191 }) {
2192 Some(result)
2193 } else {
2194 None
2195 }
2196}
2197
2198fn lifecycle_allows_result(status: NativeStatus, verdict: NativeVerdict) -> bool {
2199 match status {
2200 NativeStatus::Accepted => verdict == NativeVerdict::Accepted,
2201 NativeStatus::Failed => matches!(
2202 verdict,
2203 NativeVerdict::Failed | NativeVerdict::InsufficientEvidence
2204 ),
2205 NativeStatus::InsufficientEvidence => verdict == NativeVerdict::InsufficientEvidence,
2206 NativeStatus::Terminal => true,
2207 NativeStatus::Queued
2208 | NativeStatus::Claimed
2209 | NativeStatus::Running
2210 | NativeStatus::Executing
2211 | NativeStatus::Recoverable
2212 | NativeStatus::Unknown => false,
2213 }
2214}
2215
2216fn parse_result_view(
2217 payload: &Value,
2218 candidate: Option<&str>,
2219 source: Option<&str>,
2220 scope: Option<&AuthoritativeFrozenScopeView>,
2221) -> Result<ResultView, String> {
2222 let object = payload.as_object().ok_or("result is not an object")?;
2223 reject_unknown_keys(
2224 object,
2225 &[
2226 "verification",
2227 "verification_status",
2228 "candidate_sha256",
2229 "authoritative_source_sha256",
2230 "source_sha256",
2231 "run_id",
2232 "job_id",
2233 "worker_id",
2234 "worker",
2235 "assignment_generation",
2236 "generation",
2237 "completion_authority",
2238 "obligation_outcomes",
2239 "obligations",
2240 "evidence",
2241 "error",
2242 "repairable",
2243 ],
2244 "result authority",
2245 )?;
2246 let raw = object
2247 .get("verification")
2248 .and_then(Value::as_str)
2249 .ok_or("result verification is missing")?;
2250 let verdict = parse_verdict_exact(raw).ok_or("result verdict is not canonical")?;
2251 let status = object
2252 .get("verification_status")
2253 .and_then(Value::as_str)
2254 .ok_or("result verification_status is missing")?;
2255 if parse_verdict_exact(status) != Some(verdict)
2256 || !matches!(status, "accepted" | "failed" | "insufficient_evidence")
2257 {
2258 return Err("result verification fields disagree".to_owned());
2259 }
2260 let candidate_sha256 = object
2261 .get("candidate_sha256")
2262 .and_then(Value::as_str)
2263 .filter(|value| valid_digest(value))
2264 .ok_or("result candidate digest is malformed")?;
2265 if candidate != Some(candidate_sha256) {
2266 return Err("result candidate does not match current candidate".to_owned());
2267 }
2268 let expected_source = source.ok_or("current trusted source digest is unavailable")?;
2269 let source_sha256 =
2270 required_digest_any(object, &["authoritative_source_sha256", "source_sha256"])?;
2271 if source_sha256 != expected_source {
2272 return Err("result source digest conflicts with current source".to_owned());
2273 }
2274 let run_id = required_string(object, "run_id")?;
2275 let job_id = required_string(object, "job_id")?;
2276 if !valid_identity(&run_id) || !valid_identity(&job_id) {
2277 return Err("result Run/Job binding is malformed".to_owned());
2278 }
2279 for key in ["worker_id", "worker", "assignment_generation", "generation"] {
2280 if let Some(value) = object.get(key)
2281 && !value.is_null()
2282 && value.as_str().filter(|value| !value.is_empty()).is_none()
2283 {
2284 return Err(format!("result {key} binding is malformed"));
2285 }
2286 }
2287 if let Some(values) = object
2288 .get("obligation_outcomes")
2289 .or_else(|| object.get("obligations"))
2290 {
2291 let scope = scope.ok_or("result obligation outcomes have no frozen scope authority")?;
2292 let values = values
2293 .as_array()
2294 .ok_or("result obligation outcomes are malformed")?;
2295 let ids = values
2296 .iter()
2297 .filter_map(|value| {
2298 value
2299 .get("obligation_id")
2300 .or_else(|| value.get("id"))
2301 .and_then(Value::as_str)
2302 })
2303 .collect::<Vec<_>>();
2304 if ids.len() != scope.obligations.len()
2305 || ids
2306 .iter()
2307 .any(|id| !scope.obligations.iter().any(|(expected, _)| expected == id))
2308 || ids.iter().collect::<BTreeSet<_>>().len() != ids.len()
2309 {
2310 return Err("result obligation outcomes do not exactly bind frozen scope".to_owned());
2311 }
2312 let mut outcome_ids = BTreeSet::new();
2313 for value in values {
2314 let outcome = value
2315 .as_object()
2316 .ok_or("result obligation outcome is malformed")?;
2317 let id = required_string_any(outcome, &["obligation_id", "id"])?;
2318 if !outcome_ids.insert(id) {
2319 return Err("duplicate result obligation outcome".to_owned());
2320 }
2321 let raw_status = required_string_any(outcome, &["status", "outcome"])?;
2322 if parse_evidence_exact(&raw_status).is_none() {
2323 return Err("result obligation outcome status is unknown".to_owned());
2324 }
2325 }
2326 }
2327 Ok(ResultView {
2328 payload: payload.clone(),
2329 verdict,
2330 candidate_sha256: candidate_sha256.to_owned(),
2331 source_sha256,
2332 run_id,
2333 job_id,
2334 worker_id: optional_string_any_checked(object, &["worker_id", "worker"])?,
2335 assignment_generation: optional_string_any_checked(
2336 object,
2337 &["assignment_generation", "generation"],
2338 )?,
2339 completion_authority: object
2340 .get("completion_authority")
2341 .map(parse_completion_authority)
2342 .transpose()?,
2343 result_digest: sha256_json(payload),
2344 })
2345}
2346
2347fn parse_completion_authority(value: &Value) -> Result<String, String> {
2348 let object = value
2349 .as_object()
2350 .ok_or("result completion authority is malformed")?;
2351 reject_unknown_keys(
2352 object,
2353 &["authority_ready", "may_claim_complete"],
2354 "result completion authority",
2355 )?;
2356 if !object.get("authority_ready").is_some_and(Value::is_boolean)
2357 || !object
2358 .get("may_claim_complete")
2359 .is_some_and(Value::is_boolean)
2360 {
2361 return Err("result completion authority is malformed".to_owned());
2362 }
2363 Ok(display_json(value))
2364}
2365
2366#[derive(Debug, Clone)]
2367struct ReportView {
2368 id: String,
2369 digest: String,
2370 candidate_sha256: String,
2371 source_sha256: String,
2372 verdict: NativeVerdict,
2373 created_at: Option<String>,
2374 provenance: String,
2375 evidence_summary: Option<String>,
2376}
2377
2378impl AuthoritativeReportView {
2379 #[allow(clippy::too_many_arguments)]
2380 pub fn try_from_value(
2381 value: &Value,
2382 candidate: &str,
2383 source: Option<&str>,
2384 job: Option<&AuthoritativeJobView>,
2385 run: Option<&AuthoritativeRunView>,
2386 result: Option<&AuthoritativeResultView>,
2387 ) -> Result<Self, String> {
2388 let result = result.ok_or("report requires a validated parent Result")?;
2389 let job = job.ok_or("report requires a validated Job binding")?;
2390 let run = run.ok_or("report requires a validated Run binding")?;
2391 let source = source.ok_or("report requires a trusted source binding")?;
2392 if result.candidate_sha256() != candidate
2393 || result.source_sha256() != source
2394 || result.job_id() != job.id()
2395 || result.run_id() != run.id()
2396 {
2397 return Err("report parent Result bindings are not exact".to_owned());
2398 }
2399 let result_view = ResultView {
2400 payload: Value::Null,
2401 verdict: result.verdict,
2402 candidate_sha256: result.candidate_sha256.clone(),
2403 source_sha256: result.source_sha256.clone(),
2404 run_id: result.run_id.clone(),
2405 job_id: result.job_id.clone(),
2406 worker_id: None,
2407 assignment_generation: None,
2408 completion_authority: None,
2409 result_digest: result.result_digest.clone(),
2410 };
2411 let report = parse_report_value(
2412 value,
2413 Some(candidate),
2414 Some(source),
2415 Some(job),
2416 Some(run),
2417 Some(&result_view),
2418 )?;
2419 let object = value
2420 .as_object()
2421 .ok_or("report authority is not an object")?;
2422 Ok(Self {
2423 id: report.id,
2424 report_digest: report.digest,
2425 candidate_sha256: report.candidate_sha256,
2426 source_sha256: report.source_sha256,
2427 verdict: report.verdict,
2428 job_id: required_string(object, "job_id")?,
2429 run_id: required_string(object, "run_id")?,
2430 result_digest: required_digest_any(object, &["result_digest"])?,
2431 })
2432 }
2433
2434 #[allow(clippy::too_many_arguments)]
2435 pub fn from_value(
2436 value: &Value,
2437 candidate: &str,
2438 source: Option<&str>,
2439 job: Option<&AuthoritativeJobView>,
2440 run: Option<&AuthoritativeRunView>,
2441 result: Option<&AuthoritativeResultView>,
2442 ) -> Result<Self, String> {
2443 Self::try_from_value(value, candidate, source, job, run, result)
2444 }
2445}
2446
2447fn latest_typed_report(
2448 events: &[Event],
2449 candidate: Option<&str>,
2450 source: Option<&str>,
2451 job: Option<&AuthoritativeJobView>,
2452 run: Option<&AuthoritativeRunView>,
2453 result: Option<&ResultView>,
2454) -> Option<ReportView> {
2455 let value = unique_latest(events, |event| {
2456 checkpoint_object(event, &["report_authority"], "report")
2457 })?;
2458 let result = result?;
2459 let job = job?;
2460 let run = run?;
2461 let source = source?;
2462 let object = value.as_object()?;
2463 reject_unknown_keys(
2464 object,
2465 &[
2466 "report_id",
2467 "id",
2468 "report_digest",
2469 "digest",
2470 "candidate_sha256",
2471 "source_sha256",
2472 "authoritative_source_sha256",
2473 "verdict",
2474 "job_id",
2475 "run_id",
2476 "result_digest",
2477 "provenance",
2478 "authority_source",
2479 "created_at",
2480 "created_at_ms",
2481 "key_evidence_summary",
2482 "summary",
2483 ],
2484 "report authority",
2485 )
2486 .ok()?;
2487 let id = required_string_any(object, &["report_id", "id"]).ok()?;
2488 if !valid_identity(&id) {
2489 return None;
2490 }
2491 let digest = required_report_digest(object).ok()?;
2492 let report_candidate = required_digest_any(object, &["candidate_sha256"]).ok()?;
2493 if candidate != Some(report_candidate.as_str()) {
2494 return None;
2495 }
2496 let report_source =
2497 required_digest_any(object, &["source_sha256", "authoritative_source_sha256"]).ok()?;
2498 if report_source != source {
2499 return None;
2500 }
2501 if result.candidate_sha256 != report_candidate
2502 || result.source_sha256 != report_source
2503 || result.job_id != job.id
2504 || result.run_id != run.id
2505 {
2506 return None;
2507 }
2508 let raw_verdict = object.get("verdict").and_then(Value::as_str)?;
2509 let verdict = parse_verdict_exact(raw_verdict)?;
2510 if result.verdict != verdict {
2511 return None;
2512 }
2513 if !matches!(raw_verdict, "accepted" | "failed" | "insufficient_evidence") {
2514 return None;
2515 }
2516 let job_id = optional_string(object, "job_id");
2517 if job_id.as_deref() != Some(job.id.as_str()) {
2518 return None;
2519 }
2520 let run_id = optional_string(object, "run_id");
2521 if run_id.as_deref() != Some(run.id.as_str()) {
2522 return None;
2523 }
2524 if object.get("result_digest").and_then(Value::as_str) != Some(result.result_digest.as_str()) {
2525 return None;
2526 }
2527 let provenance = required_string_any(object, &["provenance", "authority_source"]).ok()?;
2528 if !matches!(
2529 provenance.as_str(),
2530 "canonical" | "falsegreen_core" | "falsegreen"
2531 ) {
2532 return None;
2533 }
2534 Some(ReportView {
2535 id,
2536 digest,
2537 candidate_sha256: report_candidate,
2538 source_sha256: report_source,
2539 verdict,
2540 created_at: optional_string_any_checked(object, &["created_at", "created_at_ms"]).ok()?,
2541 provenance,
2542 evidence_summary: optional_string_any_checked(object, &["key_evidence_summary", "summary"])
2543 .ok()?,
2544 })
2545}
2546
2547fn parse_report_value(
2548 value: &Value,
2549 candidate: Option<&str>,
2550 source: Option<&str>,
2551 job: Option<&AuthoritativeJobView>,
2552 run: Option<&AuthoritativeRunView>,
2553 result: Option<&ResultView>,
2554) -> Result<ReportView, String> {
2555 let object = value
2556 .as_object()
2557 .ok_or("report authority is not an object")?;
2558 reject_unknown_keys(
2559 object,
2560 &[
2561 "report_id",
2562 "id",
2563 "report_digest",
2564 "digest",
2565 "candidate_sha256",
2566 "source_sha256",
2567 "authoritative_source_sha256",
2568 "verdict",
2569 "job_id",
2570 "run_id",
2571 "result_digest",
2572 "provenance",
2573 "authority_source",
2574 "created_at",
2575 "created_at_ms",
2576 "key_evidence_summary",
2577 "summary",
2578 ],
2579 "report authority",
2580 )?;
2581 let id = required_string_any(object, &["report_id", "id"])?;
2582 if !valid_identity(&id) {
2583 return Err("report ID is malformed".to_owned());
2584 }
2585 let digest = required_report_digest(object)?;
2586 let report_candidate = required_digest_any(object, &["candidate_sha256"])?;
2587 if candidate != Some(report_candidate.as_str()) {
2588 return Err("report candidate does not match current candidate".to_owned());
2589 }
2590 let expected_source = source.ok_or("report requires a trusted source binding")?;
2591 let report_source =
2592 required_digest_any(object, &["source_sha256", "authoritative_source_sha256"])?;
2593 if report_source != expected_source {
2594 return Err("report source does not match current source".to_owned());
2595 }
2596 let raw_verdict = object
2597 .get("verdict")
2598 .and_then(Value::as_str)
2599 .ok_or("report verdict is missing")?;
2600 let verdict = parse_verdict_exact(raw_verdict).ok_or("report verdict is not canonical")?;
2601 let result = result.ok_or("report requires a validated parent Result")?;
2602 let job = job.ok_or("report requires a validated Job binding")?;
2603 let run = run.ok_or("report requires a validated Run binding")?;
2604 if result.candidate_sha256 != report_candidate
2605 || result.source_sha256 != report_source
2606 || result.job_id != job.id
2607 || result.run_id != run.id
2608 {
2609 return Err("report parent Result bindings are not exact".to_owned());
2610 }
2611 if result.verdict != verdict {
2612 return Err("report verdict conflicts with result".to_owned());
2613 }
2614 let job_id = required_string(object, "job_id")?;
2615 if job.id != job_id {
2616 return Err("report Job binding conflicts".to_owned());
2617 }
2618 let run_id = required_string(object, "run_id")?;
2619 if run.id != run_id {
2620 return Err("report Run binding conflicts".to_owned());
2621 }
2622 let expected = required_digest_any(object, &["result_digest"])?;
2623 if expected != result.result_digest {
2624 return Err("report result binding conflicts".to_owned());
2625 }
2626 let provenance = required_string_any(object, &["provenance", "authority_source"])?;
2627 if !matches!(
2628 provenance.as_str(),
2629 "canonical" | "falsegreen_core" | "falsegreen"
2630 ) {
2631 return Err("report provenance is not a known authority source".to_owned());
2632 }
2633 Ok(ReportView {
2634 id,
2635 digest,
2636 candidate_sha256: report_candidate,
2637 source_sha256: report_source,
2638 verdict,
2639 created_at: optional_string_any_checked(object, &["created_at", "created_at_ms"])?,
2640 provenance,
2641 evidence_summary: optional_string_any_checked(
2642 object,
2643 &["key_evidence_summary", "summary"],
2644 )?,
2645 })
2646}
2647
2648fn verification_from_result(
2649 result: &ResultView,
2650 scope: Option<&AuthoritativeFrozenScopeView>,
2651) -> NativeVerification {
2652 NativeVerification {
2653 verdict: result.verdict,
2654 raw_verdict: NativeField::falsegreen(status_wire_verdict(result.verdict)),
2655 source_bound: true,
2656 candidate_sha256: NativeField::falsegreen(result.candidate_sha256.clone()),
2657 authoritative_source_sha256: Some(result.source_sha256.as_str()).map_or_else(
2658 || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2659 NativeField::falsegreen,
2660 ),
2661 result_digest: NativeField::falsegreen(result.result_digest.clone()),
2662 report_reference: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2663 completion_authority: result.completion_authority.as_deref().map_or_else(
2664 || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2665 NativeField::falsegreen,
2666 ),
2667 obligation_outcomes: parse_obligation_outcomes(&result.payload, scope),
2668 certification_note: "Canonical result is displayed exactly; this read-only surface cannot certify or override it.".to_owned(),
2669 binding_error: None,
2670 }
2671}
2672
2673fn unavailable_verification() -> NativeVerification {
2674 NativeVerification {
2675 verdict: NativeVerdict::Unknown,
2676 raw_verdict: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2677 source_bound: false,
2678 candidate_sha256: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2679 authoritative_source_sha256: NativeField::unavailable(
2680 AuthorityClass::AuthoritativeFalseGreenState,
2681 ),
2682 result_digest: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2683 report_reference: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2684 completion_authority: NativeField::unavailable(
2685 AuthorityClass::AuthoritativeFalseGreenState,
2686 ),
2687 obligation_outcomes: Vec::new(),
2688 certification_note: "FalseGreen state unavailable: no validated result authority."
2689 .to_owned(),
2690 binding_error: Some("no validated result authority".to_owned()),
2691 }
2692}
2693
2694fn unavailable_job() -> NativeJob {
2695 NativeJob {
2696 id: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2697 status: NativeStatus::Unknown,
2698 status_raw: NativeField::unavailable(AuthorityClass::DiagnosticNonAuthoritativeMetadata),
2699 task_id: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2700 source_sha256: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2701 candidate_sha256: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2702 entitlement_consumption: NativeField::unavailable(
2703 AuthorityClass::AuthoritativeAgentSessionState,
2704 ),
2705 run_ids: Vec::new(),
2706 authority_decision: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2707 }
2708}
2709
2710fn unavailable_run() -> NativeRun {
2711 NativeRun {
2712 id: NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
2713 worker_id: NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
2714 job_id: NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
2715 assignment_generation: NativeField::unavailable(
2716 AuthorityClass::AuthoritativeAgentSessionState,
2717 ),
2718 status: NativeStatus::Unknown,
2719 status_raw: NativeField::unavailable(AuthorityClass::DiagnosticNonAuthoritativeMetadata),
2720 execution_started_at: NativeField::unavailable(
2721 AuthorityClass::AuthoritativeAgentSessionState,
2722 ),
2723 package_runtime_identity: NativeField::unavailable(
2724 AuthorityClass::AuthoritativeAgentSessionState,
2725 ),
2726 recovery_lease: NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
2727 }
2728}
2729
2730fn unavailable_scope() -> NativeFrozenScope {
2731 NativeFrozenScope {
2732 scope_digest: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2733 frozen: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2734 obligations: Vec::new(),
2735 distinction:
2736 "FalseGreen frozen-scope authority unavailable; no obligation is displayed as frozen."
2737 .to_owned(),
2738 }
2739}
2740
2741fn unavailable_report(verification: &NativeVerification) -> NativeReport {
2742 NativeReport {
2743 id: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2744 report_digest: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2745 candidate_sha256: verification.candidate_sha256.clone(),
2746 source_sha256: verification.authoritative_source_sha256.clone(),
2747 verdict: NativeVerdict::Unknown,
2748 key_evidence_summary: NativeField::unavailable(
2749 AuthorityClass::AuthoritativeFalseGreenState,
2750 ),
2751 created_at: NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
2752 provenance: NativeField::diagnostic(
2753 "Report: Unavailable; no real authoritative report object",
2754 ),
2755 }
2756}
2757
2758fn report_from_view(view: &ReportView) -> NativeReport {
2759 NativeReport {
2760 id: NativeField::falsegreen(view.id.clone()),
2761 report_digest: NativeField::falsegreen(view.digest.clone()),
2762 candidate_sha256: NativeField::falsegreen(view.candidate_sha256.clone()),
2763 source_sha256: Some(view.source_sha256.as_str()).map_or_else(
2764 || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2765 NativeField::falsegreen,
2766 ),
2767 verdict: view.verdict,
2768 key_evidence_summary: view.evidence_summary.as_deref().map_or_else(
2769 || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2770 NativeField::falsegreen,
2771 ),
2772 created_at: view.created_at.as_deref().map_or_else(
2773 || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2774 NativeField::falsegreen,
2775 ),
2776 provenance: Some(view.provenance.as_str()).map_or_else(
2777 || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
2778 NativeField::falsegreen,
2779 ),
2780 }
2781}
2782
2783fn evidence_from_result(
2784 result: &ResultView,
2785 scope: Option<&AuthoritativeFrozenScopeView>,
2786 current_job_id: Option<&str>,
2787 current_run_id: Option<&str>,
2788 current_source: Option<&str>,
2789) -> NativeEvidence {
2790 let Some(scope) = scope else {
2791 return unavailable_evidence("Evidence unavailable: frozen scope authority is missing.");
2792 };
2793 let Some(values) = result
2794 .payload
2795 .get("evidence")
2796 .map(strict_evidence_array)
2797 .transpose()
2798 .ok()
2799 .flatten()
2800 .flatten()
2801 else {
2802 return unavailable_evidence(
2803 "Evidence unavailable: result supplied no exact evidence array.",
2804 );
2805 };
2806 let declared_rows = result
2807 .payload
2808 .get("evidence")
2809 .and_then(|value| {
2810 value
2811 .get("declared_rows")
2812 .or_else(|| value.get("declared_total"))
2813 })
2814 .and_then(Value::as_u64)
2815 .and_then(|value| usize::try_from(value).ok());
2816 if result
2817 .payload
2818 .get("evidence")
2819 .is_some_and(evidence_is_partial)
2820 {
2821 return unavailable_evidence_with_counts(
2822 "Evidence unavailable: upstream evidence set is explicitly partial or truncated.",
2823 values.len(),
2824 0,
2825 declared_rows,
2826 [0; 4],
2827 );
2828 }
2829 if values.len() > MAX_EVIDENCE_ITEMS {
2830 return unavailable_evidence_with_counts(
2831 "Evidence unavailable: upstream evidence set exceeds validation bound.",
2832 values.len(),
2833 0,
2834 declared_rows,
2835 [0; 4],
2836 );
2837 }
2838 let mut ids = BTreeSet::new();
2839 let mut duplicate_ids = Vec::new();
2840 let mut items = Vec::with_capacity(values.len().min(MAX_DISPLAY_EVIDENCE));
2841 let mut counts = [0usize; 4];
2842 let expected_job_id = Some(result.job_id.as_str()).or(current_job_id);
2843 let expected_run_id = Some(result.run_id.as_str()).or(current_run_id);
2844 let expected_source = Some(result.source_sha256.as_str()).or(current_source);
2845 let mut invalid_rows = 0usize;
2846 for (index, value) in values.iter().enumerate() {
2847 let parsed = parse_evidence_item(
2848 value,
2849 scope,
2850 expected_job_id,
2851 expected_run_id,
2852 expected_source,
2853 Some(result.result_digest.as_str()),
2854 );
2855 let Ok((id, item, state)) = parsed else {
2856 invalid_rows += 1;
2857 continue;
2858 };
2859 if !ids.insert(id.clone()) {
2860 duplicate_ids.push(id);
2861 }
2862 match state {
2863 EvidenceState::Passed => counts[0] += 1,
2864 EvidenceState::Failed => counts[1] += 1,
2865 EvidenceState::Insufficient => counts[2] += 1,
2866 EvidenceState::Unknown => counts[3] += 1,
2867 }
2868 if index < MAX_DISPLAY_EVIDENCE {
2869 items.push(item);
2870 }
2871 }
2872 duplicate_ids.sort();
2873 duplicate_ids.dedup();
2874 if !duplicate_ids.is_empty() {
2875 return NativeEvidence {
2876 items: Vec::new(),
2879 complete: false,
2880 duplicate_ids: duplicate_ids.clone(),
2881 unavailable_reason: Some(format!(
2882 "Duplicate evidence ID(s): {}",
2883 duplicate_ids.join(", ")
2884 )),
2885 total: values.len(),
2886 passed: counts[0],
2887 failed: counts[1],
2888 insufficient: counts[2],
2889 unknown: counts[3],
2890 observed_rows: values.len(),
2891 invalid_rows: invalid_rows + duplicate_ids.len(),
2892 declared_rows,
2893 };
2894 }
2895 if invalid_rows > 0 {
2896 return NativeEvidence {
2897 items: Vec::new(),
2898 complete: false,
2899 duplicate_ids,
2900 unavailable_reason: Some(format!(
2901 "Evidence unavailable: {invalid_rows} invalid row(s); diagnostic counts preserve observed data"
2902 )),
2903 total: values.len(),
2904 passed: counts[0],
2905 failed: counts[1],
2906 insufficient: counts[2],
2907 unknown: counts[3],
2908 observed_rows: values.len(),
2909 invalid_rows,
2910 declared_rows,
2911 };
2912 }
2913 NativeEvidence {
2914 items,
2915 complete: true,
2916 duplicate_ids,
2917 unavailable_reason: (values.len() > MAX_DISPLAY_EVIDENCE).then(|| {
2918 format!(
2919 "Showing {} of {} validated evidence items",
2920 MAX_DISPLAY_EVIDENCE,
2921 values.len()
2922 )
2923 }),
2924 total: values.len(),
2925 passed: counts[0],
2926 failed: counts[1],
2927 insufficient: counts[2],
2928 unknown: counts[3],
2929 observed_rows: values.len(),
2930 invalid_rows: 0,
2931 declared_rows,
2932 }
2933}
2934
2935fn unavailable_evidence(reason: &str) -> NativeEvidence {
2936 unavailable_evidence_with_counts(reason, 0, 0, None, [0; 4])
2937}
2938
2939fn unavailable_evidence_with_counts(
2940 reason: &str,
2941 observed_rows: usize,
2942 invalid_rows: usize,
2943 declared_rows: Option<usize>,
2944 counts: [usize; 4],
2945) -> NativeEvidence {
2946 NativeEvidence {
2947 items: Vec::new(),
2948 complete: false,
2949 duplicate_ids: Vec::new(),
2950 unavailable_reason: Some(reason.to_owned()),
2951 total: observed_rows,
2952 passed: counts[0],
2953 failed: counts[1],
2954 insufficient: counts[2],
2955 unknown: counts[3],
2956 observed_rows,
2957 invalid_rows,
2958 declared_rows,
2959 }
2960}
2961
2962fn strict_evidence_array(value: &Value) -> Result<Option<&Vec<Value>>, String> {
2963 if let Some(values) = value.as_array() {
2964 return Ok(Some(values));
2965 }
2966 let Some(object) = value.as_object() else {
2967 return Ok(None);
2968 };
2969 let mut selected: Option<&Vec<Value>> = None;
2970 for key in ["items", "evidence", "obligations"] {
2971 let Some(candidate) = object.get(key) else {
2972 continue;
2973 };
2974 let values = candidate
2975 .as_array()
2976 .ok_or_else(|| format!("evidence alias {key} is malformed"))?;
2977 if selected.is_some_and(|existing| existing != values) {
2978 return Err("conflicting evidence array aliases".to_owned());
2979 }
2980 selected = Some(values);
2981 }
2982 Ok(selected)
2983}
2984
2985fn evidence_is_partial(value: &Value) -> bool {
2986 value.get("partial").and_then(Value::as_bool) == Some(true)
2987 || value.get("truncated").and_then(Value::as_bool) == Some(true)
2988}
2989
2990fn parse_evidence_item(
2991 value: &Value,
2992 scope: &AuthoritativeFrozenScopeView,
2993 expected_job_id: Option<&str>,
2994 expected_run_id: Option<&str>,
2995 expected_source: Option<&str>,
2996 expected_result_digest: Option<&str>,
2997) -> Result<(String, NativeEvidenceItem, EvidenceState), String> {
2998 let object = value.as_object().ok_or("evidence item is not an object")?;
2999 reject_unknown_keys(
3000 object,
3001 &[
3002 "evidence_id",
3003 "id",
3004 "obligation_id",
3005 "job_id",
3006 "run_id",
3007 "source_sha256",
3008 "source_digest",
3009 "result_digest",
3010 "provenance",
3011 "source",
3012 "evidence_type",
3013 "type",
3014 "status",
3015 "state",
3016 "outcome",
3017 "detail",
3018 "value",
3019 "message",
3020 "reference",
3021 "uri",
3022 "path",
3023 ],
3024 "evidence item",
3025 )?;
3026 let id = required_string_any(object, &["evidence_id", "id"])?;
3027 if !valid_identity(&id) {
3028 return Err("evidence ID is malformed".to_owned());
3029 }
3030 let obligation = required_string_any(object, &["obligation_id"])?;
3031 if !valid_identity(&obligation) {
3032 return Err("evidence obligation ID is malformed".to_owned());
3033 }
3034 if !scope
3035 .obligations
3036 .iter()
3037 .any(|(expected, _)| expected == &obligation)
3038 {
3039 return Err("evidence obligation is not in frozen scope".to_owned());
3040 }
3041 if let Some(job_id) = object.get("job_id").and_then(Value::as_str)
3042 && expected_job_id.is_some()
3043 && expected_job_id != Some(job_id)
3044 {
3045 return Err("evidence Job binding conflicts".to_owned());
3046 }
3047 if let Some(run_id) = object.get("run_id").and_then(Value::as_str)
3048 && expected_run_id.is_some()
3049 && expected_run_id != Some(run_id)
3050 {
3051 return Err("evidence Run binding conflicts".to_owned());
3052 }
3053 if let Some(source_digest) = optional_digest_any(object, &["source_sha256", "source_digest"])?
3054 && expected_source.is_some()
3055 && expected_source != Some(source_digest.as_str())
3056 {
3057 return Err("evidence source digest conflicts".to_owned());
3058 }
3059 if let Some(result_digest) = object.get("result_digest") {
3060 let result_digest = result_digest
3061 .as_str()
3062 .filter(|value| valid_digest(value))
3063 .ok_or("evidence result digest is malformed")?;
3064 if expected_result_digest.is_some() && expected_result_digest != Some(result_digest) {
3065 return Err("evidence result digest conflicts".to_owned());
3066 }
3067 }
3068 let source = required_string_any(object, &["provenance", "source"])?;
3069 if !matches!(
3070 source.as_str(),
3071 "canonical" | "falsegreen_core" | "falsegreen"
3072 ) {
3073 return Err("evidence provenance is not a known authority source".to_owned());
3074 }
3075 let evidence_type = required_string_any(object, &["evidence_type", "type"])?;
3076 if !matches!(
3077 evidence_type.as_str(),
3078 "test" | "gate" | "artifact" | "log" | "report"
3079 ) {
3080 return Err("evidence type is unknown".to_owned());
3081 }
3082 let raw_state = required_string_any(object, &["status", "state", "outcome"])?;
3083 let state = parse_evidence_exact(&raw_state).ok_or("evidence status is unknown")?;
3084 let detail = strict_display_alias(object, &["detail", "value", "message"])?
3085 .unwrap_or_else(|| "Unavailable".to_owned());
3086 let reference = optional_string_any_checked(object, &["reference", "uri", "path"])?
3087 .unwrap_or_else(|| "Unavailable".to_owned());
3088 let item = NativeEvidenceItem {
3089 evidence_id: NativeField::falsegreen(id.clone()),
3090 obligation: NativeField::falsegreen(obligation),
3091 source: NativeField::falsegreen(source),
3092 evidence_type: NativeField::falsegreen(evidence_type),
3093 reference: NativeField::falsegreen(reference),
3094 state,
3095 detail: NativeField::falsegreen(detail),
3096 };
3097 Ok((id, item, state))
3098}
3099
3100fn recovery_unknown(run: &NativeRun) -> NativeRecovery {
3101 NativeRecovery {
3102 state: RecoveryState::Unknown,
3103 reason: NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
3104 pre_begin_claim: if run.status == NativeStatus::Claimed
3105 && run.execution_started_at.value.is_none()
3106 {
3107 NativeField::agent("pre_begin")
3108 } else {
3109 NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState)
3110 },
3111 action_available: false,
3112 distinction: "Recovery classification is unavailable without an explicit recovery-authority object; no inference is made from CLAIMED, leases, terminal reasons, or result pointers.".to_owned(),
3113 }
3114}
3115
3116fn recovery_from_view(view: &AuthoritativeRecoveryView) -> NativeRecovery {
3117 NativeRecovery {
3118 state: view.state,
3119 reason: view.reason.as_deref().map_or_else(
3120 || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
3121 NativeField::falsegreen,
3122 ),
3123 pre_begin_claim: NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
3124 action_available: false,
3125 distinction: "Recovery is read-only state. No retry, recover, repair, resume, or mutation action is exposed.".to_owned(),
3126 }
3127}
3128
3129fn replacement_from_capture(
3130 capture: &Capture,
3131 verification: &NativeVerification,
3132) -> NativeReplacement {
3133 let Some(relation) = &capture.relation else {
3134 return NativeReplacement {
3135 predecessor_session: NativeField::unavailable(
3136 AuthorityClass::AuthoritativeAgentSessionState,
3137 ),
3138 replacement_session: NativeField::unavailable(
3139 AuthorityClass::AuthoritativeAgentSessionState,
3140 ),
3141 predecessor_state: NativeField::unavailable(
3142 AuthorityClass::AuthoritativeAgentSessionState,
3143 ),
3144 continuity: NativeField::diagnostic("No replacement relation for this session"),
3145 candidate_sha256: verification.candidate_sha256.clone(),
3146 workspace_state: NativeField::unavailable(
3147 AuthorityClass::AuthoritativeAgentSessionState,
3148 ),
3149 terminal_outcome: NativeField::unavailable(
3150 AuthorityClass::AuthoritativeAgentSessionState,
3151 ),
3152 };
3153 };
3154 let source_events = if relation.predecessor_session_id == capture.session.id {
3155 &capture.events
3156 } else {
3157 &capture.related_events
3158 };
3159 let replacement_events = if relation.predecessor_session_id == capture.session.id {
3160 &capture.related_events
3161 } else {
3162 &capture.events
3163 };
3164 let validated_replacement = ValidatedReplacementSessionView::try_from_record_with_related(
3165 relation,
3166 source_events,
3167 replacement_events,
3168 )
3169 .ok();
3170 let bound_git = source_events.iter().find(|event| {
3171 event.sequence == relation.source_git_state_sequence && event.kind == EventKind::GitState
3172 });
3173 let candidate_ok = source_events.iter().any(|event| {
3174 event.kind == EventKind::CandidateReady
3175 && event.sequence == relation.candidate_event_sequence
3176 && event.payload["candidate_sha256"].as_str()
3177 == Some(relation.candidate_sha256.as_str())
3178 });
3179 let workspace = bound_git
3180 .and_then(|event| event.payload.get("workspace_state_sha256"))
3181 .and_then(Value::as_str)
3182 .filter(|value| valid_digest(value));
3183 let continuity = if validated_replacement.is_some() && bound_git.is_some() && candidate_ok {
3184 NativeField::agent("candidate and exact relation-bound GitState are continuous")
3185 } else {
3186 NativeField::diagnostic(
3187 "Replacement provenance unavailable: relation-bound candidate/GitState sequence is missing or conflicting",
3188 )
3189 };
3190 let terminal = source_events
3191 .iter()
3192 .rev()
3193 .find(|event| event.kind == EventKind::TerminalState)
3194 .map(|event| display_json(&event.payload));
3195 NativeReplacement {
3196 predecessor_session: NativeField::agent(relation.predecessor_session_id.clone()),
3197 replacement_session: NativeField::agent(relation.replacement_session_id.clone()),
3198 predecessor_state: NativeField::agent(relation.predecessor_state.clone()),
3199 continuity,
3200 candidate_sha256: NativeField::agent(relation.candidate_sha256.clone()),
3201 workspace_state: workspace.map_or_else(
3202 || NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
3203 NativeField::agent,
3204 ),
3205 terminal_outcome: terminal.map_or_else(
3206 || NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
3207 NativeField::agent,
3208 ),
3209 }
3210}
3211
3212fn candidate_context(events: &[Event]) -> Option<String> {
3213 let mut selected: Option<String> = None;
3214 for event in events
3215 .iter()
3216 .filter(|event| event.kind == EventKind::CandidateReady)
3217 {
3218 let value = event
3219 .payload
3220 .get("candidate_sha256")
3221 .and_then(Value::as_str)
3222 .filter(|value| valid_digest(value))?;
3223 if selected
3224 .as_deref()
3225 .is_some_and(|existing| existing != value)
3226 {
3227 return None;
3228 }
3229 selected = Some(value.to_owned());
3230 }
3231 selected
3232}
3233
3234fn source_context(events: &[Event]) -> Option<String> {
3235 let mut selected: Option<String> = None;
3236 for event in events
3237 .iter()
3238 .filter(|event| event.kind == EventKind::GitState)
3239 {
3240 let object = event.payload.as_object()?;
3241 let value =
3242 optional_digest_any(object, &["source_sha256", "authoritative_source_sha256"]).ok()?;
3243 let Some(value) = value else {
3244 continue;
3245 };
3246 if selected
3247 .as_deref()
3248 .is_some_and(|existing| existing != value)
3249 {
3250 return None;
3251 }
3252 selected = Some(value);
3253 }
3254 selected
3255}
3256
3257fn build_surfaces(snapshot: &NativeSnapshotIdentity, facts: &Facts) -> NativeSurfaceSet {
3258 NativeSurfaceSet {
3259 task: task_surface(snapshot, facts),
3260 frozen_scope: scope_surface(snapshot, facts),
3261 job: job_surface(snapshot, facts),
3262 run: run_surface(snapshot, facts),
3263 verification_result: verification_surface(snapshot, facts),
3264 evidence: evidence_surface(snapshot, facts),
3265 report: report_surface(snapshot, facts),
3266 failure_recovery: recovery_surface(snapshot, facts),
3267 replacement_session: replacement_surface(snapshot, facts),
3268 }
3269}
3270
3271fn base_surface(
3272 snapshot: &NativeSnapshotIdentity,
3273 family: NativeSurfaceFamily,
3274 root: Component,
3275) -> Surface {
3276 let mut surface = Surface::new(
3277 format!(
3278 "native-{}-{}-{}",
3279 family.as_str(),
3280 safe_id(&snapshot.source_session_id),
3281 &snapshot.digest[..12]
3282 ),
3283 root,
3284 );
3285 surface
3286 .metadata
3287 .insert("surface_family".to_owned(), family.as_str().to_owned());
3288 surface.metadata.insert(
3289 "snapshot_source_session".to_owned(),
3290 snapshot.source_session_id.clone(),
3291 );
3292 surface.metadata.insert(
3293 "snapshot_state_generation".to_owned(),
3294 snapshot.state_generation.to_string(),
3295 );
3296 surface
3297 .metadata
3298 .insert("snapshot_digest".to_owned(), snapshot.digest.clone());
3299 surface
3300 .metadata
3301 .insert("execution".to_owned(), "read_only_no_actions".to_owned());
3302 surface
3303}
3304
3305fn label(name: &str, field: &NativeField, unknown: bool) -> String {
3306 format!(
3307 "{} [{}]: {}",
3308 name,
3309 field.authority.label(),
3310 field.display(unknown)
3311 )
3312}
3313
3314fn task_surface(snapshot: &NativeSnapshotIdentity, facts: &Facts) -> Surface {
3315 let entries = BTreeMap::from([
3316 ("task".to_owned(), label("Task", &facts.task.task_id, false)),
3317 (
3318 "lifecycle".to_owned(),
3319 label("Lifecycle", &facts.task.lifecycle, false),
3320 ),
3321 (
3322 "frozen".to_owned(),
3323 label("Frozen scope", &facts.task.frozen, false),
3324 ),
3325 (
3326 "scope_digest".to_owned(),
3327 label("Scope digest", &facts.task.scope_digest, false),
3328 ),
3329 (
3330 "scope".to_owned(),
3331 label("Scope", &facts.task.scope_summary, false),
3332 ),
3333 (
3334 "source".to_owned(),
3335 label("Source", &facts.task.source_sha256, false),
3336 ),
3337 (
3338 "candidate".to_owned(),
3339 label("Candidate", &facts.task.candidate_sha256, false),
3340 ),
3341 (
3342 "job".to_owned(),
3343 label("Current Job", &facts.task.current_job_id, false),
3344 ),
3345 ]);
3346 base_surface(
3347 snapshot,
3348 NativeSurfaceFamily::Task,
3349 Component {
3350 id: "task-root".to_owned(),
3351 kind: ComponentKind::Stack {
3352 children: vec![
3353 Component {
3354 id: "task-status".to_owned(),
3355 kind: ComponentKind::Status {
3356 label: "Task lifecycle".to_owned(),
3357 value: facts.task.lifecycle.display(false),
3358 level: StatusLevel::Info,
3359 },
3360 },
3361 Component {
3362 id: "task-facts".to_owned(),
3363 kind: ComponentKind::KeyValue { entries },
3364 },
3365 ],
3366 },
3367 },
3368 )
3369}
3370
3371fn scope_surface(snapshot: &NativeSnapshotIdentity, facts: &Facts) -> Surface {
3372 let mut rows = facts
3373 .frozen_scope
3374 .obligations
3375 .iter()
3376 .map(|item| {
3377 vec![
3378 item.id.display(false),
3379 item.requirement.display(false),
3380 item.requirement.authority.label().to_owned(),
3381 ]
3382 })
3383 .collect::<Vec<_>>();
3384 if rows.is_empty() {
3385 rows.push(vec![
3386 "Unavailable".to_owned(),
3387 "FalseGreen frozen-scope authority unavailable".to_owned(),
3388 facts.frozen_scope.scope_digest.authority.label().to_owned(),
3389 ]);
3390 }
3391 let entries = BTreeMap::from([
3392 (
3393 "scope_digest".to_owned(),
3394 label(
3395 "Immutable scope digest",
3396 &facts.frozen_scope.scope_digest,
3397 false,
3398 ),
3399 ),
3400 (
3401 "distinction".to_owned(),
3402 safe_text(&facts.frozen_scope.distinction),
3403 ),
3404 ]);
3405 base_surface(
3406 snapshot,
3407 NativeSurfaceFamily::FrozenScope,
3408 Component {
3409 id: "scope-root".to_owned(),
3410 kind: ComponentKind::Stack {
3411 children: vec![
3412 Component {
3413 id: "scope-status".to_owned(),
3414 kind: ComponentKind::Status {
3415 label: "Scope".to_owned(),
3416 value: facts.frozen_scope.frozen.display(false),
3417 level: StatusLevel::Info,
3418 },
3419 },
3420 Component {
3421 id: "scope-facts".to_owned(),
3422 kind: ComponentKind::KeyValue { entries },
3423 },
3424 Component {
3425 id: "scope-obligations".to_owned(),
3426 kind: ComponentKind::Table {
3427 columns: vec![
3428 "Obligation".to_owned(),
3429 "Requirement text".to_owned(),
3430 "Authority".to_owned(),
3431 ],
3432 rows,
3433 },
3434 },
3435 ],
3436 },
3437 },
3438 )
3439}
3440
3441fn job_surface(snapshot: &NativeSnapshotIdentity, facts: &Facts) -> Surface {
3442 let entries = BTreeMap::from([
3443 ("job".to_owned(), label("Job", &facts.job.id, false)),
3444 ("task".to_owned(), label("Task", &facts.job.task_id, false)),
3445 (
3446 "source".to_owned(),
3447 label("Source", &facts.job.source_sha256, false),
3448 ),
3449 (
3450 "candidate".to_owned(),
3451 label("Candidate", &facts.job.candidate_sha256, false),
3452 ),
3453 (
3454 "entitlement".to_owned(),
3455 label(
3456 "Entitlement/consumption",
3457 &facts.job.entitlement_consumption,
3458 true,
3459 ),
3460 ),
3461 (
3462 "authority".to_owned(),
3463 label("Authority decision", &facts.job.authority_decision, true),
3464 ),
3465 ]);
3466 let runs = if facts.job.run_ids.is_empty() {
3467 vec![vec![
3468 "Unavailable".to_owned(),
3469 "diagnostic/non-authoritative metadata".to_owned(),
3470 ]]
3471 } else {
3472 facts
3473 .job
3474 .run_ids
3475 .iter()
3476 .map(|item| vec![item.display(false), item.authority.label().to_owned()])
3477 .collect()
3478 };
3479 base_surface(
3480 snapshot,
3481 NativeSurfaceFamily::Job,
3482 Component {
3483 id: "job-root".to_owned(),
3484 kind: ComponentKind::Stack {
3485 children: vec![
3486 status_component(
3487 "job-status",
3488 "Job status",
3489 facts.job.status,
3490 &facts.job.status_raw,
3491 ),
3492 Component {
3493 id: "job-facts".to_owned(),
3494 kind: ComponentKind::KeyValue { entries },
3495 },
3496 Component {
3497 id: "job-runs".to_owned(),
3498 kind: ComponentKind::Table {
3499 columns: vec!["Run".to_owned(), "Authority".to_owned()],
3500 rows: runs,
3501 },
3502 },
3503 ],
3504 },
3505 },
3506 )
3507}
3508
3509fn run_surface(snapshot: &NativeSnapshotIdentity, facts: &Facts) -> Surface {
3510 let entries = BTreeMap::from([
3511 ("run".to_owned(), label("Run", &facts.run.id, false)),
3512 (
3513 "worker".to_owned(),
3514 label("Worker", &facts.run.worker_id, true),
3515 ),
3516 ("job".to_owned(), label("Job", &facts.run.job_id, false)),
3517 (
3518 "assignment_generation".to_owned(),
3519 label(
3520 "Assignment generation",
3521 &facts.run.assignment_generation,
3522 true,
3523 ),
3524 ),
3525 (
3526 "execution_started".to_owned(),
3527 label("Execution started", &facts.run.execution_started_at, true),
3528 ),
3529 (
3530 "package_runtime".to_owned(),
3531 label(
3532 "Package/runtime identity",
3533 &facts.run.package_runtime_identity,
3534 true,
3535 ),
3536 ),
3537 (
3538 "recovery_lease".to_owned(),
3539 label("Recovery/lease", &facts.run.recovery_lease, true),
3540 ),
3541 ]);
3542 base_surface(
3543 snapshot,
3544 NativeSurfaceFamily::Run,
3545 Component {
3546 id: "run-root".to_owned(),
3547 kind: ComponentKind::Stack {
3548 children: vec![
3549 status_component(
3550 "run-status",
3551 "Run status",
3552 facts.run.status,
3553 &facts.run.status_raw,
3554 ),
3555 Component {
3556 id: "run-facts".to_owned(),
3557 kind: ComponentKind::KeyValue { entries },
3558 },
3559 ],
3560 },
3561 },
3562 )
3563}
3564
3565fn verification_surface(snapshot: &NativeSnapshotIdentity, facts: &Facts) -> Surface {
3566 let mut entries = BTreeMap::from([
3567 (
3568 "verdict".to_owned(),
3569 format!(
3570 "VERDICT [{}]: {}",
3571 if facts.verification.source_bound {
3572 AuthorityClass::AuthoritativeFalseGreenState.label()
3573 } else {
3574 AuthorityClass::DiagnosticNonAuthoritativeMetadata.label()
3575 },
3576 facts.verification.verdict.label()
3577 ),
3578 ),
3579 (
3580 "raw".to_owned(),
3581 label(
3582 "Canonical raw status",
3583 &facts.verification.raw_verdict,
3584 true,
3585 ),
3586 ),
3587 (
3588 "source_bound".to_owned(),
3589 format!(
3590 "Source binding [{}]: {}",
3591 AuthorityClass::AuthoritativeFalseGreenState.label(),
3592 facts.verification.source_bound
3593 ),
3594 ),
3595 (
3596 "candidate".to_owned(),
3597 label("Candidate", &facts.verification.candidate_sha256, false),
3598 ),
3599 (
3600 "source".to_owned(),
3601 label(
3602 "Source",
3603 &facts.verification.authoritative_source_sha256,
3604 false,
3605 ),
3606 ),
3607 (
3608 "result_digest".to_owned(),
3609 label("Result digest", &facts.verification.result_digest, false),
3610 ),
3611 (
3612 "report".to_owned(),
3613 label(
3614 "Report reference",
3615 &facts.verification.report_reference,
3616 false,
3617 ),
3618 ),
3619 (
3620 "completion_authority".to_owned(),
3621 label(
3622 "Completion authority",
3623 &facts.verification.completion_authority,
3624 true,
3625 ),
3626 ),
3627 ]);
3628 if let Some(error) = &facts.verification.binding_error {
3629 entries.insert(
3630 "binding_error".to_owned(),
3631 format!(
3632 "Binding error [{}]: {}",
3633 AuthorityClass::DiagnosticNonAuthoritativeMetadata.label(),
3634 safe_text(error)
3635 ),
3636 );
3637 }
3638 let rows = if facts.verification.obligation_outcomes.is_empty() {
3639 vec![vec![
3640 "Unavailable".to_owned(),
3641 "UNKNOWN".to_owned(),
3642 "No validated obligation outcomes supplied".to_owned(),
3643 ]]
3644 } else {
3645 facts
3646 .verification
3647 .obligation_outcomes
3648 .iter()
3649 .map(|item| {
3650 vec![
3651 item.obligation_id.display(false),
3652 item.outcome.label().to_owned(),
3653 item.detail.display(true),
3654 ]
3655 })
3656 .collect()
3657 };
3658 base_surface(
3659 snapshot,
3660 NativeSurfaceFamily::VerificationResult,
3661 Component {
3662 id: "verification-root".to_owned(),
3663 kind: ComponentKind::Stack {
3664 children: vec![
3665 Component {
3666 id: "verification-status".to_owned(),
3667 kind: ComponentKind::Status {
3668 label: "Canonical verification verdict".to_owned(),
3669 value: facts.verification.verdict.label().to_owned(),
3670 level: facts.verification.verdict.status().level(),
3671 },
3672 },
3673 Component {
3674 id: "verification-facts".to_owned(),
3675 kind: ComponentKind::KeyValue { entries },
3676 },
3677 Component {
3678 id: "verification-obligations".to_owned(),
3679 kind: ComponentKind::Table {
3680 columns: vec![
3681 "Obligation".to_owned(),
3682 "Outcome".to_owned(),
3683 "Detail".to_owned(),
3684 ],
3685 rows,
3686 },
3687 },
3688 Component::text("verification-note", &facts.verification.certification_note),
3689 ],
3690 },
3691 },
3692 )
3693}
3694
3695fn evidence_surface(snapshot: &NativeSnapshotIdentity, facts: &Facts) -> Surface {
3696 let mut items = facts
3697 .evidence
3698 .items
3699 .iter()
3700 .map(|item| EvidenceItem {
3701 label: format!(
3702 "{} / {} / {} [{}]",
3703 item.obligation.display(false),
3704 item.source.display(true),
3705 item.evidence_type.display(true),
3706 item.state.label()
3707 ),
3708 value: format!(
3709 "{} — {}",
3710 item.reference.display(true),
3711 item.detail.display(true)
3712 ),
3713 })
3714 .collect::<Vec<_>>();
3715 if facts.evidence.total > MAX_DISPLAY_EVIDENCE {
3716 items.push(EvidenceItem {
3717 label: "Evidence count [diagnostic/non-authoritative metadata]".to_owned(),
3718 value: format!(
3719 "Showing {} of {}; pass={}, fail={}, insufficient={}, unknown={}, observed={}, invalid={}, declared={:?}",
3720 facts.evidence.items.len(),
3721 facts.evidence.total,
3722 facts.evidence.passed,
3723 facts.evidence.failed,
3724 facts.evidence.insufficient,
3725 facts.evidence.unknown,
3726 facts.evidence.observed_rows,
3727 facts.evidence.invalid_rows,
3728 facts.evidence.declared_rows,
3729 ),
3730 });
3731 }
3732 if let Some(reason) = &facts.evidence.unavailable_reason {
3733 items.push(EvidenceItem {
3734 label: "Projection state [diagnostic/non-authoritative metadata]".to_owned(),
3735 value: reason.clone(),
3736 });
3737 }
3738 if items.is_empty() {
3739 items.push(EvidenceItem {
3740 label: "Evidence".to_owned(),
3741 value: "Unavailable".to_owned(),
3742 });
3743 }
3744 base_surface(
3745 snapshot,
3746 NativeSurfaceFamily::Evidence,
3747 Component {
3748 id: "evidence-root".to_owned(),
3749 kind: ComponentKind::Stack {
3750 children: vec![
3751 Component {
3752 id: "evidence-verdict".to_owned(),
3753 kind: ComponentKind::Status {
3754 label: "Evidence authority".to_owned(),
3755 value: facts.verification.verdict.label().to_owned(),
3756 level: facts.verification.verdict.status().level(),
3757 },
3758 },
3759 Component {
3760 id: "evidence-items".to_owned(),
3761 kind: ComponentKind::Evidence {
3762 title: "Evidence items (validated provenance)".to_owned(),
3763 items,
3764 },
3765 },
3766 ],
3767 },
3768 },
3769 )
3770}
3771
3772fn report_surface(snapshot: &NativeSnapshotIdentity, facts: &Facts) -> Surface {
3773 let report_authority = if facts.report.id.value.is_some() {
3774 AuthorityClass::AuthoritativeFalseGreenState
3775 } else {
3776 AuthorityClass::DiagnosticNonAuthoritativeMetadata
3777 };
3778 let entries = BTreeMap::from([
3779 (
3780 "report".to_owned(),
3781 label("Report", &facts.report.id, false),
3782 ),
3783 (
3784 "report_digest".to_owned(),
3785 label("Report digest", &facts.report.report_digest, false),
3786 ),
3787 (
3788 "candidate".to_owned(),
3789 label("Candidate", &facts.report.candidate_sha256, false),
3790 ),
3791 (
3792 "source".to_owned(),
3793 label("Source", &facts.report.source_sha256, false),
3794 ),
3795 (
3796 "verdict".to_owned(),
3797 format!(
3798 "Verdict [{}]: {}",
3799 report_authority.label(),
3800 facts.report.verdict.label()
3801 ),
3802 ),
3803 (
3804 "evidence".to_owned(),
3805 label("Key evidence", &facts.report.key_evidence_summary, true),
3806 ),
3807 (
3808 "created".to_owned(),
3809 label("Created", &facts.report.created_at, true),
3810 ),
3811 (
3812 "provenance".to_owned(),
3813 label("Provenance", &facts.report.provenance, true),
3814 ),
3815 ]);
3816 base_surface(
3817 snapshot,
3818 NativeSurfaceFamily::Report,
3819 Component {
3820 id: "report-root".to_owned(),
3821 kind: ComponentKind::Stack {
3822 children: vec![
3823 Component {
3824 id: "report-status".to_owned(),
3825 kind: ComponentKind::Status {
3826 label: "Report verdict".to_owned(),
3827 value: facts.report.verdict.label().to_owned(),
3828 level: facts.report.verdict.status().level(),
3829 },
3830 },
3831 Component {
3832 id: "report-facts".to_owned(),
3833 kind: ComponentKind::KeyValue { entries },
3834 },
3835 ],
3836 },
3837 },
3838 )
3839}
3840
3841fn recovery_surface(snapshot: &NativeSnapshotIdentity, facts: &Facts) -> Surface {
3842 let value = match facts.recovery.state {
3843 RecoveryState::Recoverable => "RECOVERABLE",
3844 RecoveryState::Blocked => "BLOCKED",
3845 RecoveryState::Terminal => "TERMINAL",
3846 RecoveryState::Unknown => "UNKNOWN",
3847 RecoveryState::NotApplicable => "NOT APPLICABLE",
3848 };
3849 let entries = BTreeMap::from([
3850 (
3851 "reason".to_owned(),
3852 label("Reason", &facts.recovery.reason, true),
3853 ),
3854 (
3855 "claim".to_owned(),
3856 label("Pre-begin claim", &facts.recovery.pre_begin_claim, true),
3857 ),
3858 (
3859 "actions".to_owned(),
3860 "Actions [diagnostic/non-authoritative metadata]: none; read-only G4".to_owned(),
3861 ),
3862 (
3863 "distinction".to_owned(),
3864 safe_text(&facts.recovery.distinction),
3865 ),
3866 ]);
3867 base_surface(
3868 snapshot,
3869 NativeSurfaceFamily::FailureRecovery,
3870 Component {
3871 id: "recovery-root".to_owned(),
3872 kind: ComponentKind::Stack {
3873 children: vec![
3874 Component {
3875 id: "recovery-status".to_owned(),
3876 kind: ComponentKind::Status {
3877 label: "Failure/recovery state".to_owned(),
3878 value: value.to_owned(),
3879 level: if facts.recovery.state == RecoveryState::Terminal {
3880 StatusLevel::Error
3881 } else if matches!(
3882 facts.recovery.state,
3883 RecoveryState::Recoverable | RecoveryState::Blocked
3884 ) {
3885 StatusLevel::Warning
3886 } else {
3887 StatusLevel::Info
3888 },
3889 },
3890 },
3891 Component {
3892 id: "recovery-facts".to_owned(),
3893 kind: ComponentKind::KeyValue { entries },
3894 },
3895 ],
3896 },
3897 },
3898 )
3899}
3900
3901fn replacement_surface(snapshot: &NativeSnapshotIdentity, facts: &Facts) -> Surface {
3902 let entries = BTreeMap::from([
3903 (
3904 "predecessor".to_owned(),
3905 label(
3906 "Predecessor session",
3907 &facts.replacement.predecessor_session,
3908 false,
3909 ),
3910 ),
3911 (
3912 "replacement".to_owned(),
3913 label(
3914 "Replacement session",
3915 &facts.replacement.replacement_session,
3916 false,
3917 ),
3918 ),
3919 (
3920 "predecessor_state".to_owned(),
3921 label(
3922 "Immutable predecessor state",
3923 &facts.replacement.predecessor_state,
3924 false,
3925 ),
3926 ),
3927 (
3928 "continuity".to_owned(),
3929 label("Continuity", &facts.replacement.continuity, true),
3930 ),
3931 (
3932 "candidate".to_owned(),
3933 label("Candidate", &facts.replacement.candidate_sha256, false),
3934 ),
3935 (
3936 "workspace".to_owned(),
3937 label(
3938 "Workspace identity",
3939 &facts.replacement.workspace_state,
3940 true,
3941 ),
3942 ),
3943 (
3944 "terminal".to_owned(),
3945 label(
3946 "Terminal outcome",
3947 &facts.replacement.terminal_outcome,
3948 true,
3949 ),
3950 ),
3951 ]);
3952 base_surface(
3953 snapshot,
3954 NativeSurfaceFamily::ReplacementSession,
3955 Component {
3956 id: "replacement-root".to_owned(),
3957 kind: ComponentKind::Stack {
3958 children: vec![
3959 Component {
3960 id: "replacement-status".to_owned(),
3961 kind: ComponentKind::Status {
3962 label: "Replacement continuity".to_owned(),
3963 value: facts.replacement.continuity.display(true),
3964 level: StatusLevel::Info,
3965 },
3966 },
3967 Component {
3968 id: "replacement-facts".to_owned(),
3969 kind: ComponentKind::KeyValue { entries },
3970 },
3971 Component {
3972 id: "replacement-timeline".to_owned(),
3973 kind: ComponentKind::Timeline {
3974 entries: vec![
3975 TimelineEntry {
3976 label: "Predecessor".to_owned(),
3977 detail: facts.replacement.predecessor_session.display(false),
3978 state: TimelineState::Complete,
3979 },
3980 TimelineEntry {
3981 label: "Replacement".to_owned(),
3982 detail: facts.replacement.replacement_session.display(false),
3983 state: TimelineState::Active,
3984 },
3985 ],
3986 },
3987 },
3988 ],
3989 },
3990 },
3991 )
3992}
3993
3994fn status_component(id: &str, label: &str, status: NativeStatus, raw: &NativeField) -> Component {
3995 Component {
3996 id: id.to_owned(),
3997 kind: ComponentKind::Status {
3998 label: label.to_owned(),
3999 value: if raw.value.is_some() {
4000 format!("{} (raw: {})", status.label(), raw.display(true))
4001 } else {
4002 status.label().to_owned()
4003 },
4004 level: status.level(),
4005 },
4006 }
4007}
4008
4009fn required_string(object: &serde_json::Map<String, Value>, key: &str) -> Result<String, String> {
4010 object
4011 .get(key)
4012 .and_then(Value::as_str)
4013 .filter(|value| !value.is_empty())
4014 .map(str::to_owned)
4015 .ok_or_else(|| format!("missing or malformed {key}"))
4016}
4017fn required_string_any(
4018 object: &serde_json::Map<String, Value>,
4019 keys: &[&str],
4020) -> Result<String, String> {
4021 let mut selected: Option<String> = None;
4022 for key in keys {
4023 let Some(value) = object.get(*key) else {
4024 continue;
4025 };
4026 let value = value
4027 .as_str()
4028 .filter(|value| !value.is_empty())
4029 .ok_or_else(|| format!("missing or malformed {}", keys.join("/")))?;
4030 if selected.as_ref().is_some_and(|existing| existing != value) {
4031 return Err(format!("conflicting {}", keys.join("/")));
4032 }
4033 selected = Some(value.to_owned());
4034 }
4035 selected.ok_or_else(|| format!("missing or malformed {}", keys.join("/")))
4036}
4037fn optional_string(object: &serde_json::Map<String, Value>, key: &str) -> Option<String> {
4038 object
4039 .get(key)
4040 .and_then(Value::as_str)
4041 .filter(|value| !value.is_empty())
4042 .map(str::to_owned)
4043}
4044fn optional_string_any_checked(
4045 object: &serde_json::Map<String, Value>,
4046 keys: &[&str],
4047) -> Result<Option<String>, String> {
4048 let mut selected: Option<String> = None;
4049 for key in keys {
4050 let Some(value) = object.get(*key) else {
4051 continue;
4052 };
4053 let value = value
4054 .as_str()
4055 .filter(|value| !value.is_empty())
4056 .ok_or_else(|| format!("malformed {}", keys.join("/")))?;
4057 if selected.as_ref().is_some_and(|existing| existing != value) {
4058 return Err(format!("conflicting {}", keys.join("/")));
4059 }
4060 selected = Some(value.to_owned());
4061 }
4062 Ok(selected)
4063}
4064
4065fn reject_unknown_keys(
4066 object: &serde_json::Map<String, Value>,
4067 allowed: &[&str],
4068 subject: &str,
4069) -> Result<(), String> {
4070 if let Some(key) = object.keys().find(|key| !allowed.contains(&key.as_str())) {
4071 return Err(format!("{subject} contains unknown field {key}"));
4072 }
4073 Ok(())
4074}
4075
4076fn required_digest_any(
4077 object: &serde_json::Map<String, Value>,
4078 keys: &[&str],
4079) -> Result<String, String> {
4080 let value = required_string_any(object, keys)?;
4081 valid_digest(&value)
4082 .then_some(value)
4083 .ok_or_else(|| format!("malformed digest {}", keys.join("/")))
4084}
4085
4086fn required_report_digest(object: &serde_json::Map<String, Value>) -> Result<String, String> {
4087 required_digest_any(object, &["report_digest", "digest"]).map_err(|error| {
4088 if error.contains("missing or malformed") {
4089 "cryptographic report digest is missing".to_owned()
4090 } else {
4091 error
4092 }
4093 })
4094}
4095
4096fn strict_display_alias(
4097 object: &serde_json::Map<String, Value>,
4098 keys: &[&str],
4099) -> Result<Option<String>, String> {
4100 let mut selected: Option<String> = None;
4101 for key in keys {
4102 let Some(value) = object.get(*key) else {
4103 continue;
4104 };
4105 let rendered = display_json(value);
4106 if selected
4107 .as_ref()
4108 .is_some_and(|existing| existing != &rendered)
4109 {
4110 return Err(format!("conflicting {}", keys.join("/")));
4111 }
4112 selected = Some(rendered);
4113 }
4114 Ok(selected)
4115}
4116fn optional_digest_any(
4117 object: &serde_json::Map<String, Value>,
4118 keys: &[&str],
4119) -> Result<Option<String>, String> {
4120 let mut selected: Option<String> = None;
4121 let mut saw_null = false;
4122 for key in keys {
4123 let Some(value) = object.get(*key) else {
4124 continue;
4125 };
4126 let Value::String(value) = value else {
4127 if value.is_null() {
4128 if selected.is_some() {
4129 return Err(format!("conflicting digest {}", keys.join("/")));
4130 }
4131 saw_null = true;
4132 continue;
4133 }
4134 return Err(format!("malformed digest {}", keys.join("/")));
4135 };
4136 if !valid_digest(value) {
4137 return Err(format!("malformed digest {}", keys.join("/")));
4138 }
4139 if selected.as_ref().is_some_and(|existing| existing != value) {
4140 return Err(format!("conflicting digest {}", keys.join("/")));
4141 }
4142 if saw_null {
4143 return Err(format!("conflicting digest {}", keys.join("/")));
4144 }
4145 selected = Some(value.clone());
4146 }
4147 Ok(selected)
4148}
4149fn optional_falsegreen(value: Option<&str>) -> NativeField {
4150 value.map_or_else(
4151 || NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState),
4152 NativeField::falsegreen,
4153 )
4154}
4155fn optional_agent(value: Option<&str>) -> NativeField {
4156 value.map_or_else(
4157 || NativeField::unavailable(AuthorityClass::AuthoritativeAgentSessionState),
4158 NativeField::agent,
4159 )
4160}
4161fn parse_verdict_exact(raw: &str) -> Option<NativeVerdict> {
4162 match raw {
4163 "accepted" => Some(NativeVerdict::Accepted),
4164 "failed" => Some(NativeVerdict::Failed),
4165 "insufficient_evidence" => Some(NativeVerdict::InsufficientEvidence),
4166 _ => None,
4167 }
4168}
4169fn parse_status_exact(raw: &str) -> Option<NativeStatus> {
4170 match raw {
4171 "QUEUED" => Some(NativeStatus::Queued),
4172 "CLAIMED" => Some(NativeStatus::Claimed),
4173 "RUNNING" => Some(NativeStatus::Running),
4174 "EXECUTING" => Some(NativeStatus::Executing),
4175 "TERMINAL" => Some(NativeStatus::Terminal),
4176 "ACCEPTED" => Some(NativeStatus::Accepted),
4177 "FAILED" => Some(NativeStatus::Failed),
4178 "INSUFFICIENT_EVIDENCE" => Some(NativeStatus::InsufficientEvidence),
4179 _ => None,
4180 }
4181}
4182fn parse_evidence_exact(raw: &str) -> Option<EvidenceState> {
4183 match raw {
4184 "passed" => Some(EvidenceState::Passed),
4185 "failed" => Some(EvidenceState::Failed),
4186 "insufficient" => Some(EvidenceState::Insufficient),
4187 "unknown" => Some(EvidenceState::Unknown),
4188 _ => None,
4189 }
4190}
4191fn status_wire(status: NativeStatus) -> &'static str {
4192 match status {
4193 NativeStatus::Accepted => "ACCEPTED",
4194 NativeStatus::Failed => "FAILED",
4195 NativeStatus::InsufficientEvidence => "INSUFFICIENT_EVIDENCE",
4196 NativeStatus::Queued => "QUEUED",
4197 NativeStatus::Claimed => "CLAIMED",
4198 NativeStatus::Running => "RUNNING",
4199 NativeStatus::Executing => "EXECUTING",
4200 NativeStatus::Terminal => "TERMINAL",
4201 NativeStatus::Recoverable => "RECOVERABLE",
4202 NativeStatus::Unknown => "UNKNOWN",
4203 }
4204}
4205fn status_wire_verdict(verdict: NativeVerdict) -> &'static str {
4206 match verdict {
4207 NativeVerdict::Accepted => "accepted",
4208 NativeVerdict::Failed => "failed",
4209 NativeVerdict::InsufficientEvidence => "insufficient_evidence",
4210 NativeVerdict::Incomplete => "incomplete",
4211 NativeVerdict::Invalid => "invalid",
4212 NativeVerdict::Unknown => "unknown",
4213 }
4214}
4215fn parse_obligation_outcomes(
4216 payload: &Value,
4217 scope: Option<&AuthoritativeFrozenScopeView>,
4218) -> Vec<NativeObligationOutcome> {
4219 let Some(values) = payload
4220 .get("obligation_outcomes")
4221 .or_else(|| payload.get("obligations"))
4222 .and_then(Value::as_array)
4223 else {
4224 return Vec::new();
4225 };
4226 let Some(scope) = scope else {
4227 return Vec::new();
4228 };
4229 values
4230 .iter()
4231 .filter_map(|value| {
4232 let object = value.as_object()?;
4233 let id = required_string_any(object, &["obligation_id", "id"]).ok()?;
4234 if !scope
4235 .obligations
4236 .iter()
4237 .any(|(expected, _)| expected == &id)
4238 {
4239 return None;
4240 }
4241 let outcome = parse_evidence_exact(
4242 required_string_any(object, &["status", "outcome"])
4243 .ok()?
4244 .as_str(),
4245 )?;
4246 Some(NativeObligationOutcome {
4247 obligation_id: NativeField::falsegreen(id),
4248 outcome,
4249 detail: object
4250 .get("detail")
4251 .map(display_json)
4252 .map(NativeField::falsegreen)
4253 .unwrap_or_else(|| {
4254 NativeField::unavailable(AuthorityClass::AuthoritativeFalseGreenState)
4255 }),
4256 evidence_count: object
4257 .get("evidence_count")
4258 .and_then(Value::as_u64)
4259 .unwrap_or_default() as usize,
4260 })
4261 })
4262 .collect()
4263}
4264fn scope_digest_for_obligations(obligations: &[(String, String)]) -> String {
4265 let value = serde_json::json!({"obligations": obligations.iter().map(|(id, requirement)| serde_json::json!({"id": id, "requirement": requirement})).collect::<Vec<_>>()});
4266 sha256_json(&value)
4267}
4268pub fn frozen_scope_digest(obligations: &[(String, String)]) -> String {
4269 scope_digest_for_obligations(obligations)
4270}
4271fn display_json(value: &Value) -> String {
4272 safe_text(&value.to_string())
4273}
4274fn safe_text(value: &str) -> String {
4275 let value = sanitize_text(value);
4276 let mut output = String::new();
4277 for character in value.chars() {
4278 let character = if matches!(character, '\n' | '\r' | '\t') {
4279 ' '
4280 } else {
4281 character
4282 };
4283 if output.len() + character.len_utf8() > MAX_EVIDENCE_VALUE_BYTES {
4284 output.push('…');
4285 break;
4286 }
4287 output.push(character);
4288 }
4289 output
4290}
4291fn sha256_json(value: &Value) -> String {
4292 sha256_bytes(serde_json::to_string(value).unwrap_or_default().as_bytes())
4293}
4294fn sha256_bytes(value: &[u8]) -> String {
4295 format!("{:x}", Sha256::digest(value))
4296}
4297fn valid_digest(value: &str) -> bool {
4298 value.len() == 64
4299 && value
4300 .bytes()
4301 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
4302}
4303fn valid_identity(value: &str) -> bool {
4304 !value.is_empty()
4305 && value.len() <= 256
4306 && value.chars().all(|character| {
4307 !character.is_control()
4308 && !matches!(
4309 character,
4310 '\u{061c}'
4311 | '\u{200b}'
4312 | '\u{200c}'
4313 | '\u{200d}'
4314 | '\u{200e}'
4315 | '\u{200f}'
4316 | '\u{202a}'..='\u{202e}'
4317 | '\u{2060}'
4318 | '\u{2066}'..='\u{2069}'
4319 | '\u{feff}'
4320 )
4321 })
4322}
4323fn safe_id(value: &str) -> String {
4324 value
4325 .chars()
4326 .filter(|character| {
4327 character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '.' | ':')
4328 })
4329 .take(64)
4330 .collect()
4331}
4332fn session_state_label(state: SessionState) -> &'static str {
4333 match state {
4334 SessionState::Initializing => "INITIALIZING",
4335 SessionState::Working => "WORKING",
4336 SessionState::WaitingForTool => "WAITING_FOR_TOOL",
4337 SessionState::CandidateReady => "CANDIDATE_READY",
4338 SessionState::Verifying => "VERIFYING",
4339 SessionState::Repairing => "REPAIRING",
4340 SessionState::Paused => "PAUSED",
4341 SessionState::AcceptedAwaitingAuthority => "ACCEPTED_AWAITING_AUTHORITY",
4342 SessionState::Completed => "COMPLETED",
4343 SessionState::Failed => "FAILED",
4344 SessionState::BudgetExhausted => "BUDGET_EXHAUSTED",
4345 }
4346}
4347
4348fn is_known_session_lifecycle(value: &str) -> bool {
4349 matches!(
4350 value,
4351 "INITIALIZING"
4352 | "WORKING"
4353 | "WAITING_FOR_TOOL"
4354 | "CANDIDATE_READY"
4355 | "VERIFYING"
4356 | "REPAIRING"
4357 | "PAUSED"
4358 | "ACCEPTED_AWAITING_AUTHORITY"
4359 | "COMPLETED"
4360 | "FAILED"
4361 | "BUDGET_EXHAUSTED"
4362 )
4363}