1pub mod agent_proof;
32pub mod evidence_ledger;
33pub mod oracle;
34pub mod replay;
35
36pub use agent_proof::{
38 AgentProofError, AgentTaskProofBundle, AgentTaskProofBundleBuilder, BlockerRecord,
39 CommandRecord, CommitRecord, FileReservationRecord, RchRecord, ReplayInstructions,
40 ReplaySafetyLevel, ValidationFrontierRecord,
41};
42pub use evidence_ledger::{AtpEvidenceEntry, AtpEvidenceLedger, EvidenceSummary};
43pub use oracle::{AtpOracleChecks, AtpOracleResult, AtpTransferOracle, AtpTransferState};
44pub use replay::{
45 AtpReplayCoordinator, AtpReplayResult, ReplayError, TraceMinimizer, TraceMinimizerConfig,
46};
47
48use crate::lab::oracle::OracleStats;
49use crate::lab::oracle::evidence::{
50 BayesFactor, EvidenceEntry, EvidenceLine, EvidenceStrength, LogLikelihoodContributions,
51};
52use crate::trace::{TraceBuffer, TraceData, TraceEvent, TraceEventKind};
53use serde::{Deserialize, Serialize};
54use sha2::{Digest, Sha256};
55use std::collections::BTreeMap;
56use std::path::{Path, PathBuf};
57use thiserror::Error;
58
59pub const ATP_CRASHPACK_SCHEMA_VERSION: u32 = 1;
61
62#[derive(Debug, Clone)]
64pub struct TransferOracle {
65 oracle_name: String,
66 manifest_checks: bool,
67 journal_checks: bool,
68 quiescence_checks: bool,
69 obligation_checks: bool,
70 path_consistency_checks: bool,
71}
72
73impl TransferOracle {
74 pub fn new(name: impl Into<String>) -> Self {
76 Self {
77 oracle_name: name.into(),
78 manifest_checks: true,
79 journal_checks: true,
80 quiescence_checks: true,
81 obligation_checks: true,
82 path_consistency_checks: true,
83 }
84 }
85
86 pub fn with_manifest_checks(mut self, enabled: bool) -> Self {
88 self.manifest_checks = enabled;
89 self
90 }
91
92 pub fn with_journal_checks(mut self, enabled: bool) -> Self {
94 self.journal_checks = enabled;
95 self
96 }
97
98 pub fn validate_transfer(&self, state: &TransferState) -> TransferOracleResult {
100 let mut violations = Vec::new();
101 let mut stats = OracleStats {
102 entities_tracked: 0,
103 events_recorded: 0,
104 };
105
106 if self.manifest_checks {
107 if let Some(violation) = self.check_manifest_integrity(state) {
108 violations.push(violation);
109 stats.entities_tracked += 1;
110 }
111 stats.events_recorded += 1;
112 }
113
114 if self.journal_checks {
115 if let Some(violation) = self.check_journal_consistency(state) {
116 violations.push(violation);
117 stats.entities_tracked += 1;
118 }
119 stats.events_recorded += 1;
120 }
121
122 if self.quiescence_checks {
123 if let Some(violation) = self.check_quiescence(state) {
124 violations.push(violation);
125 stats.entities_tracked += 1;
126 }
127 stats.events_recorded += 1;
128 }
129
130 if self.obligation_checks {
131 if let Some(violation) = self.check_obligation_leaks(state) {
132 violations.push(violation);
133 stats.entities_tracked += 1;
134 }
135 stats.events_recorded += 1;
136 }
137
138 if self.path_consistency_checks {
139 if let Some(violation) = self.check_path_consistency(state) {
140 violations.push(violation);
141 stats.entities_tracked += 1;
142 }
143 stats.events_recorded += 1;
144 }
145
146 let passed = stats.entities_tracked == 0;
147 TransferOracleResult {
148 oracle_name: self.oracle_name.clone(),
149 violations,
150 stats,
151 passed,
152 }
153 }
154
155 fn check_manifest_integrity(&self, state: &TransferState) -> Option<TransferViolation> {
156 if state.manifest_hash != state.expected_manifest_hash {
158 return Some(TransferViolation {
159 violation_type: "manifest_integrity".to_string(),
160 description: format!(
161 "Manifest hash mismatch: expected {}, got {}",
162 state.expected_manifest_hash, state.manifest_hash
163 ),
164 severity: ViolationSeverity::High,
165 evidence: BTreeMap::from([
166 (
167 "expected_hash".to_string(),
168 state.expected_manifest_hash.clone(),
169 ),
170 ("actual_hash".to_string(), state.manifest_hash.clone()),
171 ]),
172 });
173 }
174 None
175 }
176
177 fn check_journal_consistency(&self, state: &TransferState) -> Option<TransferViolation> {
178 if state.journal_gaps > 0 {
180 return Some(TransferViolation {
181 violation_type: "journal_consistency".to_string(),
182 description: format!(
183 "Journal has {} gaps or ordering violations",
184 state.journal_gaps
185 ),
186 severity: ViolationSeverity::High,
187 evidence: BTreeMap::from([(
188 "gap_count".to_string(),
189 state.journal_gaps.to_string(),
190 )]),
191 });
192 }
193 None
194 }
195
196 fn check_quiescence(&self, state: &TransferState) -> Option<TransferViolation> {
197 if state.pending_operations > 0 {
199 return Some(TransferViolation {
200 violation_type: "quiescence".to_string(),
201 description: format!(
202 "Transfer attempted with {} pending operations",
203 state.pending_operations
204 ),
205 severity: ViolationSeverity::Medium,
206 evidence: BTreeMap::from([(
207 "pending_count".to_string(),
208 state.pending_operations.to_string(),
209 )]),
210 });
211 }
212 None
213 }
214
215 fn check_obligation_leaks(&self, state: &TransferState) -> Option<TransferViolation> {
216 if state.leaked_obligations > 0 {
218 return Some(TransferViolation {
219 violation_type: "obligation_leak".to_string(),
220 description: format!("Found {} leaked obligations", state.leaked_obligations),
221 severity: ViolationSeverity::High,
222 evidence: BTreeMap::from([(
223 "leak_count".to_string(),
224 state.leaked_obligations.to_string(),
225 )]),
226 });
227 }
228 None
229 }
230
231 fn check_path_consistency(&self, state: &TransferState) -> Option<TransferViolation> {
232 if !state.path_outcomes_consistent {
234 return Some(TransferViolation {
235 violation_type: "path_consistency".to_string(),
236 description: "Path outcomes are inconsistent across replicas".to_string(),
237 severity: ViolationSeverity::High,
238 evidence: BTreeMap::new(),
239 });
240 }
241 None
242 }
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct TransferState {
248 pub manifest_hash: String,
249 pub expected_manifest_hash: String,
250 pub journal_gaps: u32,
251 pub pending_operations: u32,
252 pub leaked_obligations: u32,
253 pub path_outcomes_consistent: bool,
254}
255
256impl TransferState {
257 pub fn new() -> Self {
258 Self {
259 manifest_hash: String::new(),
260 expected_manifest_hash: String::new(),
261 journal_gaps: 0,
262 pending_operations: 0,
263 leaked_obligations: 0,
264 path_outcomes_consistent: true,
265 }
266 }
267}
268
269impl Default for TransferState {
270 fn default() -> Self {
271 Self::new()
272 }
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct TransferOracleResult {
278 pub oracle_name: String,
279 pub violations: Vec<TransferViolation>,
280 pub stats: OracleStats,
281 pub passed: bool,
282}
283
284impl TransferOracleResult {
285 pub fn has_violations(&self) -> bool {
286 !self.violations.is_empty()
287 }
288
289 pub fn high_severity_violations(&self) -> Vec<&TransferViolation> {
290 self.violations
291 .iter()
292 .filter(|v| matches!(v.severity, ViolationSeverity::High))
293 .collect()
294 }
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct TransferViolation {
300 pub violation_type: String,
301 pub description: String,
302 pub severity: ViolationSeverity,
303 pub evidence: BTreeMap<String, String>,
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
308pub enum ViolationSeverity {
309 Low,
310 Medium,
311 High,
312 Critical,
313}
314
315#[derive(Debug, Default)]
317pub struct CrashpackBuilder {
318 oracle_results: Vec<TransferOracleResult>,
319 trace_buffer: Option<TraceBuffer>,
320 seeds: BTreeMap<String, u64>,
321 artifact_paths: Vec<String>,
322 metadata: BTreeMap<String, String>,
323}
324
325impl CrashpackBuilder {
326 pub fn new() -> Self {
327 Self::default()
328 }
329
330 pub fn with_oracle_result(mut self, result: TransferOracleResult) -> Self {
331 self.oracle_results.push(result);
332 self
333 }
334
335 pub fn with_trace(mut self, trace: TraceBuffer) -> Self {
336 self.trace_buffer = Some(trace);
337 self
338 }
339
340 pub fn with_seed(mut self, name: impl Into<String>, seed: u64) -> Self {
341 self.seeds.insert(name.into(), seed);
342 self
343 }
344
345 pub fn with_artifact_path(mut self, path: impl Into<String>) -> Self {
346 let path = path.into();
347 if !self.artifact_paths.contains(&path) {
348 self.artifact_paths.push(path); }
350 self
351 }
352
353 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
354 self.metadata.insert(key.into(), value.into());
355 self
356 }
357
358 pub fn build(self) -> Result<AtpCrashpack, CrashpackError> {
359 Ok(AtpCrashpack {
360 schema_version: ATP_CRASHPACK_SCHEMA_VERSION,
361 oracle_results: self.oracle_results,
362 trace_events: self
363 .trace_buffer
364 .as_ref()
365 .map(|buf| buf.iter().cloned().collect())
366 .unwrap_or_default(),
367 seeds: self.seeds,
368 artifact_paths: self.artifact_paths,
369 metadata: self.metadata,
370 })
371 }
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct AtpCrashpack {
377 pub schema_version: u32,
378 pub oracle_results: Vec<TransferOracleResult>,
379 pub trace_events: Vec<TraceEvent>,
380 pub seeds: BTreeMap<String, u64>,
381 pub artifact_paths: Vec<String>,
382 pub metadata: BTreeMap<String, String>,
383}
384
385impl AtpCrashpack {
386 pub fn emit_atp_trace(&self, output_dir: impl AsRef<Path>) -> Result<(), CrashpackError> {
388 let output_dir = output_dir.as_ref();
389 std::fs::create_dir_all(output_dir)?;
390
391 let trace_path = output_dir.join("transfer.atp-trace");
393 let trace_data = serde_json::to_string_pretty(&self.trace_events)?;
394 std::fs::write(&trace_path, trace_data)?;
395
396 let journal_data = self.generate_journal_digest()?;
398 let journal_digest = journal_digest_ref(&journal_data);
399 let journal_path = output_dir.join("journal");
400 std::fs::write(&journal_path, &journal_data)?;
401
402 let journal_digest_path = output_dir.join("journal.digest");
403 std::fs::write(
404 &journal_digest_path,
405 format!("digest: {journal_digest}\nbytes: {}\n", journal_data.len()),
406 )?;
407
408 let evidence_ledger_path = output_dir.join("evidence-ledger.json");
411 let evidence_ledger = self.generate_evidence_ledger();
412 std::fs::write(&evidence_ledger_path, evidence_ledger.export_json()?)?;
413
414 let manifest_path = output_dir.join("manifest");
416 let manifest_data = self.generate_manifest(&journal_digest)?;
417 std::fs::write(&manifest_path, manifest_data)?;
418
419 self.emit_specialized_logs(output_dir)?;
421
422 let replay_cmd = self.generate_replay_command()?;
424 let replay_path = output_dir.join("replay_command.sh");
425 std::fs::write(&replay_path, replay_cmd)?;
426
427 Ok(())
428 }
429
430 fn generate_manifest(&self, journal_digest: &str) -> Result<String, CrashpackError> {
431 let mut manifest = format!(
432 "# ATP Crashpack Manifest\nschema_version: {}\nviolations: {}\njournal_digest: {journal_digest}\njournal_digest_artifact: journal.digest\nevidence_ledger: evidence-ledger.json\n",
433 self.schema_version,
434 self.oracle_results
435 .iter()
436 .map(|r| r.violations.len())
437 .sum::<usize>()
438 );
439
440 for (key, value) in &self.metadata {
441 manifest.push_str(&format!("metadata.{key}: {value}\n"));
442 }
443
444 if !self.seeds.is_empty() {
445 manifest.push_str("seeds:\n");
446 for (name, seed) in &self.seeds {
447 manifest.push_str(&format!(" {name}: {seed}\n"));
448 }
449 }
450
451 if !self.artifact_paths.is_empty() {
452 manifest.push_str("artifact_paths:\n");
453 for path in &self.artifact_paths {
454 manifest.push_str(&format!(" - {path}\n"));
455 }
456 }
457
458 Ok(manifest)
459 }
460
461 fn generate_journal_digest(&self) -> Result<String, CrashpackError> {
462 let mut journal = String::from("# ATP Journal Digest\n");
463
464 for result in &self.oracle_results {
465 journal.push_str(&format!("oracle: {}\n", result.oracle_name));
466 journal.push_str(&format!(
467 " events_recorded: {}\n",
468 result.stats.events_recorded
469 ));
470 journal.push_str(&format!(
471 " entities_tracked: {}\n",
472 result.stats.entities_tracked
473 ));
474 journal.push_str(&format!(" passed: {}\n", result.passed));
475 if !result.violations.is_empty() {
476 journal.push_str(" violations:\n");
477 for violation in &result.violations {
478 journal.push_str(&format!(" - type: {}\n", violation.violation_type));
479 journal.push_str(&format!(" severity: {:?}\n", violation.severity));
480 journal.push_str(&format!(" description: {}\n", violation.description));
481 if !violation.evidence.is_empty() {
482 journal.push_str(" evidence:\n");
483 for (key, value) in &violation.evidence {
484 journal.push_str(&format!(" {key}: {value}\n"));
485 }
486 }
487 }
488 }
489 }
490
491 Ok(journal)
492 }
493
494 fn generate_evidence_ledger(&self) -> AtpEvidenceLedger {
495 let mut ledger = AtpEvidenceLedger::new();
496
497 for (name, seed) in &self.seeds {
498 ledger.record_seed(name.clone(), *seed);
499 }
500
501 for (key, value) in &self.metadata {
502 ledger.add_metadata(key.clone(), value.clone());
503 }
504
505 for artifact in [
506 "transfer.atp-trace",
507 "manifest",
508 "journal",
509 "journal.digest",
510 "evidence-ledger.json",
511 "pathlog",
512 "quiclog",
513 "repairlog",
514 "replay_command.sh",
515 ] {
516 ledger.record_artifact_path(PathBuf::from(artifact));
517 }
518
519 for artifact in &self.artifact_paths {
520 ledger.record_artifact_path(PathBuf::from(artifact));
521 }
522
523 for result in &self.oracle_results {
524 ledger.record_oracle_result(
525 result.oracle_name.clone(),
526 evidence_for_oracle_result(result),
527 Some(PathBuf::from("transfer.atp-trace")),
528 );
529 }
530
531 ledger
532 }
533
534 fn emit_specialized_logs(&self, output_dir: &Path) -> Result<(), CrashpackError> {
535 self.write_specialized_log(output_dir, "pathlog", is_atp_path_event)?;
536 self.write_specialized_log(output_dir, "quiclog", is_atp_quic_event)?;
537 self.write_specialized_log(output_dir, "repairlog", is_atp_repair_event)?;
538
539 Ok(())
540 }
541
542 fn write_specialized_log(
543 &self,
544 output_dir: &Path,
545 file_name: &str,
546 include: impl Fn(&TraceEvent) -> bool,
547 ) -> Result<(), CrashpackError> {
548 let log = atp_specialized_log(&self.trace_events, include);
549 std::fs::write(output_dir.join(file_name), log)?;
550 Ok(())
551 }
552}
553
554pub(crate) fn atp_specialized_log(
555 trace_events: &[TraceEvent],
556 include: impl Fn(&TraceEvent) -> bool,
557) -> String {
558 trace_events
559 .iter()
560 .filter(|event| include(event))
561 .map(ToString::to_string)
562 .collect::<Vec<_>>()
563 .join("\n")
564}
565
566pub(crate) fn is_atp_path_event(event: &TraceEvent) -> bool {
567 matches!(
568 event.kind,
569 TraceEventKind::Spawn
570 | TraceEventKind::Schedule
571 | TraceEventKind::Yield
572 | TraceEventKind::Wake
573 | TraceEventKind::Poll
574 | TraceEventKind::Complete
575 | TraceEventKind::RegionCreated
576 | TraceEventKind::RegionCloseBegin
577 | TraceEventKind::RegionCloseComplete
578 | TraceEventKind::RegionCancelled
579 | TraceEventKind::Checkpoint
580 ) || message_contains_any(event, &["path", "route", "racing"])
581}
582
583pub(crate) fn is_atp_quic_event(event: &TraceEvent) -> bool {
584 message_contains_any(event, &["quic", "udp", "packet", "connection id"])
585}
586
587pub(crate) fn is_atp_repair_event(event: &TraceEvent) -> bool {
588 message_contains_any(event, &["repair", "raptorq", "fec", "symbol"])
589}
590
591fn message_contains_any(event: &TraceEvent, needles: &[&str]) -> bool {
592 let TraceData::Message(message) = &event.data else {
593 return false;
594 };
595 let message = message.to_ascii_lowercase();
596 needles.iter().any(|needle| message.contains(needle))
597}
598
599impl AtpCrashpack {
600 fn generate_replay_command(&self) -> Result<String, CrashpackError> {
601 let mut cmd = String::from("#!/bin/bash\n");
602 cmd.push_str("# ATP Replay Command\n");
603 cmd.push_str("# Generated by ATP crashpack\n\n");
604
605 for (name, seed) in &self.seeds {
607 cmd.push_str(&format!(
608 "export ATP_SEED_{}={}\n",
609 seed_env_suffix(name),
610 seed
611 ));
612 }
613
614 cmd.push_str("\n# Replay command\n");
615 cmd.push_str(
616 "asupersync atp replay --trace-file transfer.atp-trace --manifest manifest \
617 --journal-digest journal.digest --evidence-ledger evidence-ledger.json \
618 --pathlog pathlog --quiclog quiclog --repairlog repairlog --validate-oracles",
619 );
620
621 for result in &self.oracle_results {
623 cmd.push_str(&format!(" --oracle {}", shell_arg(&result.oracle_name)));
624 }
625
626 cmd.push('\n');
627 Ok(cmd)
628 }
629}
630
631fn seed_env_suffix(name: &str) -> String {
632 let mut suffix = String::with_capacity(name.len());
633 for ch in name.chars() {
634 if ch.is_ascii_alphanumeric() {
635 suffix.push(ch.to_ascii_uppercase());
636 } else {
637 suffix.push('_');
638 }
639 }
640
641 let suffix = suffix.trim_matches('_');
642 if suffix.is_empty() {
643 "SEED".to_string()
644 } else {
645 suffix.to_string()
646 }
647}
648
649fn shell_arg(raw: &str) -> String {
650 if !raw.is_empty() && raw.bytes().all(shell_safe_byte) {
651 return raw.to_string();
652 }
653
654 let mut quoted = String::with_capacity(raw.len() + 2);
655 quoted.push('\'');
656 for ch in raw.chars() {
657 if ch == '\'' {
658 quoted.push_str("'\"'\"'");
659 } else {
660 quoted.push(ch);
661 }
662 }
663 quoted.push('\'');
664 quoted
665}
666
667fn shell_safe_byte(byte: u8) -> bool {
668 byte.is_ascii_alphanumeric()
669 || matches!(
670 byte,
671 b'_' | b'-' | b'.' | b'/' | b':' | b'@' | b'%' | b'+' | b'=' | b','
672 )
673}
674
675fn journal_digest_ref(journal_data: &str) -> String {
676 let digest = Sha256::digest(journal_data.as_bytes());
677 format!("sha256:{}", hex::encode(digest))
678}
679
680fn evidence_for_oracle_result(result: &TransferOracleResult) -> EvidenceEntry {
681 let log10_bf = if result.passed {
682 -1.0
683 } else {
684 result
685 .violations
686 .iter()
687 .map(|violation| severity_log10_bf(&violation.severity))
688 .reduce(f64::max)
689 .unwrap_or(0.5)
690 };
691
692 let strength = EvidenceStrength::from_log10_bf(log10_bf);
693 let max_severity = result
694 .violations
695 .iter()
696 .max_by_key(|violation| severity_rank(&violation.severity))
697 .map_or("none", |violation| severity_label(&violation.severity));
698
699 EvidenceEntry {
700 invariant: result.oracle_name.clone(),
701 passed: result.passed,
702 bayes_factor: BayesFactor {
703 log10_bf,
704 hypothesis: format!("{} violation", result.oracle_name),
705 strength,
706 },
707 log_likelihoods: LogLikelihoodContributions {
708 structural: log10_bf / 2.0,
709 detection: log10_bf / 2.0,
710 total: log10_bf,
711 },
712 evidence_lines: vec![EvidenceLine {
713 equation: "BF = P(oracle evidence | violation) / P(oracle evidence | clean)"
714 .to_string(),
715 substitution: format!(
716 "passed={}, violations={}, events_recorded={}, entities_tracked={}, max_severity={max_severity}",
717 result.passed,
718 result.violations.len(),
719 result.stats.events_recorded,
720 result.stats.entities_tracked
721 ),
722 intuition: if result.passed {
723 format!(
724 "{} produced deterministic clean evidence",
725 result.oracle_name
726 )
727 } else {
728 format!(
729 "{} reported {} deterministic violation(s)",
730 result.oracle_name,
731 result.violations.len()
732 )
733 },
734 }],
735 }
736}
737
738fn severity_log10_bf(severity: &ViolationSeverity) -> f64 {
739 match severity {
740 ViolationSeverity::Low => 0.6,
741 ViolationSeverity::Medium => 1.0,
742 ViolationSeverity::High => 1.6,
743 ViolationSeverity::Critical => 2.4,
744 }
745}
746
747fn severity_label(severity: &ViolationSeverity) -> &'static str {
748 match severity {
749 ViolationSeverity::Low => "low",
750 ViolationSeverity::Medium => "medium",
751 ViolationSeverity::High => "high",
752 ViolationSeverity::Critical => "critical",
753 }
754}
755
756fn severity_rank(severity: &ViolationSeverity) -> u8 {
757 match severity {
758 ViolationSeverity::Low => 0,
759 ViolationSeverity::Medium => 1,
760 ViolationSeverity::High => 2,
761 ViolationSeverity::Critical => 3,
762 }
763}
764
765#[derive(Debug, Error)]
767pub enum CrashpackError {
768 #[error("IO error: {0}")]
769 Io(#[from] std::io::Error),
770 #[error("Serialization error: {0}")]
771 Serialization(#[from] serde_json::Error),
772 #[error("Invalid crashpack format: {0}")]
773 InvalidFormat(String),
774}