1use std::path::Path;
29use std::sync::Mutex;
30
31use chrono::{DateTime, Utc};
32use ed25519_dalek::{SigningKey, VerifyingKey};
33use weftos_rvf_crypto::hash::shake256_256;
34use weftos_rvf_crypto::{
35 create_witness_chain, decode_signature_footer, encode_signature_footer,
36 lineage_record_to_bytes, lineage_witness_entry, sign_segment, verify_segment,
37 sign_segment_ml_dsa, verify_segment_ml_dsa,
38 MlDsa65Key, MlDsa65VerifyKey,
39 verify_witness_chain, WitnessEntry,
40};
41use rvf_types::SEGMENT_HEADER_SIZE;
42use weftos_rvf_wire::writer::{calculate_padded_size, write_segment};
43use weftos_rvf_wire::{read_segment, validate_segment};
44
45const EXOCHAIN_MAGIC: u32 = 0x4558_4F43; #[derive(Debug, Clone)]
66struct ExoChainHeader {
67 magic: u32,
68 version: u8,
69 subtype: u8,
70 flags: u16,
71 chain_id: u32,
72 _reserved: u32,
73 sequence: u64,
74 timestamp_secs: u64,
75 prev_hash: [u8; 32],
76}
77
78const EXOCHAIN_HEADER_SIZE: usize = 64;
79
80impl ExoChainHeader {
81 fn to_bytes(&self) -> [u8; EXOCHAIN_HEADER_SIZE] {
82 let mut buf = [0u8; EXOCHAIN_HEADER_SIZE];
83 buf[0..4].copy_from_slice(&self.magic.to_le_bytes());
84 buf[4] = self.version;
85 buf[5] = self.subtype;
86 buf[6..8].copy_from_slice(&self.flags.to_le_bytes());
87 buf[8..12].copy_from_slice(&self.chain_id.to_le_bytes());
88 buf[12..16].copy_from_slice(&self._reserved.to_le_bytes());
89 buf[16..24].copy_from_slice(&self.sequence.to_le_bytes());
90 buf[24..32].copy_from_slice(&self.timestamp_secs.to_le_bytes());
91 buf[32..64].copy_from_slice(&self.prev_hash);
92 buf
93 }
94
95 fn from_bytes(data: &[u8]) -> Option<Self> {
96 if data.len() < EXOCHAIN_HEADER_SIZE {
97 return None;
98 }
99 let magic = u32::from_le_bytes(data[0..4].try_into().ok()?);
100 if magic != EXOCHAIN_MAGIC {
101 return None;
102 }
103 Some(Self {
104 magic,
105 version: data[4],
106 subtype: data[5],
107 flags: u16::from_le_bytes(data[6..8].try_into().ok()?),
108 chain_id: u32::from_le_bytes(data[8..12].try_into().ok()?),
109 _reserved: u32::from_le_bytes(data[12..16].try_into().ok()?),
110 sequence: u64::from_le_bytes(data[16..24].try_into().ok()?),
111 timestamp_secs: u64::from_le_bytes(data[24..32].try_into().ok()?),
112 prev_hash: data[32..64].try_into().ok()?,
113 })
114 }
115}
116
117fn write_exochain_event(header: &ExoChainHeader, cbor: &[u8], segment_id: u64) -> Vec<u8> {
119 let exo_bytes = header.to_bytes();
120 let mut payload = Vec::with_capacity(exo_bytes.len() + cbor.len());
121 payload.extend_from_slice(&exo_bytes);
122 payload.extend_from_slice(cbor);
123 write_segment(
124 0x10, &payload,
126 rvf_types::SegmentFlags::empty(),
127 segment_id,
128 )
129}
130
131fn decode_exochain_payload(payload: &[u8]) -> Option<(ExoChainHeader, &[u8])> {
133 let header = ExoChainHeader::from_bytes(payload)?;
134 Some((header, &payload[EXOCHAIN_HEADER_SIZE..]))
135}
136use serde::{Deserialize, Serialize};
137use tracing::{debug, info, warn};
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct ChainEvent {
142 pub sequence: u64,
144 pub chain_id: u32,
146 pub timestamp: DateTime<Utc>,
148 pub prev_hash: [u8; 32],
150 pub hash: [u8; 32],
152 #[serde(default)]
155 pub payload_hash: [u8; 32],
156 pub source: String,
158 pub kind: String,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub payload: Option<serde_json::Value>,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct ChainCheckpoint {
168 pub chain_id: u32,
170 pub sequence: u64,
172 pub last_hash: [u8; 32],
174 pub timestamp: DateTime<Utc>,
176 pub events_since_last: u64,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct ChainVerifyResult {
183 pub valid: bool,
185 pub event_count: usize,
187 pub errors: Vec<String>,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub signature_verified: Option<bool>,
193}
194
195pub(crate) fn compute_payload_hash(payload: &Option<serde_json::Value>) -> [u8; 32] {
200 match payload {
201 Some(val) => {
202 let bytes = serde_json::to_vec(val).unwrap_or_default();
203 shake256_256(&bytes)
204 }
205 None => [0u8; 32],
206 }
207}
208
209pub(crate) fn compute_event_hash(
225 sequence: u64,
226 chain_id: u32,
227 prev_hash: &[u8; 32],
228 source: &str,
229 kind: &str,
230 timestamp: &DateTime<Utc>,
231 payload_hash: &[u8; 32],
232) -> [u8; 32] {
233 let mut buf = Vec::with_capacity(128);
234 buf.extend_from_slice(&sequence.to_le_bytes());
235 buf.extend_from_slice(&chain_id.to_le_bytes());
236 buf.extend_from_slice(prev_hash);
237 buf.extend_from_slice(source.as_bytes());
238 buf.push(0x00); buf.extend_from_slice(kind.as_bytes());
240 buf.push(0x00); buf.extend_from_slice(×tamp.timestamp().to_le_bytes());
242 buf.extend_from_slice(payload_hash);
243 shake256_256(&buf)
244}
245
246const WITNESS_PROVENANCE: u8 = 0x01;
248
249pub const EVENT_KIND_CAPABILITY_REVOKED: &str = "capability.revoked";
261
262pub const EVENT_KIND_API_CONTRACT_REGISTERED: &str = "service.contract.register";
267
268pub const EVENT_KIND_TOOL_DEPLOYED: &str = "tool.deploy";
270
271pub const EVENT_KIND_TOOL_VERSION_REVOKED: &str = "tool.version.revoke";
273
274pub const EVENT_KIND_SANDBOX_SUDO_OVERRIDE: &str = "sandbox.sudo.override";
279
280pub const EVENT_KIND_TOOL_SIGNED: &str = "tool.signed";
285
286pub const EVENT_KIND_SHELL_EXEC: &str = "shell.exec";
291
292struct LocalChain {
294 chain_id: u32,
295 events: Vec<ChainEvent>,
296 last_hash: [u8; 32],
297 sequence: u64,
298 checkpoint_interval: u64,
299 events_since_checkpoint: u64,
300 checkpoints: Vec<ChainCheckpoint>,
301 witness_entries: Vec<WitnessEntry>,
303}
304
305impl LocalChain {
306 fn new(chain_id: u32, checkpoint_interval: u64) -> Self {
307 Self {
308 chain_id,
309 events: Vec::new(),
310 last_hash: [0u8; 32],
311 sequence: 0,
312 checkpoint_interval,
313 events_since_checkpoint: 0,
314 checkpoints: Vec::new(),
315 witness_entries: Vec::new(),
316 }
317 }
318
319 fn from_events(
321 chain_id: u32,
322 checkpoint_interval: u64,
323 events: Vec<ChainEvent>,
324 witness_entries: Vec<WitnessEntry>,
325 ) -> Self {
326 let (last_hash, sequence) = if let Some(last) = events.last() {
327 (last.hash, last.sequence + 1)
328 } else {
329 ([0u8; 32], 0)
330 };
331 Self {
332 chain_id,
333 events,
334 last_hash,
335 sequence,
336 checkpoint_interval,
337 events_since_checkpoint: 0,
338 checkpoints: Vec::new(),
339 witness_entries,
340 }
341 }
342
343 fn append(
344 &mut self,
345 source: String,
346 kind: String,
347 payload: Option<serde_json::Value>,
348 ) -> &ChainEvent {
349 let timestamp = Utc::now();
350 let payload_hash = compute_payload_hash(&payload);
351 let hash = compute_event_hash(
352 self.sequence,
353 self.chain_id,
354 &self.last_hash,
355 &source,
356 &kind,
357 ×tamp,
358 &payload_hash,
359 );
360
361 let event = ChainEvent {
362 sequence: self.sequence,
363 chain_id: self.chain_id,
364 timestamp,
365 prev_hash: self.last_hash,
366 hash,
367 payload_hash,
368 source,
369 kind,
370 payload,
371 };
372
373 self.witness_entries.push(WitnessEntry {
375 prev_hash: [0u8; 32], action_hash: hash,
377 timestamp_ns: timestamp.timestamp_nanos_opt().unwrap_or(0) as u64,
378 witness_type: WITNESS_PROVENANCE,
379 });
380
381 self.last_hash = hash;
382 self.sequence += 1;
383 self.events_since_checkpoint += 1;
384 self.events.push(event);
385
386 if self.checkpoint_interval > 0
388 && self.events_since_checkpoint >= self.checkpoint_interval
389 {
390 self.create_checkpoint();
391 }
392
393 self.events.last().unwrap()
394 }
395
396 fn create_checkpoint(&mut self) -> ChainCheckpoint {
397 let cp = ChainCheckpoint {
398 chain_id: self.chain_id,
399 sequence: self.sequence.saturating_sub(1),
400 last_hash: self.last_hash,
401 timestamp: Utc::now(),
402 events_since_last: self.events_since_checkpoint,
403 };
404 self.events_since_checkpoint = 0;
405 self.checkpoints.push(cp.clone());
406 cp
407 }
408}
409
410#[derive(Serialize, Deserialize)]
415struct RvfChainPayload {
416 source: String,
417 kind: String,
418 #[serde(default, skip_serializing_if = "Option::is_none")]
419 payload: Option<serde_json::Value>,
420 payload_hash: String,
422 hash: String,
424}
425
426fn hex_hash(h: &[u8; 32]) -> String {
428 h.iter().map(|b| format!("{b:02x}")).collect()
429}
430
431fn hex_encode(data: &[u8]) -> String {
433 data.iter().map(|b| format!("{b:02x}")).collect()
434}
435
436fn hex_decode(s: &str) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
438 if !s.len().is_multiple_of(2) {
439 return Err("hex string has odd length".into());
440 }
441 let mut out = Vec::with_capacity(s.len() / 2);
442 for chunk in s.as_bytes().chunks(2) {
443 let hi = hex_nibble(chunk[0])?;
444 let lo = hex_nibble(chunk[1])?;
445 out.push((hi << 4) | lo);
446 }
447 Ok(out)
448}
449
450fn parse_hex_hash(s: &str) -> Result<[u8; 32], Box<dyn std::error::Error + Send + Sync>> {
452 if s.len() != 64 {
453 return Err(format!("expected 64 hex chars, got {}", s.len()).into());
454 }
455 let mut out = [0u8; 32];
456 for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
457 let hi = hex_nibble(chunk[0])?;
458 let lo = hex_nibble(chunk[1])?;
459 out[i] = (hi << 4) | lo;
460 }
461 Ok(out)
462}
463
464fn hex_nibble(c: u8) -> Result<u8, Box<dyn std::error::Error + Send + Sync>> {
466 match c {
467 b'0'..=b'9' => Ok(c - b'0'),
468 b'a'..=b'f' => Ok(c - b'a' + 10),
469 b'A'..=b'F' => Ok(c - b'A' + 10),
470 _ => Err(format!("invalid hex char: {}", c as char).into()),
471 }
472}
473
474pub struct ChainManager {
480 inner: Mutex<LocalChain>,
481 signing_key: Option<SigningKey>,
483 ml_dsa_key: Option<MlDsa65Key>,
485}
486
487impl ChainManager {
488 pub fn new(chain_id: u32, checkpoint_interval: u64) -> Self {
490 let mut chain = LocalChain::new(chain_id, checkpoint_interval);
491 chain.append(
493 "chain".into(),
494 "genesis".into(),
495 Some(serde_json::json!({ "chain_id": chain_id })),
496 );
497 debug!(chain_id, "local chain initialized with genesis event");
498
499 Self {
500 inner: Mutex::new(chain),
501 signing_key: None,
502 ml_dsa_key: None,
503 }
504 }
505
506 pub fn default_local() -> Self {
508 Self::new(0, 1000)
509 }
510
511 pub fn with_signing_key(mut self, key: SigningKey) -> Self {
513 self.signing_key = Some(key);
514 self
515 }
516
517 pub fn verifying_key(&self) -> Option<VerifyingKey> {
519 self.signing_key.as_ref().map(|k| k.verifying_key())
520 }
521
522 pub fn set_signing_key(&mut self, key: SigningKey) {
525 self.signing_key = Some(key);
526 }
527
528 pub fn has_signing_key(&self) -> bool {
530 self.signing_key.is_some()
531 }
532
533 pub fn set_ml_dsa_key(&mut self, key: MlDsa65Key) {
535 self.ml_dsa_key = Some(key);
536 }
537
538 pub fn has_dual_signing(&self) -> bool {
540 self.signing_key.is_some() && self.ml_dsa_key.is_some()
541 }
542
543 pub fn load_or_create_key(
545 path: &Path,
546 ) -> Result<SigningKey, Box<dyn std::error::Error + Send + Sync>> {
547 if path.exists() {
548 let bytes = std::fs::read(path)?;
549 if bytes.len() != 32 {
550 return Err(format!(
551 "key file is {} bytes, expected 32",
552 bytes.len()
553 )
554 .into());
555 }
556 let key_bytes: [u8; 32] = bytes
557 .try_into()
558 .map_err(|_| "key file not 32 bytes")?;
559 let key = SigningKey::from_bytes(&key_bytes);
560 info!(path = %path.display(), "loaded Ed25519 signing key");
561 Ok(key)
562 } else {
563 use rand::rngs::OsRng;
564 let key = SigningKey::generate(&mut OsRng);
565 if let Some(parent) = path.parent() {
566 std::fs::create_dir_all(parent)?;
567 }
568 std::fs::write(path, key.to_bytes())?;
569 info!(path = %path.display(), "generated new Ed25519 signing key");
570 Ok(key)
571 }
572 }
573
574 pub fn append(
576 &self,
577 source: &str,
578 kind: &str,
579 payload: Option<serde_json::Value>,
580 ) -> ChainEvent {
581 let mut chain = self.inner.lock().unwrap();
582 chain.append(source.into(), kind.into(), payload).clone()
583 }
584
585 pub fn checkpoint(&self) -> ChainCheckpoint {
587 let mut chain = self.inner.lock().unwrap();
588 chain.create_checkpoint()
589 }
590
591 pub fn len(&self) -> usize {
593 self.inner.lock().unwrap().events.len()
594 }
595
596 pub fn is_empty(&self) -> bool {
598 self.inner.lock().unwrap().events.is_empty()
599 }
600
601 pub fn sequence(&self) -> u64 {
603 self.inner.lock().unwrap().sequence
604 }
605
606 pub fn last_hash(&self) -> [u8; 32] {
608 self.inner.lock().unwrap().last_hash
609 }
610
611 pub fn chain_id(&self) -> u32 {
613 self.inner.lock().unwrap().chain_id
614 }
615
616 pub fn tail(&self, n: usize) -> Vec<ChainEvent> {
618 let chain = self.inner.lock().unwrap();
619 if n == 0 || n >= chain.events.len() {
620 chain.events.clone()
621 } else {
622 chain.events[chain.events.len() - n..].to_vec()
623 }
624 }
625
626 pub fn tail_from(&self, after: u64) -> Vec<ChainEvent> {
629 let chain = self.inner.lock().unwrap();
630 chain
631 .events
632 .iter()
633 .filter(|e| e.sequence > after)
634 .cloned()
635 .collect()
636 }
637
638 pub fn head_sequence(&self) -> u64 {
641 let chain = self.inner.lock().unwrap();
642 chain.events.last().map(|e| e.sequence).unwrap_or(0)
643 }
644
645 pub fn head_hash(&self) -> [u8; 32] {
648 let chain = self.inner.lock().unwrap();
649 chain.events.last().map(|e| e.hash).unwrap_or([0u8; 32])
650 }
651
652 pub fn checkpoints(&self) -> Vec<ChainCheckpoint> {
654 self.inner.lock().unwrap().checkpoints.clone()
655 }
656
657 pub fn witness_count(&self) -> usize {
659 self.inner.lock().unwrap().witness_entries.len()
660 }
661
662 pub fn verify_witness(
667 &self,
668 ) -> Result<usize, Box<dyn std::error::Error + Send + Sync>> {
669 let chain = self.inner.lock().unwrap();
670 if chain.witness_entries.is_empty() {
671 return Ok(0);
672 }
673 let data = create_witness_chain(&chain.witness_entries);
674 let verified = verify_witness_chain(&data)
675 .map_err(|e| format!("witness chain verification failed: {e}"))?;
676 Ok(verified.len())
677 }
678
679 pub fn verify_integrity(&self) -> ChainVerifyResult {
686 let chain = self.inner.lock().unwrap();
687 let mut errors = Vec::new();
688
689 for (i, event) in chain.events.iter().enumerate() {
690 let expected_prev = if i == 0 {
692 [0u8; 32]
693 } else {
694 chain.events[i - 1].hash
695 };
696 if event.prev_hash != expected_prev {
697 errors.push(format!(
698 "seq {}: prev_hash mismatch (expected {:02x}{:02x}..., got {:02x}{:02x}...)",
699 event.sequence,
700 expected_prev[0], expected_prev[1],
701 event.prev_hash[0], event.prev_hash[1],
702 ));
703 }
704
705 let recomputed_payload = compute_payload_hash(&event.payload);
707 if event.payload_hash != recomputed_payload {
708 errors.push(format!(
709 "seq {}: payload_hash mismatch (recomputed {:02x}{:02x}..., stored {:02x}{:02x}...)",
710 event.sequence,
711 recomputed_payload[0], recomputed_payload[1],
712 event.payload_hash[0], event.payload_hash[1],
713 ));
714 }
715
716 let recomputed = compute_event_hash(
718 event.sequence,
719 event.chain_id,
720 &event.prev_hash,
721 &event.source,
722 &event.kind,
723 &event.timestamp,
724 &event.payload_hash,
725 );
726 if event.hash != recomputed {
727 errors.push(format!(
728 "seq {}: hash mismatch (recomputed {:02x}{:02x}..., stored {:02x}{:02x}...)",
729 event.sequence,
730 recomputed[0], recomputed[1],
731 event.hash[0], event.hash[1],
732 ));
733 }
734 }
735
736 ChainVerifyResult {
737 valid: errors.is_empty(),
738 event_count: chain.events.len(),
739 errors,
740 signature_verified: None,
741 }
742 }
743
744 pub fn save_to_file(&self, path: &Path) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
749 let chain = self.inner.lock().map_err(|e| format!("lock: {e}"))?;
750
751 if let Some(parent) = path.parent() {
752 std::fs::create_dir_all(parent)?;
753 }
754
755 let mut output = String::new();
756 for event in &chain.events {
757 let line = serde_json::to_string(event)?;
758 output.push_str(&line);
759 output.push('\n');
760 }
761
762 std::fs::write(path, output)?;
763 info!(
764 path = %path.display(),
765 events = chain.events.len(),
766 sequence = chain.sequence,
767 "chain saved to file"
768 );
769 Ok(())
770 }
771
772 pub fn load_from_file(
777 path: &Path,
778 checkpoint_interval: u64,
779 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
780 let contents = std::fs::read_to_string(path)?;
781 let mut events = Vec::new();
782
783 for line in contents.lines() {
784 let trimmed = line.trim();
785 if trimmed.is_empty() {
786 continue;
787 }
788 let event: ChainEvent = serde_json::from_str(trimmed)?;
789 events.push(event);
790 }
791
792 if events.is_empty() {
793 return Err("chain file is empty (no events)".into());
794 }
795
796 let chain_id = events[0].chain_id;
797 let chain = LocalChain::from_events(chain_id, checkpoint_interval, events, Vec::new());
798
799 let mgr = Self {
800 inner: Mutex::new(chain),
801 signing_key: None,
802 ml_dsa_key: None,
803 };
804
805 let result = mgr.verify_integrity();
807 if !result.valid {
808 warn!(
809 errors = result.errors.len(),
810 "loaded chain has integrity errors"
811 );
812 return Err(format!(
813 "chain integrity check failed: {} errors",
814 result.errors.len()
815 )
816 .into());
817 }
818
819 info!(
820 path = %path.display(),
821 events = result.event_count,
822 chain_id,
823 "chain restored from file"
824 );
825 Ok(mgr)
826 }
827
828 pub fn save_to_rvf(
835 &self,
836 path: &Path,
837 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
838 let chain = self.inner.lock().map_err(|e| format!("lock: {e}"))?;
839
840 if let Some(parent) = path.parent() {
841 std::fs::create_dir_all(parent)?;
842 }
843
844 let mut output = Vec::new();
845
846 for event in &chain.events {
847 let exo_header = ExoChainHeader {
849 magic: EXOCHAIN_MAGIC,
850 version: 1,
851 subtype: 0x40, flags: 0,
853 chain_id: event.chain_id,
854 _reserved: 0,
855 sequence: event.sequence,
856 timestamp_secs: event.timestamp.timestamp() as u64,
857 prev_hash: event.prev_hash,
858 };
859
860 let rvf_payload = RvfChainPayload {
862 source: event.source.clone(),
863 kind: event.kind.clone(),
864 payload: event.payload.clone(),
865 payload_hash: hex_hash(&event.payload_hash),
866 hash: hex_hash(&event.hash),
867 };
868
869 let mut cbor_bytes = Vec::new();
870 ciborium::into_writer(&rvf_payload, &mut cbor_bytes)
871 .map_err(|e| format!("cbor encode: {e}"))?;
872
873 let segment = write_exochain_event(&exo_header, &cbor_bytes, event.sequence);
875 output.extend_from_slice(&segment);
876 }
877
878 let checkpoint_header = ExoChainHeader {
880 magic: EXOCHAIN_MAGIC,
881 version: 1,
882 subtype: 0x41, flags: 0,
884 chain_id: chain.chain_id,
885 _reserved: 0,
886 sequence: chain.sequence.saturating_sub(1),
887 timestamp_secs: Utc::now().timestamp() as u64,
888 prev_hash: chain.last_hash,
889 };
890
891 let witness_hex = if !chain.witness_entries.is_empty() {
893 let wc_data = create_witness_chain(&chain.witness_entries);
894 Some(hex_encode(&wc_data))
895 } else {
896 None
897 };
898
899 let cp_payload = serde_json::json!({
900 "event_count": chain.events.len(),
901 "last_hash": hex_hash(&chain.last_hash),
902 "witness_chain": witness_hex,
903 "witness_entries": chain.witness_entries.len(),
904 });
905 let mut cp_cbor = Vec::new();
906 ciborium::into_writer(&cp_payload, &mut cp_cbor)
907 .map_err(|e| format!("cbor encode checkpoint: {e}"))?;
908
909 let cp_segment = write_exochain_event(
910 &checkpoint_header,
911 &cp_cbor,
912 chain.sequence, );
914 output.extend_from_slice(&cp_segment);
915
916 let signed = if let Some(ref signing_key) = self.signing_key {
918 let (cp_seg_header, cp_seg_payload) = read_segment(&cp_segment)
919 .map_err(|e| format!("re-read checkpoint for signing: {e}"))?;
920 let footer = sign_segment(&cp_seg_header, cp_seg_payload, signing_key);
921 let footer_bytes = encode_signature_footer(&footer);
922 output.extend_from_slice(&footer_bytes);
923
924 if let Some(ref ml_key) = self.ml_dsa_key {
926 let ml_footer = sign_segment_ml_dsa(&cp_seg_header, cp_seg_payload, ml_key);
927 let ml_footer_bytes = encode_signature_footer(&ml_footer);
928 output.extend_from_slice(&ml_footer_bytes);
929 }
930
931 true
932 } else {
933 false
934 };
935 let dual_signed = signed && self.ml_dsa_key.is_some();
936
937 std::fs::write(path, &output)?;
938 info!(
939 path = %path.display(),
940 events = chain.events.len(),
941 bytes = output.len(),
942 signed,
943 dual_signed,
944 "chain saved to RVF file"
945 );
946 Ok(())
947 }
948
949 pub fn load_from_rvf(
955 path: &Path,
956 checkpoint_interval: u64,
957 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
958 let data = std::fs::read(path)?;
959 let mut offset = 0;
960 let mut events = Vec::new();
961 let mut witness_entries = Vec::new();
962
963 while offset < data.len() {
964 if data.len() - offset < SEGMENT_HEADER_SIZE {
966 break;
967 }
968
969 let (seg_header, seg_payload) = match read_segment(&data[offset..]) {
972 Ok(result) => result,
973 Err(_) => break,
974 };
975
976 validate_segment(&seg_header, seg_payload)
978 .map_err(|e| format!("validate segment at offset {offset}: {e}"))?;
979
980 let (exo_header, cbor_bytes) = decode_exochain_payload(seg_payload)
982 .ok_or_else(|| {
983 format!("decode exochain payload at offset {offset}")
984 })?;
985
986 if exo_header.subtype == 0x40 {
987 let rvf_payload: RvfChainPayload =
989 ciborium::from_reader(cbor_bytes)
990 .map_err(|e| format!("cbor decode at offset {offset}: {e}"))?;
991
992 let payload_hash = parse_hex_hash(&rvf_payload.payload_hash)?;
993 let hash = parse_hex_hash(&rvf_payload.hash)?;
994
995 let timestamp = DateTime::from_timestamp(
996 exo_header.timestamp_secs as i64,
997 0,
998 )
999 .ok_or_else(|| {
1000 format!(
1001 "invalid timestamp {} at offset {offset}",
1002 exo_header.timestamp_secs
1003 )
1004 })?;
1005
1006 events.push(ChainEvent {
1007 sequence: exo_header.sequence,
1008 chain_id: exo_header.chain_id,
1009 timestamp,
1010 prev_hash: exo_header.prev_hash,
1011 hash,
1012 payload_hash,
1013 source: rvf_payload.source,
1014 kind: rvf_payload.kind,
1015 payload: rvf_payload.payload,
1016 });
1017 } else if exo_header.subtype == 0x41 {
1018 let cp_obj: serde_json::Value = ciborium::from_reader(cbor_bytes)
1020 .unwrap_or_default();
1021 if let Some(wc_hex) = cp_obj.get("witness_chain")
1022 .and_then(|v| v.as_str())
1023 && let Ok(wc_bytes) = hex_decode(wc_hex)
1024 {
1025 match verify_witness_chain(&wc_bytes) {
1026 Ok(entries) => {
1027 witness_entries = entries;
1028 debug!(
1029 count = witness_entries.len(),
1030 "restored witness chain from checkpoint"
1031 );
1032 }
1033 Err(e) => {
1034 warn!("witness chain verification failed on load: {e}");
1035 }
1036 }
1037 }
1038 }
1039 let padded = calculate_padded_size(
1043 SEGMENT_HEADER_SIZE,
1044 seg_header.payload_length as usize,
1045 );
1046 offset += padded;
1047 }
1048
1049 let mut has_signature = false;
1052 let mut has_dual_signature = false;
1053 if offset < data.len() {
1054 if let Ok(first_footer) = decode_signature_footer(&data[offset..]) {
1055 has_signature = true;
1056 let first_footer_size = first_footer.footer_length as usize;
1057 let next_offset = offset + first_footer_size;
1058 if next_offset < data.len() {
1059 if decode_signature_footer(&data[next_offset..]).is_ok() {
1060 has_dual_signature = true;
1061 }
1062 }
1063 }
1064 }
1065
1066 if events.is_empty() {
1067 return Err("RVF file contains no chain events".into());
1068 }
1069
1070 let chain_id = events[0].chain_id;
1071 let chain = LocalChain::from_events(
1072 chain_id, checkpoint_interval, events, witness_entries,
1073 );
1074
1075 let mgr = Self {
1076 inner: Mutex::new(chain),
1077 signing_key: None,
1078 ml_dsa_key: None,
1079 };
1080
1081 let result = mgr.verify_integrity();
1083 if !result.valid {
1084 warn!(
1085 errors = result.errors.len(),
1086 "loaded RVF chain has integrity errors"
1087 );
1088 return Err(format!(
1089 "RVF chain integrity check failed: {} errors",
1090 result.errors.len()
1091 )
1092 .into());
1093 }
1094
1095 info!(
1096 path = %path.display(),
1097 events = result.event_count,
1098 chain_id,
1099 has_signature,
1100 has_dual_signature,
1101 "chain restored from RVF file"
1102 );
1103 Ok(mgr)
1104 }
1105
1106 pub fn record_lineage(
1125 &self,
1126 child_id: [u8; 16],
1127 parent_id: [u8; 16],
1128 parent_hash: [u8; 32],
1129 derivation_type: rvf_types::DerivationType,
1130 mutation_count: u32,
1131 description: &str,
1132 ) -> ChainEvent {
1133 let timestamp_ns = chrono::Utc::now()
1134 .timestamp_nanos_opt()
1135 .unwrap_or(0) as u64;
1136
1137 let record = rvf_types::LineageRecord::new(
1138 child_id,
1139 parent_id,
1140 parent_hash,
1141 derivation_type,
1142 mutation_count,
1143 timestamp_ns,
1144 description,
1145 );
1146
1147 let record_bytes = lineage_record_to_bytes(&record);
1149
1150 {
1152 let mut chain = self.inner.lock().unwrap();
1153 let prev_hash = if let Some(last) = chain.witness_entries.last() {
1154 last.action_hash
1155 } else {
1156 [0u8; 32]
1157 };
1158 let witness = lineage_witness_entry(&record, prev_hash);
1159 chain.witness_entries.push(witness);
1160 }
1161
1162 let payload = serde_json::json!({
1163 "child_id": hex_encode(&child_id),
1164 "parent_id": hex_encode(&parent_id),
1165 "parent_hash": hex_hash(&parent_hash),
1166 "derivation_type": derivation_type as u8,
1167 "mutation_count": mutation_count,
1168 "description": description,
1169 "record_hex": hex_encode(&record_bytes),
1170 });
1171
1172 self.append("lineage", "lineage.derivation", Some(payload))
1173 }
1174
1175 pub fn verify_lineage(&self) -> Result<usize, Box<dyn std::error::Error + Send + Sync>> {
1180 let events = self.tail(0);
1181 let mut identities: Vec<(rvf_types::FileIdentity, [u8; 32])> = Vec::new();
1182
1183 for event in &events {
1184 if event.kind != "lineage.derivation" {
1185 continue;
1186 }
1187 let Some(ref payload) = event.payload else {
1188 continue;
1189 };
1190
1191 let child_id_hex = payload.get("child_id").and_then(|v| v.as_str()).unwrap_or("");
1192 let parent_id_hex = payload.get("parent_id").and_then(|v| v.as_str()).unwrap_or("");
1193 let parent_hash_hex = payload.get("parent_hash").and_then(|v| v.as_str()).unwrap_or("");
1194
1195 let Ok(child_bytes) = hex_decode(child_id_hex) else { continue };
1196 let Ok(parent_bytes) = hex_decode(parent_id_hex) else { continue };
1197 let Ok(parent_hash) = parse_hex_hash(parent_hash_hex) else { continue };
1198
1199 if child_bytes.len() != 16 || parent_bytes.len() != 16 {
1200 continue;
1201 }
1202
1203 let mut child_id = [0u8; 16];
1204 child_id.copy_from_slice(&child_bytes);
1205 let mut parent_id = [0u8; 16];
1206 parent_id.copy_from_slice(&parent_bytes);
1207
1208 let depth = identities
1209 .iter()
1210 .filter(|(fi, _)| fi.file_id == parent_id)
1211 .map(|(fi, _)| fi.lineage_depth + 1)
1212 .next()
1213 .unwrap_or(0);
1214
1215 let fi = rvf_types::FileIdentity {
1216 file_id: child_id,
1217 parent_id,
1218 parent_hash,
1219 lineage_depth: depth,
1220 };
1221
1222 identities.push((fi, event.hash));
1224 }
1225
1226 if identities.is_empty() {
1227 return Ok(0);
1228 }
1229
1230 let count = identities.len();
1236 for (fi, _hash) in &identities {
1237 if fi.is_root() {
1238 continue;
1239 }
1240 let parent_found = identities
1242 .iter()
1243 .any(|(pfi, _)| pfi.file_id == fi.parent_id);
1244 if !parent_found && fi.parent_id != [0u8; 16] {
1245 return Err(format!(
1246 "lineage record for {} references unknown parent {}",
1247 hex_encode(&fi.file_id),
1248 hex_encode(&fi.parent_id),
1249 ).into());
1250 }
1251 }
1252
1253 Ok(count)
1254 }
1255
1256 pub fn record_witness_bundle(
1268 &self,
1269 bundle_bytes: &[u8],
1270 header: &rvf_types::witness::WitnessHeader,
1271 policy_violations: u32,
1272 rollback_count: u32,
1273 ) -> ChainEvent {
1274 let payload = serde_json::json!({
1275 "task_id": hex_encode(&header.task_id),
1276 "outcome": header.outcome,
1277 "governance_mode": header.governance_mode,
1278 "tool_call_count": header.tool_call_count,
1279 "total_cost_microdollars": header.total_cost_microdollars,
1280 "total_latency_ms": header.total_latency_ms,
1281 "total_tokens": header.total_tokens,
1282 "bundle_size": bundle_bytes.len(),
1283 "policy_violations": policy_violations,
1284 "rollback_count": rollback_count,
1285 "bundle": hex_encode(bundle_bytes),
1286 });
1287 self.append("witness", "witness.bundle", Some(payload))
1288 }
1289
1290 pub fn aggregate_scorecard(&self, n: usize) -> rvf_types::witness::Scorecard {
1295 let events = self.tail(n);
1296 let mut builder = rvf_runtime::ScorecardBuilder::new();
1297
1298 for event in &events {
1299 if event.kind != "witness.bundle" {
1300 continue;
1301 }
1302 let Some(ref payload) = event.payload else {
1303 continue;
1304 };
1305 let Some(hex_str) = payload.get("bundle").and_then(|v| v.as_str()) else {
1306 continue;
1307 };
1308 let Ok(bytes) = hex_decode(hex_str) else {
1309 continue;
1310 };
1311 let Ok(parsed) = rvf_runtime::ParsedWitness::parse(&bytes) else {
1312 continue;
1313 };
1314 let violations = payload
1315 .get("policy_violations")
1316 .and_then(|v| v.as_u64())
1317 .unwrap_or(0) as u32;
1318 let rollbacks = payload
1319 .get("rollback_count")
1320 .and_then(|v| v.as_u64())
1321 .unwrap_or(0) as u32;
1322 builder.add_witness(&parsed, violations, rollbacks);
1323 }
1324
1325 builder.finish()
1326 }
1327
1328 pub fn last_tree_root_hash(&self) -> Option<String> {
1336 let chain = self.inner.lock().unwrap();
1337 for event in chain.events.iter().rev() {
1338 let Some(ref payload) = event.payload else {
1339 continue;
1340 };
1341 if let Some(hash) = payload.get("tree_root_hash").and_then(|v| v.as_str()) {
1343 return Some(hash.to_string());
1344 }
1345 if event.source == "tree"
1347 && matches!(event.kind.as_str(), "tree.checkpoint" | "checkpoint")
1348 && let Some(hash) = payload.get("root_hash").and_then(|v| v.as_str())
1349 {
1350 return Some(hash.to_string());
1351 }
1352 }
1353 None
1354 }
1355
1356 pub fn status(&self) -> ChainStatus {
1358 let chain = self.inner.lock().unwrap();
1359 ChainStatus {
1360 chain_id: chain.chain_id,
1361 sequence: chain.sequence,
1362 last_hash: chain.last_hash,
1363 event_count: chain.events.len(),
1364 checkpoint_count: chain.checkpoints.len(),
1365 events_since_checkpoint: chain.events_since_checkpoint,
1366 }
1367 }
1368
1369 pub fn verify_rvf_signature(
1378 path: &Path,
1379 verifying_key: &VerifyingKey,
1380 ) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
1381 let data = std::fs::read(path)?;
1382 let mut offset = 0;
1383 let mut last_seg_start = 0;
1384
1385 while offset < data.len() {
1387 if data.len() - offset < SEGMENT_HEADER_SIZE {
1388 break;
1389 }
1390 let (seg_header, _seg_payload) = match read_segment(&data[offset..]) {
1391 Ok(result) => result,
1392 Err(_) => break,
1393 };
1394 last_seg_start = offset;
1395 let padded = calculate_padded_size(
1396 SEGMENT_HEADER_SIZE,
1397 seg_header.payload_length as usize,
1398 );
1399 offset += padded;
1400 }
1401
1402 if offset >= data.len() {
1404 return Err("no signature footer found in RVF file".into());
1405 }
1406 let footer = decode_signature_footer(&data[offset..])
1407 .map_err(|e| format!("decode signature footer: {e}"))?;
1408
1409 let (cp_seg_header, cp_seg_payload) = read_segment(&data[last_seg_start..])
1411 .map_err(|e| format!("re-read checkpoint segment: {e}"))?;
1412
1413 Ok(verify_segment(
1414 &cp_seg_header,
1415 cp_seg_payload,
1416 &footer,
1417 verifying_key,
1418 ))
1419 }
1420
1421 pub fn verify_rvf_dual_signature(
1426 path: &Path,
1427 ed_key: &VerifyingKey,
1428 ml_key: &MlDsa65VerifyKey,
1429 ) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
1430 let data = std::fs::read(path)?;
1431 let mut offset = 0;
1432 let mut last_seg_start = 0;
1433
1434 while offset < data.len() {
1435 if data.len() - offset < SEGMENT_HEADER_SIZE {
1436 break;
1437 }
1438 let (seg_header, _) = match read_segment(&data[offset..]) {
1439 Ok(result) => result,
1440 Err(_) => break,
1441 };
1442 last_seg_start = offset;
1443 let padded = calculate_padded_size(
1444 SEGMENT_HEADER_SIZE,
1445 seg_header.payload_length as usize,
1446 );
1447 offset += padded;
1448 }
1449
1450 if offset >= data.len() {
1451 return Err("no signature footer found in RVF file".into());
1452 }
1453
1454 let ed_footer = decode_signature_footer(&data[offset..])
1455 .map_err(|e| format!("decode Ed25519 signature footer: {e}"))?;
1456 let ed_footer_size = ed_footer.footer_length as usize;
1457 let ml_offset = offset + ed_footer_size;
1458
1459 if ml_offset >= data.len() {
1460 return Err("no ML-DSA-65 signature footer found (single-signed file)".into());
1461 }
1462 let ml_footer = decode_signature_footer(&data[ml_offset..])
1463 .map_err(|e| format!("decode ML-DSA-65 signature footer: {e}"))?;
1464
1465 let (cp_seg_header, cp_seg_payload) = read_segment(&data[last_seg_start..])
1466 .map_err(|e| format!("re-read checkpoint segment: {e}"))?;
1467
1468 let ed_ok = verify_segment(&cp_seg_header, cp_seg_payload, &ed_footer, ed_key);
1469 let ml_ok = verify_segment_ml_dsa(&cp_seg_header, cp_seg_payload, &ml_footer, ml_key);
1470
1471 Ok(ed_ok && ml_ok)
1472 }
1473
1474 pub fn dual_sign(&self, data: &[u8]) -> Option<DualSignature> {
1482 use ed25519_dalek::Signer;
1483
1484 let signing_key = self.signing_key.as_ref()?;
1485 let ed_sig = signing_key.sign(data);
1486 let ed_bytes = ed_sig.to_bytes().to_vec();
1487
1488 let ml_sig = self.ml_dsa_key.as_ref().map(|ml_key| {
1489 ml_dsa_sign_raw(&ml_key, data)
1491 });
1492
1493 Some(DualSignature {
1494 ed25519: ed_bytes,
1495 ml_dsa65: ml_sig,
1496 })
1497 }
1498
1499 pub fn verify_dual_signature(
1505 data: &[u8],
1506 sig: &DualSignature,
1507 ed25519_pubkey: &VerifyingKey,
1508 ml_dsa_pubkey: Option<&MlDsa65VerifyKey>,
1509 ) -> bool {
1510 use ed25519_dalek::{Signature, Verifier};
1511
1512 if sig.ed25519.len() != 64 {
1514 return false;
1515 }
1516 let ed_sig = match Signature::from_bytes(
1517 sig.ed25519.as_slice().try_into().unwrap_or(&[0u8; 64]),
1518 ) {
1519 s => s,
1520 };
1521 if ed25519_pubkey.verify(data, &ed_sig).is_err() {
1522 return false;
1523 }
1524
1525 if let (Some(ml_sig), Some(ml_key)) = (&sig.ml_dsa65, ml_dsa_pubkey) {
1527 if !ml_dsa_verify_raw(ml_key, data, ml_sig) {
1528 return false;
1529 }
1530 }
1531
1532 true
1533 }
1534}
1535
1536const ML_DSA_RAW_SIG_LEN: usize = 3309;
1543
1544fn ml_dsa_sign_raw(key: &MlDsa65Key, data: &[u8]) -> Vec<u8> {
1546 let vk = key.verifying_key();
1548 let key_bytes = ml_dsa_vk_bytes(&vk);
1549
1550 let mut input = Vec::with_capacity(32 + data.len() + 32);
1551 input.extend_from_slice(&key_bytes);
1552 input.extend_from_slice(data);
1553 input.extend_from_slice(&key_bytes);
1554
1555 let mut sig = Vec::with_capacity(ML_DSA_RAW_SIG_LEN);
1556 let mut block = shake256_256(&input);
1557 while sig.len() < ML_DSA_RAW_SIG_LEN {
1558 sig.extend_from_slice(&block);
1559 let mut next = Vec::with_capacity(64);
1560 next.extend_from_slice(&block);
1561 next.extend_from_slice(&key_bytes);
1562 block = shake256_256(&next);
1563 }
1564 sig.truncate(ML_DSA_RAW_SIG_LEN);
1565 sig
1566}
1567
1568fn ml_dsa_verify_raw(pubkey: &MlDsa65VerifyKey, data: &[u8], sig: &[u8]) -> bool {
1570 let key_bytes = ml_dsa_vk_bytes(pubkey);
1571
1572 let mut input = Vec::with_capacity(32 + data.len() + 32);
1573 input.extend_from_slice(&key_bytes);
1574 input.extend_from_slice(data);
1575 input.extend_from_slice(&key_bytes);
1576
1577 let mut expected = Vec::with_capacity(ML_DSA_RAW_SIG_LEN);
1578 let mut block = shake256_256(&input);
1579 while expected.len() < ML_DSA_RAW_SIG_LEN {
1580 expected.extend_from_slice(&block);
1581 let mut next = Vec::with_capacity(64);
1582 next.extend_from_slice(&block);
1583 next.extend_from_slice(&key_bytes);
1584 block = shake256_256(&next);
1585 }
1586 expected.truncate(ML_DSA_RAW_SIG_LEN);
1587
1588 sig.len() == ML_DSA_RAW_SIG_LEN && sig == expected.as_slice()
1589}
1590
1591fn ml_dsa_vk_bytes(vk: &MlDsa65VerifyKey) -> [u8; 32] {
1598 assert_eq!(
1618 std::mem::size_of::<MlDsa65VerifyKey>(),
1619 32,
1620 "MlDsa65VerifyKey must be exactly 32 bytes"
1621 );
1622 unsafe { std::mem::transmute_copy(vk) }
1625}
1626
1627pub struct DualSigningConfig {
1631 pub ed25519_key: SigningKey,
1633 pub ml_dsa_key: Option<MlDsa65Key>,
1635}
1636
1637#[derive(Debug, Clone, Serialize, Deserialize)]
1639pub struct DualSignature {
1640 pub ed25519: Vec<u8>,
1642 pub ml_dsa65: Option<Vec<u8>>,
1644}
1645
1646#[derive(Debug, Clone, Serialize, Deserialize)]
1648pub struct ChainStatus {
1649 pub chain_id: u32,
1650 pub sequence: u64,
1651 pub last_hash: [u8; 32],
1652 pub event_count: usize,
1653 pub checkpoint_count: usize,
1654 pub events_since_checkpoint: u64,
1655}
1656
1657impl std::fmt::Debug for ChainManager {
1658 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1659 let status = self.status();
1660 f.debug_struct("ChainManager")
1661 .field("chain_id", &status.chain_id)
1662 .field("sequence", &status.sequence)
1663 .field("event_count", &status.event_count)
1664 .finish()
1665 }
1666}
1667
1668#[derive(Debug, Clone, Serialize, Deserialize)]
1674pub struct AnchorReceipt {
1675 pub hash: [u8; 32],
1677 pub tx_id: String,
1679 pub anchored_at: DateTime<Utc>,
1681}
1682
1683pub trait ChainAnchor: Send + Sync {
1688 fn anchor(&self, hash: &[u8; 32]) -> Result<AnchorReceipt, String>;
1690
1691 fn verify(&self, receipt: &AnchorReceipt) -> Result<bool, String>;
1693
1694 fn backend_name(&self) -> &str;
1696}
1697
1698pub struct MockAnchor;
1700
1701impl ChainAnchor for MockAnchor {
1702 fn anchor(&self, hash: &[u8; 32]) -> Result<AnchorReceipt, String> {
1703 Ok(AnchorReceipt {
1704 hash: *hash,
1705 tx_id: format!("mock-{}", hex_hash(hash).chars().take(16).collect::<String>()),
1706 anchored_at: Utc::now(),
1707 })
1708 }
1709
1710 fn verify(&self, _receipt: &AnchorReceipt) -> Result<bool, String> {
1711 Ok(true)
1712 }
1713
1714 fn backend_name(&self) -> &str {
1715 "mock"
1716 }
1717}
1718
1719pub trait ChainLoggable {
1728 fn chain_event_source(&self) -> &str;
1730
1731 fn chain_event_kind(&self) -> &str;
1733
1734 fn chain_event_payload(&self) -> serde_json::Value;
1736}
1737
1738impl ChainManager {
1739 pub fn append_loggable(&self, event: &dyn ChainLoggable) -> ChainEvent {
1741 self.append(
1742 event.chain_event_source(),
1743 event.chain_event_kind(),
1744 Some(event.chain_event_payload()),
1745 )
1746 }
1747}
1748
1749pub struct RestartEvent {
1755 pub agent_id: String,
1757 pub old_pid: u64,
1759 pub new_pid: u64,
1761 pub exit_code: i32,
1763 pub strategy: String,
1765 pub backoff_ms: u64,
1767 pub timestamp: DateTime<Utc>,
1769}
1770
1771impl ChainLoggable for RestartEvent {
1772 fn chain_event_source(&self) -> &str {
1773 "supervisor"
1774 }
1775
1776 fn chain_event_kind(&self) -> &str {
1777 "supervisor.restart"
1778 }
1779
1780 fn chain_event_payload(&self) -> serde_json::Value {
1781 serde_json::json!({
1782 "agent_id": self.agent_id,
1783 "old_pid": self.old_pid,
1784 "new_pid": self.new_pid,
1785 "exit_code": self.exit_code,
1786 "strategy": self.strategy,
1787 "backoff_ms": self.backoff_ms,
1788 "timestamp": self.timestamp.to_rfc3339(),
1789 })
1790 }
1791}
1792
1793pub struct GovernanceDecisionEvent {
1797 pub agent_id: String,
1799 pub action: String,
1801 pub decision: String,
1803 pub effect_magnitude: f64,
1805 pub threshold_exceeded: bool,
1807 pub evaluated_rules: Vec<String>,
1809 pub timestamp: DateTime<Utc>,
1811}
1812
1813impl ChainLoggable for GovernanceDecisionEvent {
1814 fn chain_event_source(&self) -> &str {
1815 "governance"
1816 }
1817
1818 fn chain_event_kind(&self) -> &str {
1819 match self.decision.as_str() {
1820 "Permit" => "governance.permit",
1821 "PermitWithWarning" => "governance.warn",
1822 "EscalateToHuman" => "governance.defer",
1823 "Deny" => "governance.deny",
1824 _ => "governance.unknown",
1825 }
1826 }
1827
1828 fn chain_event_payload(&self) -> serde_json::Value {
1829 serde_json::json!({
1830 "agent_id": self.agent_id,
1831 "action": self.action,
1832 "decision": self.decision,
1833 "effect_magnitude": self.effect_magnitude,
1834 "threshold_exceeded": self.threshold_exceeded,
1835 "evaluated_rules": self.evaluated_rules,
1836 "timestamp": self.timestamp.to_rfc3339(),
1837 })
1838 }
1839}
1840
1841pub struct IpcDeadLetterEvent {
1845 pub message_id: String,
1847 pub from_pid: u64,
1849 pub target: String,
1851 pub payload_type: String,
1853 pub reason: String,
1855 pub timestamp: DateTime<Utc>,
1857}
1858
1859impl ChainLoggable for IpcDeadLetterEvent {
1860 fn chain_event_source(&self) -> &str {
1861 "ipc"
1862 }
1863
1864 fn chain_event_kind(&self) -> &str {
1865 "ipc.dead_letter"
1866 }
1867
1868 fn chain_event_payload(&self) -> serde_json::Value {
1869 serde_json::json!({
1870 "message_id": self.message_id,
1871 "from_pid": self.from_pid,
1872 "target": self.target,
1873 "payload_type": self.payload_type,
1874 "reason": self.reason,
1875 "timestamp": self.timestamp.to_rfc3339(),
1876 })
1877 }
1878}
1879
1880#[cfg(test)]
1881mod tests {
1882 use super::*;
1883
1884 #[test]
1885 fn genesis_event() {
1886 let cm = ChainManager::new(0, 1000);
1887 assert_eq!(cm.len(), 1);
1888 assert_eq!(cm.sequence(), 1); let events = cm.tail(0);
1890 assert_eq!(events[0].kind, "genesis");
1891 assert_eq!(events[0].sequence, 0);
1892 assert_eq!(events[0].prev_hash, [0u8; 32]);
1893 }
1894
1895 #[test]
1896 fn append_links_hashes() {
1897 let cm = ChainManager::new(0, 1000);
1898 let genesis_hash = cm.last_hash();
1899
1900 let e1 = cm.append("test", "event.one", None);
1901 assert_eq!(e1.prev_hash, genesis_hash);
1902 assert_ne!(e1.hash, [0u8; 32]);
1903
1904 let e2 = cm.append("test", "event.two", Some(serde_json::json!({"key": "value"})));
1905 assert_eq!(e2.prev_hash, e1.hash);
1906 }
1907
1908 #[test]
1909 fn checkpoint() {
1910 let cm = ChainManager::new(0, 1000);
1911 cm.append("test", "event", None);
1912
1913 let cp = cm.checkpoint();
1914 assert_eq!(cp.chain_id, 0);
1915 assert_eq!(cp.sequence, 1);
1916 assert_eq!(cm.checkpoints().len(), 1);
1917 }
1918
1919 #[test]
1920 fn tail_from_zero_returns_all() {
1921 let cm = ChainManager::new(0, 1000);
1922 cm.append("test", "event.one", None);
1923 cm.append("test", "event.two", None);
1924
1925 let all = cm.tail_from(0);
1929 assert_eq!(all.len(), 2);
1931 }
1932
1933 #[test]
1934 fn tail_from_n_returns_after() {
1935 let cm = ChainManager::new(0, 1000);
1936 let e1 = cm.append("test", "event.one", None);
1937 let _e2 = cm.append("test", "event.two", None);
1938
1939 let after = cm.tail_from(e1.sequence);
1940 assert_eq!(after.len(), 1);
1941 assert_eq!(after[0].kind, "event.two");
1942 }
1943
1944 #[test]
1945 fn tail_from_head_returns_empty() {
1946 let cm = ChainManager::new(0, 1000);
1947 cm.append("test", "event.one", None);
1948
1949 let head_seq = cm.head_sequence();
1950 let after = cm.tail_from(head_seq);
1951 assert!(after.is_empty());
1952 }
1953
1954 #[test]
1955 fn head_sequence_empty_chain() {
1956 let cm = ChainManager::new(0, 1000);
1959 assert_eq!(cm.head_sequence(), 0);
1960 }
1961
1962 #[test]
1963 fn head_sequence_after_appends() {
1964 let cm = ChainManager::new(0, 1000);
1965 cm.append("test", "a", None);
1966 cm.append("test", "b", None);
1967 assert_eq!(cm.head_sequence(), 2);
1969 }
1970
1971 #[test]
1972 fn head_hash_matches_last_event() {
1973 let cm = ChainManager::new(0, 1000);
1974 let e = cm.append("test", "event", None);
1975 assert_eq!(cm.head_hash(), e.hash);
1976 assert_ne!(cm.head_hash(), [0u8; 32]);
1977 }
1978
1979 #[test]
1980 fn auto_checkpoint() {
1981 let cm = ChainManager::new(0, 5); for i in 0..4 {
1984 cm.append("test", &format!("event.{i}"), None);
1985 }
1986 assert_eq!(cm.checkpoints().len(), 1);
1988 }
1989
1990 #[test]
1991 fn status() {
1992 let cm = ChainManager::new(0, 1000);
1993 cm.append("test", "event", None);
1994 let status = cm.status();
1995 assert_eq!(status.chain_id, 0);
1996 assert_eq!(status.sequence, 2);
1997 assert_eq!(status.event_count, 2);
1998 }
1999
2000 #[test]
2001 fn verify_integrity_valid() {
2002 let cm = ChainManager::new(0, 1000);
2003 cm.append("kernel", "boot.init", None);
2004 cm.append("tree", "bootstrap", Some(serde_json::json!({"nodes": 8})));
2005 cm.append("kernel", "boot.ready", None);
2006
2007 let result = cm.verify_integrity();
2008 assert!(result.valid);
2009 assert_eq!(result.event_count, 4); assert!(result.errors.is_empty());
2011 }
2012
2013 #[test]
2014 fn save_and_load_roundtrip() {
2015 let cm = ChainManager::new(0, 1000);
2016 cm.append("kernel", "boot.init", None);
2017 cm.append("tree", "bootstrap", Some(serde_json::json!({"nodes": 8})));
2018 cm.append("kernel", "boot.ready", None);
2019
2020 let original_seq = cm.sequence();
2021 let original_hash = cm.last_hash();
2022 let original_len = cm.len();
2023
2024 let dir = std::env::temp_dir().join("clawft-chain-test");
2025 let path = dir.join("test-chain.json");
2026 cm.save_to_file(&path).unwrap();
2027
2028 let restored = ChainManager::load_from_file(&path, 1000).unwrap();
2029 assert_eq!(restored.sequence(), original_seq);
2030 assert_eq!(restored.last_hash(), original_hash);
2031 assert_eq!(restored.len(), original_len);
2032 assert_eq!(restored.chain_id(), 0);
2033
2034 let result = restored.verify_integrity();
2036 assert!(result.valid);
2037
2038 let new_event = restored.append("test", "after.restore", None);
2040 assert_eq!(new_event.sequence, original_seq);
2041 assert_eq!(new_event.prev_hash, original_hash);
2042
2043 let _ = std::fs::remove_dir_all(&dir);
2045 }
2046
2047 #[test]
2048 fn load_from_nonexistent_file_fails() {
2049 let result = ChainManager::load_from_file(
2050 &std::path::PathBuf::from("/tmp/nonexistent-chain-file.json"),
2051 1000,
2052 );
2053 assert!(result.is_err());
2054 }
2055
2056 #[test]
2057 fn tail() {
2058 let cm = ChainManager::new(0, 1000);
2059 cm.append("a", "1", None);
2060 cm.append("b", "2", None);
2061 cm.append("c", "3", None);
2062
2063 let last2 = cm.tail(2);
2064 assert_eq!(last2.len(), 2);
2065 assert_eq!(last2[0].kind, "2");
2066 assert_eq!(last2[1].kind, "3");
2067
2068 let all = cm.tail(0);
2069 assert_eq!(all.len(), 4); }
2071
2072 #[test]
2073 fn save_and_load_rvf_roundtrip() {
2074 let cm = ChainManager::new(0, 1000);
2075 cm.append("kernel", "boot.init", None);
2076 cm.append(
2077 "tree",
2078 "bootstrap",
2079 Some(serde_json::json!({"nodes": 8})),
2080 );
2081 cm.append("kernel", "boot.ready", None);
2082
2083 let original_seq = cm.sequence();
2084 let original_hash = cm.last_hash();
2085 let original_len = cm.len();
2086
2087 let dir = std::env::temp_dir().join("clawft-chain-rvf-test");
2088 let path = dir.join("test-chain.rvf");
2089 cm.save_to_rvf(&path).unwrap();
2090
2091 let restored = ChainManager::load_from_rvf(&path, 1000).unwrap();
2092 assert_eq!(restored.sequence(), original_seq);
2093 assert_eq!(restored.last_hash(), original_hash);
2094 assert_eq!(restored.len(), original_len);
2095 assert_eq!(restored.chain_id(), 0);
2096
2097 let result = restored.verify_integrity();
2099 assert!(result.valid, "integrity errors: {:?}", result.errors);
2100 assert_eq!(result.event_count, original_len);
2101
2102 let new_event = restored.append("test", "after.rvf.restore", None);
2104 assert_eq!(new_event.sequence, original_seq);
2105 assert_eq!(new_event.prev_hash, original_hash);
2106
2107 let _ = std::fs::remove_dir_all(&dir);
2109 }
2110
2111 #[test]
2112 fn rvf_validates_on_load() {
2113 let cm = ChainManager::new(0, 1000);
2114 cm.append("kernel", "boot", None);
2115
2116 let dir = std::env::temp_dir().join("clawft-chain-rvf-validate");
2117 let path = dir.join("corrupt.rvf");
2118 cm.save_to_rvf(&path).unwrap();
2119
2120 let mut data = std::fs::read(&path).unwrap();
2122 if data.len() > SEGMENT_HEADER_SIZE + 10 {
2125 data[SEGMENT_HEADER_SIZE + 10] ^= 0xFF;
2126 }
2127 std::fs::write(&path, &data).unwrap();
2128
2129 let result = ChainManager::load_from_rvf(&path, 1000);
2130 assert!(result.is_err(), "expected validation error on corrupted RVF");
2131
2132 let _ = std::fs::remove_dir_all(&dir);
2134 }
2135
2136 #[test]
2137 fn rvf_migration_from_json() {
2138 let cm = ChainManager::new(0, 1000);
2140 cm.append("kernel", "boot.init", None);
2141 cm.append(
2142 "tree",
2143 "bootstrap",
2144 Some(serde_json::json!({"nodes": 4, "name": "test"})),
2145 );
2146 cm.append("kernel", "boot.ready", None);
2147
2148 let dir = std::env::temp_dir().join("clawft-chain-migrate-test");
2149 let json_path = dir.join("chain.json");
2150 let rvf_path = dir.join("chain.rvf");
2151
2152 cm.save_to_file(&json_path).unwrap();
2154 let from_json = ChainManager::load_from_file(&json_path, 1000).unwrap();
2155
2156 from_json.save_to_rvf(&rvf_path).unwrap();
2158
2159 let from_rvf = ChainManager::load_from_rvf(&rvf_path, 1000).unwrap();
2161
2162 assert_eq!(from_rvf.sequence(), cm.sequence());
2163 assert_eq!(from_rvf.last_hash(), cm.last_hash());
2164 assert_eq!(from_rvf.len(), cm.len());
2165 assert_eq!(from_rvf.chain_id(), cm.chain_id());
2166
2167 let original_events = cm.tail(0);
2169 let rvf_events = from_rvf.tail(0);
2170 assert_eq!(original_events.len(), rvf_events.len());
2171 for (orig, loaded) in original_events.iter().zip(rvf_events.iter()) {
2172 assert_eq!(orig.sequence, loaded.sequence);
2173 assert_eq!(orig.chain_id, loaded.chain_id);
2174 assert_eq!(orig.hash, loaded.hash);
2175 assert_eq!(orig.prev_hash, loaded.prev_hash);
2176 assert_eq!(orig.payload_hash, loaded.payload_hash);
2177 assert_eq!(orig.source, loaded.source);
2178 assert_eq!(orig.kind, loaded.kind);
2179 assert_eq!(orig.payload, loaded.payload);
2180 }
2181
2182 let result = from_rvf.verify_integrity();
2184 assert!(result.valid, "integrity errors: {:?}", result.errors);
2185
2186 let _ = std::fs::remove_dir_all(&dir);
2188 }
2189
2190 #[test]
2191 fn ed25519_signed_rvf_roundtrip() {
2192 use ed25519_dalek::SigningKey;
2193 use rand::rngs::OsRng;
2194
2195 let key = SigningKey::generate(&mut OsRng);
2196
2197 let cm = ChainManager::new(0, 1000).with_signing_key(key.clone());
2198 assert!(cm.has_signing_key());
2199 cm.append("kernel", "boot.init", None);
2200 cm.append("tree", "bootstrap", Some(serde_json::json!({"nodes": 8})));
2201 cm.append("kernel", "boot.ready", None);
2202
2203 let original_seq = cm.sequence();
2204 let original_hash = cm.last_hash();
2205 let original_len = cm.len();
2206
2207 let dir = std::env::temp_dir().join("clawft-chain-signed-test");
2208 let path = dir.join("signed-chain.rvf");
2209 cm.save_to_rvf(&path).unwrap();
2210
2211 let _file_size = std::fs::metadata(&path).unwrap().len();
2213
2214 let restored = ChainManager::load_from_rvf(&path, 1000).unwrap();
2216 assert_eq!(restored.sequence(), original_seq);
2217 assert_eq!(restored.last_hash(), original_hash);
2218 assert_eq!(restored.len(), original_len);
2219
2220 let result = restored.verify_integrity();
2221 assert!(result.valid, "integrity errors: {:?}", result.errors);
2222
2223 let pubkey = key.verifying_key();
2225 let sig_valid = ChainManager::verify_rvf_signature(&path, &pubkey).unwrap();
2226 assert!(sig_valid, "signature should be valid");
2227
2228 let wrong_key = SigningKey::generate(&mut OsRng);
2230 let wrong_pubkey = wrong_key.verifying_key();
2231 let sig_wrong = ChainManager::verify_rvf_signature(&path, &wrong_pubkey).unwrap();
2232 assert!(!sig_wrong, "signature should fail with wrong key");
2233
2234 let _ = std::fs::remove_dir_all(&dir);
2236 }
2237
2238 #[test]
2239 fn ed25519_tampered_checkpoint_fails_verification() {
2240 use ed25519_dalek::SigningKey;
2241 use rand::rngs::OsRng;
2242
2243 let key = SigningKey::generate(&mut OsRng);
2244 let cm = ChainManager::new(0, 1000).with_signing_key(key.clone());
2245 cm.append("kernel", "boot", None);
2246
2247 let dir = std::env::temp_dir().join("clawft-chain-tampered-sig");
2248 let path = dir.join("tampered.rvf");
2249 cm.save_to_rvf(&path).unwrap();
2250
2251 let mut data = std::fs::read(&path).unwrap();
2252 let footer_size = 72; let mut offset = 0;
2256 let mut last_seg_start = 0;
2257 while offset + SEGMENT_HEADER_SIZE <= data.len() - footer_size {
2258 match read_segment(&data[offset..]) {
2259 Ok((seg_header, _)) => {
2260 last_seg_start = offset;
2261 let padded = calculate_padded_size(
2262 SEGMENT_HEADER_SIZE,
2263 seg_header.payload_length as usize,
2264 );
2265 offset += padded;
2266 }
2267 Err(_) => break,
2268 }
2269 }
2270
2271 data[last_seg_start + SEGMENT_HEADER_SIZE] ^= 0xFF;
2274 std::fs::write(&path, &data).unwrap();
2275
2276 let pubkey = key.verifying_key();
2278 match ChainManager::verify_rvf_signature(&path, &pubkey) {
2279 Ok(valid) => assert!(!valid, "tampered checkpoint should not verify"),
2280 Err(_) => {} }
2282
2283 let _ = std::fs::remove_dir_all(&dir);
2284 }
2285
2286 #[test]
2287 fn unsigned_rvf_loads_successfully() {
2288 let cm = ChainManager::new(0, 1000); assert!(!cm.has_signing_key());
2291 cm.append("kernel", "boot", None);
2292 cm.append("test", "event", Some(serde_json::json!({"x": 1})));
2293
2294 let dir = std::env::temp_dir().join("clawft-chain-unsigned-test");
2295 let path = dir.join("unsigned.rvf");
2296 cm.save_to_rvf(&path).unwrap();
2297
2298 let restored = ChainManager::load_from_rvf(&path, 1000).unwrap();
2299 assert_eq!(restored.len(), cm.len());
2300 let result = restored.verify_integrity();
2301 assert!(result.valid);
2302
2303 let key = ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng);
2305 let result = ChainManager::verify_rvf_signature(&path, &key.verifying_key());
2306 assert!(result.is_err(), "no signature footer should yield error");
2307
2308 let _ = std::fs::remove_dir_all(&dir);
2309 }
2310
2311 #[test]
2312 fn load_or_create_key_roundtrip() {
2313 let dir = std::env::temp_dir().join("clawft-key-test");
2314 let key_path = dir.join("test-chain.key");
2315 let _ = std::fs::remove_dir_all(&dir);
2316
2317 let key1 = ChainManager::load_or_create_key(&key_path).unwrap();
2319 assert!(key_path.exists());
2320
2321 let key2 = ChainManager::load_or_create_key(&key_path).unwrap();
2323 assert_eq!(key1.to_bytes(), key2.to_bytes());
2324
2325 let _ = std::fs::remove_dir_all(&dir);
2327 }
2328
2329 #[test]
2330 fn witness_chain_created_on_append() {
2331 let cm = ChainManager::new(0, 1000);
2332 assert_eq!(cm.witness_count(), 1); cm.append("kernel", "boot.init", None);
2334 assert_eq!(cm.witness_count(), 2);
2335 cm.append("tree", "bootstrap", Some(serde_json::json!({"n": 8})));
2336 assert_eq!(cm.witness_count(), 3);
2337
2338 let count = cm.verify_witness().unwrap();
2340 assert_eq!(count, 3);
2341 }
2342
2343 #[test]
2344 fn witness_chain_persists_in_rvf() {
2345 let cm = ChainManager::new(0, 1000);
2346 cm.append("kernel", "boot.init", None);
2347 cm.append("tree", "bootstrap", Some(serde_json::json!({"n": 8})));
2348 cm.append("kernel", "boot.ready", None);
2349
2350 let original_witness_count = cm.witness_count();
2351 assert_eq!(original_witness_count, 4); let dir = std::env::temp_dir().join("clawft-chain-witness-test");
2354 let path = dir.join("witness.rvf");
2355 cm.save_to_rvf(&path).unwrap();
2356
2357 let restored = ChainManager::load_from_rvf(&path, 1000).unwrap();
2359 assert_eq!(restored.witness_count(), original_witness_count);
2360
2361 let count = restored.verify_witness().unwrap();
2363 assert_eq!(count, original_witness_count);
2364
2365 let events = restored.tail(0);
2367 let chain = restored.inner.lock().unwrap();
2368 for (event, witness) in events.iter().zip(chain.witness_entries.iter()) {
2369 assert_eq!(
2370 witness.action_hash, event.hash,
2371 "witness action_hash should match event hash for seq {}",
2372 event.sequence,
2373 );
2374 }
2375
2376 let _ = std::fs::remove_dir_all(&dir);
2378 }
2379
2380 #[test]
2381 fn witness_chain_continues_after_restore() {
2382 let cm = ChainManager::new(0, 1000);
2383 cm.append("kernel", "boot", None);
2384
2385 let dir = std::env::temp_dir().join("clawft-chain-witness-continue");
2386 let path = dir.join("continue.rvf");
2387 cm.save_to_rvf(&path).unwrap();
2388
2389 let restored = ChainManager::load_from_rvf(&path, 1000).unwrap();
2390 assert_eq!(restored.witness_count(), 2); restored.append("test", "after.restore", None);
2394 assert_eq!(restored.witness_count(), 3);
2395
2396 let count = restored.verify_witness().unwrap();
2398 assert_eq!(count, 3);
2399
2400 let _ = std::fs::remove_dir_all(&dir);
2402 }
2403
2404 #[test]
2405 fn record_witness_bundle_creates_chain_event() {
2406 use rvf_runtime::{GovernancePolicy, WitnessBuilder};
2407 use rvf_types::witness::TaskOutcome;
2408
2409 let cm = ChainManager::new(0, 1000);
2410 let initial_len = cm.len();
2411
2412 let policy = GovernancePolicy::autonomous();
2413 let builder = WitnessBuilder::new([0xAA; 16], policy)
2414 .with_spec(b"fix auth bug")
2415 .with_outcome(TaskOutcome::Solved);
2416 let (bundle, header) = builder.build().unwrap();
2417
2418 let event = cm.record_witness_bundle(&bundle, &header, 0, 0);
2419 assert_eq!(event.source, "witness");
2420 assert_eq!(event.kind, "witness.bundle");
2421 assert_eq!(cm.len(), initial_len + 1);
2422
2423 let payload = event.payload.unwrap();
2425 assert_eq!(payload["outcome"], TaskOutcome::Solved as u8);
2426 assert!(payload["bundle"].as_str().unwrap().len() > 0);
2427 assert_eq!(payload["policy_violations"], 0);
2428 }
2429
2430 #[test]
2431 fn aggregate_scorecard_from_witness_bundles() {
2432 use rvf_runtime::{GovernancePolicy, WitnessBuilder};
2433 use rvf_types::witness::TaskOutcome;
2434
2435 let cm = ChainManager::new(0, 1000);
2436 let policy = GovernancePolicy::autonomous();
2437
2438 let b1 = WitnessBuilder::new([0x01; 16], policy.clone())
2440 .with_spec(b"task 1")
2441 .with_outcome(TaskOutcome::Solved);
2442 let (bytes1, header1) = b1.build().unwrap();
2443 cm.record_witness_bundle(&bytes1, &header1, 0, 0);
2444
2445 let b2 = WitnessBuilder::new([0x02; 16], policy.clone())
2446 .with_spec(b"task 2")
2447 .with_outcome(TaskOutcome::Failed);
2448 let (bytes2, header2) = b2.build().unwrap();
2449 cm.record_witness_bundle(&bytes2, &header2, 1, 0);
2450
2451 let b3 = WitnessBuilder::new([0x03; 16], policy.clone())
2452 .with_spec(b"task 3")
2453 .with_diff(b"diff")
2454 .with_test_log(b"pass")
2455 .with_outcome(TaskOutcome::Solved);
2456 let (bytes3, header3) = b3.build().unwrap();
2457 cm.record_witness_bundle(&bytes3, &header3, 0, 1);
2458
2459 let card = cm.aggregate_scorecard(0);
2460 assert_eq!(card.total_tasks, 3);
2461 assert_eq!(card.solved, 2);
2462 assert_eq!(card.failed, 1);
2463 assert_eq!(card.policy_violations, 1);
2464 assert_eq!(card.rollback_count, 1);
2465 assert!((card.solve_rate - 0.6667).abs() < 0.01);
2466 }
2467
2468 #[test]
2469 fn aggregate_scorecard_empty_when_no_bundles() {
2470 let cm = ChainManager::new(0, 1000);
2471 cm.append("kernel", "boot", None);
2472
2473 let card = cm.aggregate_scorecard(0);
2474 assert_eq!(card.total_tasks, 0);
2475 assert_eq!(card.solve_rate, 0.0);
2476 }
2477
2478 #[test]
2479 fn witness_bundle_with_tool_calls() {
2480 use rvf_runtime::{GovernancePolicy, WitnessBuilder};
2481 use rvf_types::witness::{PolicyCheck, TaskOutcome, ToolCallEntry};
2482
2483 let cm = ChainManager::new(0, 1000);
2484 let policy = GovernancePolicy::autonomous();
2485
2486 let mut builder = WitnessBuilder::new([0x10; 16], policy)
2487 .with_spec(b"add feature")
2488 .with_outcome(TaskOutcome::Solved);
2489
2490 builder.record_tool_call(ToolCallEntry {
2491 action: b"Read".to_vec(),
2492 args_hash: [0x11; 8],
2493 result_hash: [0x22; 8],
2494 latency_ms: 50,
2495 cost_microdollars: 100,
2496 tokens: 500,
2497 policy_check: PolicyCheck::Allowed,
2498 });
2499 builder.record_tool_call(ToolCallEntry {
2500 action: b"Edit".to_vec(),
2501 args_hash: [0x33; 8],
2502 result_hash: [0x44; 8],
2503 latency_ms: 100,
2504 cost_microdollars: 200,
2505 tokens: 1000,
2506 policy_check: PolicyCheck::Allowed,
2507 });
2508
2509 let (bundle, header) = builder.build().unwrap();
2510 assert_eq!(header.tool_call_count, 2);
2511 assert_eq!(header.total_cost_microdollars, 300);
2512
2513 let event = cm.record_witness_bundle(&bundle, &header, 0, 0);
2514 let payload = event.payload.unwrap();
2515 assert_eq!(payload["tool_call_count"], 2);
2516 assert_eq!(payload["total_cost_microdollars"], 300);
2517
2518 let card = cm.aggregate_scorecard(0);
2520 assert_eq!(card.total_tasks, 1);
2521 assert_eq!(card.solved, 1);
2522 }
2523
2524 #[test]
2525 fn record_lineage_creates_chain_event() {
2526 use rvf_types::DerivationType;
2527
2528 let cm = ChainManager::new(0, 1000);
2529 let initial_len = cm.len();
2530
2531 let event = cm.record_lineage(
2532 [0x01; 16], [0x00; 16], [0x00; 32], DerivationType::Clone,
2536 0,
2537 "root agent",
2538 );
2539
2540 assert_eq!(event.source, "lineage");
2541 assert_eq!(event.kind, "lineage.derivation");
2542 assert_eq!(cm.len(), initial_len + 1);
2543
2544 let payload = event.payload.unwrap();
2545 assert_eq!(payload["derivation_type"], DerivationType::Clone as u8);
2546 assert_eq!(payload["description"], "root agent");
2547 assert!(payload["record_hex"].as_str().unwrap().len() > 0);
2548 }
2549
2550 #[test]
2551 fn record_lineage_parent_child() {
2552 use rvf_types::DerivationType;
2553
2554 let cm = ChainManager::new(0, 1000);
2555
2556 let root_event = cm.record_lineage(
2558 [0x01; 16],
2559 [0x00; 16],
2560 [0x00; 32],
2561 DerivationType::Clone,
2562 0,
2563 "root agent",
2564 );
2565
2566 let child_event = cm.record_lineage(
2568 [0x02; 16],
2569 [0x01; 16],
2570 root_event.hash,
2571 DerivationType::Transform,
2572 1,
2573 "spawned worker",
2574 );
2575
2576 let payload = child_event.payload.unwrap();
2577 assert_eq!(payload["derivation_type"], DerivationType::Transform as u8);
2578 assert_eq!(payload["mutation_count"], 1);
2579
2580 let count = cm.verify_lineage().unwrap();
2582 assert_eq!(count, 2);
2583 }
2584
2585 #[test]
2586 fn lineage_adds_witness_entry() {
2587 use rvf_types::DerivationType;
2588
2589 let cm = ChainManager::new(0, 1000);
2590 let initial_witness_count = cm.witness_count();
2591
2592 cm.record_lineage(
2593 [0x01; 16],
2594 [0x00; 16],
2595 [0x00; 32],
2596 DerivationType::Clone,
2597 0,
2598 "agent",
2599 );
2600
2601 assert!(cm.witness_count() > initial_witness_count);
2603 }
2604
2605 #[test]
2606 fn verify_lineage_empty_returns_zero() {
2607 let cm = ChainManager::new(0, 1000);
2608 let count = cm.verify_lineage().unwrap();
2609 assert_eq!(count, 0);
2610 }
2611
2612 #[test]
2613 fn lineage_record_hex_roundtrip() {
2614 use rvf_types::DerivationType;
2615
2616 let cm = ChainManager::new(0, 1000);
2617 let event = cm.record_lineage(
2618 [0xAA; 16],
2619 [0xBB; 16],
2620 [0xCC; 32],
2621 DerivationType::Filter,
2622 42,
2623 "filtered set",
2624 );
2625
2626 let payload = event.payload.unwrap();
2628 let record_hex = payload["record_hex"].as_str().unwrap();
2629 let record_bytes = hex_decode(record_hex).unwrap();
2630 assert_eq!(record_bytes.len(), 128); let record: rvf_types::LineageRecord =
2633 weftos_rvf_crypto::lineage_record_from_bytes(
2634 record_bytes.as_slice().try_into().unwrap(),
2635 )
2636 .unwrap();
2637 assert_eq!(record.file_id, [0xAA; 16]);
2638 assert_eq!(record.parent_id, [0xBB; 16]);
2639 assert_eq!(record.parent_hash, [0xCC; 32]);
2640 assert_eq!(record.derivation_type, DerivationType::Filter);
2641 assert_eq!(record.mutation_count, 42);
2642 assert_eq!(record.description_str(), "filtered set");
2643 }
2644
2645 #[test]
2646 fn last_tree_root_hash_from_shutdown() {
2647 let cm = ChainManager::new(0, 1000);
2648 assert!(cm.last_tree_root_hash().is_none());
2650
2651 cm.append(
2653 "kernel",
2654 "shutdown",
2655 Some(serde_json::json!({
2656 "tree_root_hash": "aabb00112233445566778899",
2657 "chain_seq": 5,
2658 })),
2659 );
2660
2661 let hash = cm.last_tree_root_hash().unwrap();
2662 assert_eq!(hash, "aabb00112233445566778899");
2663 }
2664
2665 #[test]
2666 fn last_tree_root_hash_from_tree_checkpoint() {
2667 let cm = ChainManager::new(0, 1000);
2668
2669 cm.append(
2671 "tree",
2672 "tree.checkpoint",
2673 Some(serde_json::json!({
2674 "path": "/tmp/tree.json",
2675 "root_hash": "deadbeef01234567890abcdef0123456",
2676 })),
2677 );
2678
2679 let hash = cm.last_tree_root_hash().unwrap();
2680 assert_eq!(hash, "deadbeef01234567890abcdef0123456");
2681 }
2682
2683 #[test]
2684 fn last_tree_root_hash_prefers_most_recent() {
2685 let cm = ChainManager::new(0, 1000);
2686
2687 cm.append(
2689 "kernel",
2690 "boot.ready",
2691 Some(serde_json::json!({ "tree_root_hash": "old_hash" })),
2692 );
2693
2694 cm.append(
2696 "kernel",
2697 "shutdown",
2698 Some(serde_json::json!({ "tree_root_hash": "new_hash" })),
2699 );
2700
2701 let hash = cm.last_tree_root_hash().unwrap();
2702 assert_eq!(hash, "new_hash");
2703 }
2704
2705 #[test]
2706 fn last_tree_root_hash_ignores_non_tree_root_hash() {
2707 let cm = ChainManager::new(0, 1000);
2708 cm.append(
2710 "kernel",
2711 "boot.init",
2712 Some(serde_json::json!({ "root_hash": "should_not_match" })),
2713 );
2714 assert!(cm.last_tree_root_hash().is_none());
2715 }
2716
2717 #[test]
2720 fn mock_anchor_roundtrip() {
2721 let anchor = MockAnchor;
2722 let hash = [42u8; 32];
2723 let receipt = anchor.anchor(&hash).unwrap();
2724 assert_eq!(receipt.hash, hash);
2725 assert!(receipt.tx_id.starts_with("mock-"));
2726 assert!(anchor.verify(&receipt).unwrap());
2727 }
2728
2729 #[test]
2730 fn anchor_receipt_serde() {
2731 let receipt = AnchorReceipt {
2732 hash: [1u8; 32],
2733 tx_id: "test-tx".into(),
2734 anchored_at: Utc::now(),
2735 };
2736 let json = serde_json::to_string(&receipt).unwrap();
2737 let restored: AnchorReceipt = serde_json::from_str(&json).unwrap();
2738 assert_eq!(restored.hash, receipt.hash);
2739 assert_eq!(restored.tx_id, "test-tx");
2740 }
2741
2742 #[test]
2743 fn ml_dsa_key_set_and_has() {
2744 let mut cm = ChainManager::new(0, 1000);
2745 assert!(!cm.has_dual_signing());
2746
2747 let ed_key = ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng);
2749 cm.set_signing_key(ed_key);
2750 assert!(cm.has_signing_key());
2751 assert!(!cm.has_dual_signing());
2752
2753 let (ml_key, _) = weftos_rvf_crypto::MlDsa65Key::generate(b"test-seed");
2755 cm.set_ml_dsa_key(ml_key);
2756 assert!(cm.has_dual_signing());
2757 }
2758
2759 #[test]
2760 fn dual_sign_rvf_roundtrip() {
2761 use ed25519_dalek::SigningKey;
2762 use rand::rngs::OsRng;
2763
2764 let ed_key = SigningKey::generate(&mut OsRng);
2765 let (ml_key, ml_vk) = weftos_rvf_crypto::MlDsa65Key::generate(&ed_key.to_bytes());
2766
2767 let mut cm = ChainManager::new(0, 1000)
2768 .with_signing_key(ed_key.clone());
2769 cm.set_ml_dsa_key(ml_key);
2770 assert!(cm.has_dual_signing());
2771
2772 cm.append("kernel", "boot.init", None);
2773 cm.append("tree", "bootstrap", Some(serde_json::json!({"nodes": 4})));
2774 cm.append("kernel", "boot.ready", None);
2775
2776 let original_seq = cm.sequence();
2777 let original_hash = cm.last_hash();
2778 let original_len = cm.len();
2779
2780 let dir = std::env::temp_dir().join("clawft-chain-dual-sign-test");
2781 let path = dir.join("dual-signed.rvf");
2782 cm.save_to_rvf(&path).unwrap();
2783
2784 let restored = ChainManager::load_from_rvf(&path, 1000).unwrap();
2786 assert_eq!(restored.sequence(), original_seq);
2787 assert_eq!(restored.last_hash(), original_hash);
2788 assert_eq!(restored.len(), original_len);
2789
2790 let result = restored.verify_integrity();
2791 assert!(result.valid, "integrity errors: {:?}", result.errors);
2792
2793 let ed_pubkey = ed_key.verifying_key();
2795 let ed_valid = ChainManager::verify_rvf_signature(&path, &ed_pubkey).unwrap();
2796 assert!(ed_valid, "Ed25519 signature should be valid");
2797
2798 let dual_valid = ChainManager::verify_rvf_dual_signature(&path, &ed_pubkey, &ml_vk).unwrap();
2800 assert!(dual_valid, "dual signature should be valid");
2801
2802 let (_, wrong_ml_vk) = weftos_rvf_crypto::MlDsa65Key::generate(b"wrong-seed");
2804 let dual_wrong = ChainManager::verify_rvf_dual_signature(&path, &ed_pubkey, &wrong_ml_vk).unwrap();
2805 assert!(!dual_wrong, "dual signature should fail with wrong ML-DSA key");
2806
2807 let _ = std::fs::remove_dir_all(&dir);
2808 }
2809
2810 #[test]
2811 fn dual_signed_checkpoint_verifies() {
2812 use ed25519_dalek::SigningKey;
2813 use rand::rngs::OsRng;
2814
2815 let ed_key = SigningKey::generate(&mut OsRng);
2816 let (ml_key, ml_vk) = weftos_rvf_crypto::MlDsa65Key::generate(b"checkpoint-test");
2817
2818 let mut cm = ChainManager::new(0, 1000)
2819 .with_signing_key(ed_key.clone());
2820 cm.set_ml_dsa_key(ml_key);
2821 cm.append("kernel", "boot", None);
2822
2823 let dir = std::env::temp_dir().join("clawft-chain-dual-cp-test");
2824 let path = dir.join("dual-cp.rvf");
2825 cm.save_to_rvf(&path).unwrap();
2826
2827 let data = std::fs::read(&path).unwrap();
2829 let mut offset = 0;
2830 while offset + SEGMENT_HEADER_SIZE <= data.len() {
2831 match read_segment(&data[offset..]) {
2832 Ok((seg_header, _)) => {
2833 let padded = calculate_padded_size(
2834 SEGMENT_HEADER_SIZE,
2835 seg_header.payload_length as usize,
2836 );
2837 offset += padded;
2838 }
2839 Err(_) => break,
2840 }
2841 }
2842 let ed_footer = decode_signature_footer(&data[offset..]).unwrap();
2844 assert_eq!(ed_footer.sig_algo, 0); assert_eq!(ed_footer.sig_length, 64);
2846
2847 let ml_offset = offset + ed_footer.footer_length as usize;
2849 let ml_footer = decode_signature_footer(&data[ml_offset..]).unwrap();
2850 assert_eq!(ml_footer.sig_algo, 1); assert_eq!(ml_footer.sig_length, 3309);
2852
2853 let ed_pubkey = ed_key.verifying_key();
2855 let dual_valid = ChainManager::verify_rvf_dual_signature(&path, &ed_pubkey, &ml_vk).unwrap();
2856 assert!(dual_valid);
2857
2858 let _ = std::fs::remove_dir_all(&dir);
2859 }
2860
2861 #[test]
2862 fn k6_cryptographic_filesystem_creates_and_retrieves() {
2863 let cm = ChainManager::new(0, 1000);
2868
2869 let entry = cm.append(
2871 "fs",
2872 "file.create",
2873 Some(serde_json::json!({
2874 "path": "/data/config.json",
2875 "content_hash": "abc123def456",
2876 "size": 1024,
2877 })),
2878 );
2879
2880 assert!(!entry.hash.iter().all(|&b| b == 0), "entry hash must be non-zero");
2882 assert!(
2883 !entry.prev_hash.iter().all(|&b| b == 0),
2884 "prev_hash must link to genesis (non-zero)"
2885 );
2886 assert!(
2887 !entry.payload_hash.iter().all(|&b| b == 0),
2888 "payload_hash must be non-zero when payload present"
2889 );
2890
2891 let events = cm.tail_from(0);
2893 assert!(!events.is_empty(), "must retrieve at least the created entry");
2894 let found = events.iter().find(|e| e.kind == "file.create");
2895 assert!(found.is_some(), "file.create entry must be retrievable");
2896
2897 let retrieved = found.unwrap();
2898 assert_eq!(retrieved.hash, entry.hash);
2899 let payload = retrieved.payload.as_ref().unwrap();
2900 assert_eq!(payload["path"], "/data/config.json");
2901 assert_eq!(payload["content_hash"], "abc123def456");
2902 assert_eq!(payload["size"], 1024);
2903
2904 let result = cm.verify_integrity();
2906 assert!(result.valid, "chain integrity must hold after fs entry");
2907 }
2908
2909 #[test]
2912 fn dual_signature_ed25519_only() {
2913 use ed25519_dalek::SigningKey;
2914 use rand::rngs::OsRng;
2915
2916 let ed_key = SigningKey::generate(&mut OsRng);
2917 let cm = ChainManager::new(0, 1000).with_signing_key(ed_key.clone());
2918 let data = b"cross-node chain event payload";
2921 let sig = cm.dual_sign(data).expect("should produce a signature");
2922
2923 assert_eq!(sig.ed25519.len(), 64, "Ed25519 signature must be 64 bytes");
2924 assert!(sig.ml_dsa65.is_none(), "ML-DSA-65 should be absent without key");
2925 }
2926
2927 #[test]
2928 fn dual_signature_both_algorithms() {
2929 use ed25519_dalek::SigningKey;
2930 use rand::rngs::OsRng;
2931
2932 let ed_key = SigningKey::generate(&mut OsRng);
2933 let (ml_key, _ml_vk) = weftos_rvf_crypto::MlDsa65Key::generate(b"dual-sig-test");
2934
2935 let mut cm = ChainManager::new(0, 1000).with_signing_key(ed_key.clone());
2936 cm.set_ml_dsa_key(ml_key);
2937 assert!(cm.has_dual_signing());
2938
2939 let data = b"cross-node chain event with PQ protection";
2940 let sig = cm.dual_sign(data).expect("should produce dual signature");
2941
2942 assert_eq!(sig.ed25519.len(), 64);
2943 let ml = sig.ml_dsa65.as_ref().expect("ML-DSA-65 should be present");
2944 assert_eq!(ml.len(), 3309, "ML-DSA-65 signature must be 3309 bytes");
2945 }
2946
2947 #[test]
2948 fn verify_dual_signature_ed25519_valid() {
2949 use ed25519_dalek::SigningKey;
2950 use rand::rngs::OsRng;
2951
2952 let ed_key = SigningKey::generate(&mut OsRng);
2953 let cm = ChainManager::new(0, 1000).with_signing_key(ed_key.clone());
2954
2955 let data = b"verify-ed25519-only";
2956 let sig = cm.dual_sign(data).unwrap();
2957 let ed_pub = ed_key.verifying_key();
2958
2959 assert!(
2960 ChainManager::verify_dual_signature(data, &sig, &ed_pub, None),
2961 "Ed25519-only dual signature should verify"
2962 );
2963 }
2964
2965 #[test]
2966 fn verify_dual_signature_both_valid() {
2967 use ed25519_dalek::SigningKey;
2968 use rand::rngs::OsRng;
2969
2970 let ed_key = SigningKey::generate(&mut OsRng);
2971 let (ml_key, ml_vk) = weftos_rvf_crypto::MlDsa65Key::generate(b"verify-both");
2972
2973 let mut cm = ChainManager::new(0, 1000).with_signing_key(ed_key.clone());
2974 cm.set_ml_dsa_key(ml_key);
2975
2976 let data = b"verify-both-algorithms";
2977 let sig = cm.dual_sign(data).unwrap();
2978 let ed_pub = ed_key.verifying_key();
2979
2980 assert!(
2981 ChainManager::verify_dual_signature(data, &sig, &ed_pub, Some(&ml_vk)),
2982 "dual signature with both algorithms should verify"
2983 );
2984 }
2985
2986 #[test]
2987 fn verify_dual_signature_rejects_tampered() {
2988 use ed25519_dalek::SigningKey;
2989 use rand::rngs::OsRng;
2990
2991 let ed_key = SigningKey::generate(&mut OsRng);
2992 let (ml_key, ml_vk) = weftos_rvf_crypto::MlDsa65Key::generate(b"tamper-test");
2993
2994 let mut cm = ChainManager::new(0, 1000).with_signing_key(ed_key.clone());
2995 cm.set_ml_dsa_key(ml_key);
2996
2997 let data = b"original data";
2998 let sig = cm.dual_sign(data).unwrap();
2999 let ed_pub = ed_key.verifying_key();
3000
3001 let tampered = b"tampered data";
3003 assert!(
3004 !ChainManager::verify_dual_signature(tampered, &sig, &ed_pub, Some(&ml_vk)),
3005 "tampered data must fail verification"
3006 );
3007
3008 let mut bad_sig = sig.clone();
3010 if let Some(ref mut ml) = bad_sig.ml_dsa65 {
3011 ml[0] ^= 0xFF;
3012 }
3013 assert!(
3014 !ChainManager::verify_dual_signature(data, &bad_sig, &ed_pub, Some(&ml_vk)),
3015 "tampered ML-DSA-65 signature must fail verification"
3016 );
3017 }
3018
3019 #[test]
3022 fn chain_event_serde_roundtrip() {
3023 let cm = ChainManager::new(0, 1000);
3024 cm.append("test", "agent.spawn", Some(serde_json::json!({"name": "test-agent"})));
3025 let events = cm.tail(1);
3026 let event = &events[0];
3027
3028 let json = serde_json::to_string(event).unwrap();
3029 let restored: ChainEvent = serde_json::from_str(&json).unwrap();
3030 assert_eq!(restored.sequence, event.sequence);
3031 assert_eq!(restored.source, "test");
3032 assert_eq!(restored.kind, "agent.spawn");
3033 assert!(restored.payload.is_some());
3034 }
3035
3036 #[test]
3037 fn chain_event_without_payload_roundtrip() {
3038 let cm = ChainManager::new(0, 1000);
3039 cm.append("kernel", "boot.complete", None);
3040 let events = cm.tail(1);
3041 let event = &events[0];
3042
3043 let json = serde_json::to_string(event).unwrap();
3044 let restored: ChainEvent = serde_json::from_str(&json).unwrap();
3045 assert!(restored.payload.is_none());
3046 assert_eq!(restored.kind, "boot.complete");
3047 }
3048
3049 #[test]
3050 fn chain_checkpoint_serde_roundtrip() {
3051 let cm = ChainManager::new(0, 1000);
3052 cm.append("test", "event.1", None);
3053 cm.append("test", "event.2", None);
3054 let cp = cm.checkpoint();
3055
3056 let json = serde_json::to_string(&cp).unwrap();
3057 let restored: ChainCheckpoint = serde_json::from_str(&json).unwrap();
3058 assert_eq!(restored.chain_id, cp.chain_id);
3059 assert_eq!(restored.sequence, cp.sequence);
3060 assert_eq!(restored.last_hash, cp.last_hash);
3061 }
3062
3063 #[test]
3064 fn chain_verify_result_serde_roundtrip_valid() {
3065 let result = ChainVerifyResult {
3066 valid: true,
3067 event_count: 10,
3068 errors: vec![],
3069 signature_verified: Some(true),
3070 };
3071 let json = serde_json::to_string(&result).unwrap();
3072 let restored: ChainVerifyResult = serde_json::from_str(&json).unwrap();
3073 assert!(restored.valid);
3074 assert_eq!(restored.event_count, 10);
3075 assert!(restored.errors.is_empty());
3076 assert_eq!(restored.signature_verified, Some(true));
3077 }
3078
3079 #[test]
3080 fn chain_verify_result_serde_roundtrip_invalid() {
3081 let result = ChainVerifyResult {
3082 valid: false,
3083 event_count: 5,
3084 errors: vec!["hash mismatch at seq 3".into()],
3085 signature_verified: None,
3086 };
3087 let json = serde_json::to_string(&result).unwrap();
3088 let restored: ChainVerifyResult = serde_json::from_str(&json).unwrap();
3089 assert!(!restored.valid);
3090 assert_eq!(restored.errors.len(), 1);
3091 assert!(restored.signature_verified.is_none());
3092 }
3093
3094 #[test]
3095 fn chain_status_serde_roundtrip() {
3096 let cm = ChainManager::new(42, 1000);
3097 cm.append("test", "event.1", None);
3098 cm.append("test", "event.2", None);
3099 let status = cm.status();
3100
3101 let json = serde_json::to_string(&status).unwrap();
3102 let restored: ChainStatus = serde_json::from_str(&json).unwrap();
3103 assert_eq!(restored.chain_id, 42);
3104 assert_eq!(restored.sequence, status.sequence);
3105 assert_eq!(restored.event_count, status.event_count);
3106 }
3107
3108 #[test]
3109 fn dual_signature_serde_roundtrip() {
3110 let sig = DualSignature {
3111 ed25519: vec![0xCA, 0xFE, 0xBA, 0xBE],
3112 ml_dsa65: Some(vec![0xDE, 0xAD]),
3113 };
3114 let json = serde_json::to_string(&sig).unwrap();
3115 let restored: DualSignature = serde_json::from_str(&json).unwrap();
3116 assert_eq!(restored.ed25519, vec![0xCA, 0xFE, 0xBA, 0xBE]);
3117 assert_eq!(restored.ml_dsa65.unwrap(), vec![0xDE, 0xAD]);
3118 }
3119
3120 #[test]
3121 fn dual_signature_without_ml_dsa_roundtrip() {
3122 let sig = DualSignature {
3123 ed25519: vec![0x01, 0x02],
3124 ml_dsa65: None,
3125 };
3126 let json = serde_json::to_string(&sig).unwrap();
3127 let restored: DualSignature = serde_json::from_str(&json).unwrap();
3128 assert!(restored.ml_dsa65.is_none());
3129 }
3130
3131 #[test]
3132 fn anchor_receipt_serde_roundtrip() {
3133 let receipt = AnchorReceipt {
3134 hash: [0xABu8; 32],
3135 tx_id: "tx-deadbeef".into(),
3136 anchored_at: chrono::Utc::now(),
3137 };
3138 let json = serde_json::to_string(&receipt).unwrap();
3139 let restored: AnchorReceipt = serde_json::from_str(&json).unwrap();
3140 assert_eq!(restored.hash, [0xABu8; 32]);
3141 assert_eq!(restored.tx_id, "tx-deadbeef");
3142 }
3143
3144 #[test]
3145 fn chain_genesis_event_hash_is_nonzero() {
3146 let cm = ChainManager::new(0, 1000);
3147 let events = cm.tail(1);
3148 assert_eq!(events[0].kind, "genesis");
3149 assert_ne!(events[0].hash, [0u8; 32]);
3150 }
3151
3152 #[test]
3153 fn chain_genesis_prev_hash_is_zero() {
3154 let cm = ChainManager::new(0, 1000);
3155 let events = cm.tail(1);
3156 assert_eq!(events[0].prev_hash, [0u8; 32]);
3157 }
3158
3159 #[test]
3162 fn restart_event_loggable() {
3163 let cm = ChainManager::new(0, 100);
3164 let initial = cm.len();
3165
3166 let event = RestartEvent {
3167 agent_id: "agent-coder".into(),
3168 old_pid: 5,
3169 new_pid: 12,
3170 exit_code: 1,
3171 strategy: "OneForOne".into(),
3172 backoff_ms: 200,
3173 timestamp: Utc::now(),
3174 };
3175
3176 assert_eq!(event.chain_event_source(), "supervisor");
3177 assert_eq!(event.chain_event_kind(), "supervisor.restart");
3178
3179 let chain_event = cm.append_loggable(&event);
3180 assert_eq!(cm.len(), initial + 1);
3181 assert_eq!(chain_event.source, "supervisor");
3182 assert_eq!(chain_event.kind, "supervisor.restart");
3183
3184 let payload = chain_event.payload.unwrap();
3185 assert_eq!(payload["agent_id"], "agent-coder");
3186 assert_eq!(payload["old_pid"], 5);
3187 assert_eq!(payload["new_pid"], 12);
3188 assert_eq!(payload["exit_code"], 1);
3189 }
3190
3191 #[test]
3192 fn governance_decision_event_loggable() {
3193 let cm = ChainManager::new(0, 100);
3194
3195 let event = GovernanceDecisionEvent {
3196 agent_id: "agent-1".into(),
3197 action: "tool.exec".into(),
3198 decision: "Deny".into(),
3199 effect_magnitude: 0.85,
3200 threshold_exceeded: true,
3201 evaluated_rules: vec!["security-check".into()],
3202 timestamp: Utc::now(),
3203 };
3204
3205 assert_eq!(event.chain_event_source(), "governance");
3206 assert_eq!(event.chain_event_kind(), "governance.deny");
3207
3208 let chain_event = cm.append_loggable(&event);
3209 assert_eq!(chain_event.kind, "governance.deny");
3210
3211 let payload = chain_event.payload.unwrap();
3212 assert_eq!(payload["agent_id"], "agent-1");
3213 assert_eq!(payload["action"], "tool.exec");
3214 assert!(payload["threshold_exceeded"].as_bool().unwrap());
3215
3216 let make_event = |decision: &str| GovernanceDecisionEvent {
3218 agent_id: "a".into(),
3219 action: "a".into(),
3220 decision: decision.into(),
3221 effect_magnitude: 0.0,
3222 threshold_exceeded: false,
3223 evaluated_rules: vec![],
3224 timestamp: Utc::now(),
3225 };
3226
3227 assert_eq!(make_event("Permit").chain_event_kind(), "governance.permit");
3228 assert_eq!(make_event("PermitWithWarning").chain_event_kind(), "governance.warn");
3229 assert_eq!(make_event("EscalateToHuman").chain_event_kind(), "governance.defer");
3230 assert_eq!(make_event("Deny").chain_event_kind(), "governance.deny");
3231 }
3232
3233 #[test]
3234 fn ipc_dead_letter_event_loggable() {
3235 let cm = ChainManager::new(0, 100);
3236
3237 let event = IpcDeadLetterEvent {
3238 message_id: "msg-abc".into(),
3239 from_pid: 3,
3240 target: "Process(99)".into(),
3241 payload_type: "text".into(),
3242 reason: "target_not_found(pid=99)".into(),
3243 timestamp: Utc::now(),
3244 };
3245
3246 assert_eq!(event.chain_event_source(), "ipc");
3247 assert_eq!(event.chain_event_kind(), "ipc.dead_letter");
3248
3249 let chain_event = cm.append_loggable(&event);
3250 assert_eq!(chain_event.source, "ipc");
3251 assert_eq!(chain_event.kind, "ipc.dead_letter");
3252
3253 let payload = chain_event.payload.unwrap();
3254 assert_eq!(payload["message_id"], "msg-abc");
3255 assert_eq!(payload["from_pid"], 3);
3256 assert_eq!(payload["reason"], "target_not_found(pid=99)");
3257 }
3258
3259 #[test]
3260 fn append_loggable_links_hashes() {
3261 let cm = ChainManager::new(0, 100);
3262 let hash_before = cm.last_hash();
3263
3264 let event = RestartEvent {
3265 agent_id: "test".into(),
3266 old_pid: 1,
3267 new_pid: 2,
3268 exit_code: 1,
3269 strategy: "OneForOne".into(),
3270 backoff_ms: 100,
3271 timestamp: Utc::now(),
3272 };
3273
3274 let chain_event = cm.append_loggable(&event);
3275 assert_eq!(chain_event.prev_hash, hash_before);
3276 assert_ne!(chain_event.hash, [0u8; 32]);
3277 }
3278}