Skip to main content

clawft_kernel/
chain.rs

1//! Local exochain manager for kernel event logging.
2//!
3//! Provides an append-only event chain with SHAKE-256 hash linking
4//! (via [`weftos_rvf_crypto`]). Each event references the hash of the
5//! previous event *and* a content hash of its payload, forming
6//! an immutable, tamper-evident audit trail suitable for cross-service
7//! and cross-node verification.
8//!
9//! ## Hash scheme
10//!
11//! Every event carries three hashes:
12//! - **`prev_hash`** — SHAKE-256 of the preceding event (chain link)
13//! - **`payload_hash`** — SHAKE-256 of the canonical JSON payload bytes
14//!   (content commitment; zeroed when payload is `None`)
15//! - **`hash`** — SHAKE-256 of `(sequence ‖ chain_id ‖ prev_hash ‖
16//!   source ‖ 0x00 ‖ kind ‖ 0x00 ‖ timestamp ‖ payload_hash)`
17//!
18//! Together these enable *two-way verification*: given an event you
19//! can verify the chain link backward *and* the payload content
20//! independently.
21//!
22//! # K0 Scope
23//! Local chain only: genesis, append, checkpoint.
24//!
25//! # K1+ Scope (not implemented)
26//! Global root chain, BridgeEvent anchoring, ruvector-raft consensus.
27
28use 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
45// ── ExoChain-specific RVF types ─────────────────────────────────────
46//
47// These were previously in rvf-types/rvf-wire but were removed upstream.
48// They are exochain-specific protocol types that belong with this module.
49
50/// Magic number for ExoChain headers inside RVF segment payloads.
51const EXOCHAIN_MAGIC: u32 = 0x4558_4F43; // "EXOC"
52
53/// 64-byte header embedded in the payload of an RVF segment for exochain events.
54///
55/// Layout (all fields little-endian):
56///   [0..4]   magic: u32        EXOCHAIN_MAGIC
57///   [4]      version: u8       protocol version (1)
58///   [5]      subtype: u8       0x40=Event, 0x41=Checkpoint, 0x42=Proof
59///   [6..8]   flags: u16        reserved
60///   [8..12]  chain_id: u32     chain identifier
61///   [12..16] _reserved: u32    must be 0
62///   [16..24] sequence: u64     event sequence number
63///   [24..32] timestamp_secs: u64  unix timestamp
64///   [32..64] prev_hash: [u8;32]  hash of previous event
65#[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
117/// Write an RVF segment containing an ExoChainHeader + CBOR payload.
118fn 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, // domain-specific segment type
125        &payload,
126        rvf_types::SegmentFlags::empty(),
127        segment_id,
128    )
129}
130
131/// Decode an RVF segment payload into ExoChainHeader + remaining CBOR bytes.
132fn 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/// A chain event -- one entry in the append-only log.
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct ChainEvent {
142    /// Sequence number (0 = genesis).
143    pub sequence: u64,
144    /// Chain ID (0 = local).
145    pub chain_id: u32,
146    /// Event timestamp.
147    pub timestamp: DateTime<Utc>,
148    /// SHAKE-256 hash of the previous event (zeroed for genesis).
149    pub prev_hash: [u8; 32],
150    /// SHAKE-256 hash of this event (covers all fields incl. payload).
151    pub hash: [u8; 32],
152    /// SHAKE-256 hash of the canonical payload bytes (zeroed when
153    /// payload is `None`). Enables independent content verification.
154    #[serde(default)]
155    pub payload_hash: [u8; 32],
156    /// Event source (e.g. "kernel", "service.cron", "cluster").
157    pub source: String,
158    /// Event kind (e.g. "boot", "service.start", "peer.join").
159    pub kind: String,
160    /// Optional payload (JSON).
161    #[serde(default, skip_serializing_if = "Option::is_none")]
162    pub payload: Option<serde_json::Value>,
163}
164
165/// A checkpoint snapshot of the chain state.
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct ChainCheckpoint {
168    /// Chain ID.
169    pub chain_id: u32,
170    /// Sequence number at checkpoint.
171    pub sequence: u64,
172    /// Hash of the last event at checkpoint.
173    pub last_hash: [u8; 32],
174    /// Timestamp of the checkpoint.
175    pub timestamp: DateTime<Utc>,
176    /// Number of events since last checkpoint.
177    pub events_since_last: u64,
178}
179
180/// Result of chain integrity verification.
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct ChainVerifyResult {
183    /// Whether the entire chain is valid.
184    pub valid: bool,
185    /// Number of events verified.
186    pub event_count: usize,
187    /// List of errors found (empty if valid).
188    pub errors: Vec<String>,
189    /// Ed25519 signature verification status.
190    /// `None` = no signature present, `Some(true)` = valid, `Some(false)` = invalid.
191    #[serde(default, skip_serializing_if = "Option::is_none")]
192    pub signature_verified: Option<bool>,
193}
194
195/// Compute the SHAKE-256 content hash of a payload.
196///
197/// Returns the 32-byte SHAKE-256 hash of the canonical JSON bytes,
198/// or all zeros if the payload is `None`.
199pub(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
209/// Compute the SHAKE-256 hash for a chain event.
210///
211/// This is the canonical hash function used for both event creation
212/// and integrity verification. The hash commits to **all** fields:
213///
214/// ```text
215/// SHAKE-256(
216///     sequence(8)  ‖  chain_id(4)  ‖  prev_hash(32)  ‖
217///     source  ‖  0x00  ‖  kind  ‖  0x00  ‖
218///     timestamp(8)  ‖  payload_hash(32)
219/// )
220/// ```
221///
222/// The null-byte separators between `source` and `kind` prevent
223/// domain collisions (e.g. "foo" + "bar.baz" vs "foo.bar" + "baz").
224pub(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); // separator
239    buf.extend_from_slice(kind.as_bytes());
240    buf.push(0x00); // separator
241    buf.extend_from_slice(&timestamp.timestamp().to_le_bytes());
242    buf.extend_from_slice(payload_hash);
243    shake256_256(&buf)
244}
245
246/// Witness type constants for kernel events.
247const WITNESS_PROVENANCE: u8 = 0x01;
248
249// ── Well-known chain event kinds (k3:D8, k2:D8) ────────────────────
250//
251// These constants define canonical `kind` strings for chain events
252// so that producers and consumers agree on the vocabulary.
253
254/// Capability revocation event (k3:D8 — informational revocation).
255///
256/// Revocation is recorded in the chain as data; enforcement is
257/// handled by the governance gate at evaluation time. This allows
258/// governance rules to decide whether revoked capabilities result
259/// in a hard block, a warning, or are allowed in specific contexts.
260pub const EVENT_KIND_CAPABILITY_REVOKED: &str = "capability.revoked";
261
262/// API contract registration event (k2:D8).
263///
264/// Emitted when a service registers or updates its API schema.
265/// The payload should include `service`, `version`, `schema_hash`.
266pub const EVENT_KIND_API_CONTRACT_REGISTERED: &str = "service.contract.register";
267
268/// Tool version deployment event.
269pub const EVENT_KIND_TOOL_DEPLOYED: &str = "tool.deploy";
270
271/// Tool version revocation event.
272pub const EVENT_KIND_TOOL_VERSION_REVOKED: &str = "tool.version.revoke";
273
274/// Sandbox sudo override event (k3:D12).
275///
276/// Emitted when a sudo override bypasses environment sandbox restrictions.
277/// The payload should include `agent_id`, `tool`, `path`, `reason`.
278pub const EVENT_KIND_SANDBOX_SUDO_OVERRIDE: &str = "sandbox.sudo.override";
279
280/// Tool signed event (k3:D9).
281///
282/// Emitted when a tool is registered with a verified cryptographic signature.
283/// The payload should include `tool_name`, `tool_hash`, `signer_id`.
284pub const EVENT_KIND_TOOL_SIGNED: &str = "tool.signed";
285
286/// Shell command execution event (k3:D10).
287///
288/// Emitted when a shell command is executed through the sandbox.
289/// The payload should include `command`, `exit_code`, `execution_time_ms`.
290pub const EVENT_KIND_SHELL_EXEC: &str = "shell.exec";
291
292/// Local chain state.
293struct 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 — one per event for cryptographic audit trail.
302    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    /// Restore from a saved set of events and optional witness entries.
320    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            &timestamp,
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        // Create a witness entry for this event.
374        self.witness_entries.push(WitnessEntry {
375            prev_hash: [0u8; 32], // linked at serialization time
376            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        // Auto-checkpoint
387        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/// CBOR payload structure for RVF segment persistence.
411///
412/// Contains the per-event fields that are not already covered by the
413/// ExoChainHeader (which stores sequence, chain_id, timestamp, prev_hash).
414#[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    /// Hex-encoded 32-byte payload hash.
421    payload_hash: String,
422    /// Hex-encoded 32-byte event hash.
423    hash: String,
424}
425
426/// Encode a 32-byte hash as a lowercase hex string.
427fn hex_hash(h: &[u8; 32]) -> String {
428    h.iter().map(|b| format!("{b:02x}")).collect()
429}
430
431/// Encode arbitrary bytes as a lowercase hex string.
432fn hex_encode(data: &[u8]) -> String {
433    data.iter().map(|b| format!("{b:02x}")).collect()
434}
435
436/// Decode a hex string into a byte vector.
437fn 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
450/// Parse a 64-char hex string back into a 32-byte array.
451fn 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
464/// Convert a single ASCII hex character to its nibble value.
465fn 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
474/// Thread-safe chain manager.
475///
476/// Wraps a local chain with mutex protection for concurrent access
477/// from multiple kernel subsystems. Optionally holds an Ed25519
478/// signing key for cryptographic chain signing.
479pub struct ChainManager {
480    inner: Mutex<LocalChain>,
481    /// Ed25519 signing key for RVF segment signing.
482    signing_key: Option<SigningKey>,
483    /// ML-DSA-65 signing key for post-quantum dual signing.
484    ml_dsa_key: Option<MlDsa65Key>,
485}
486
487impl ChainManager {
488    /// Create a new chain manager with genesis event.
489    pub fn new(chain_id: u32, checkpoint_interval: u64) -> Self {
490        let mut chain = LocalChain::new(chain_id, checkpoint_interval);
491        // Genesis event
492        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    /// Create with default settings.
507    pub fn default_local() -> Self {
508        Self::new(0, 1000)
509    }
510
511    /// Attach an Ed25519 signing key for RVF segment signing.
512    pub fn with_signing_key(mut self, key: SigningKey) -> Self {
513        self.signing_key = Some(key);
514        self
515    }
516
517    /// Get the verifying (public) key, if a signing key is set.
518    pub fn verifying_key(&self) -> Option<VerifyingKey> {
519        self.signing_key.as_ref().map(|k| k.verifying_key())
520    }
521
522    /// Set the signing key (mutable borrow — use with `Arc::get_mut()`
523    /// before sharing the manager across tasks).
524    pub fn set_signing_key(&mut self, key: SigningKey) {
525        self.signing_key = Some(key);
526    }
527
528    /// Whether this chain manager has a signing key attached.
529    pub fn has_signing_key(&self) -> bool {
530        self.signing_key.is_some()
531    }
532
533    /// Set the ML-DSA-65 key for post-quantum dual signing.
534    pub fn set_ml_dsa_key(&mut self, key: MlDsa65Key) {
535        self.ml_dsa_key = Some(key);
536    }
537
538    /// Whether dual signing (Ed25519 + ML-DSA-65) is enabled.
539    pub fn has_dual_signing(&self) -> bool {
540        self.signing_key.is_some() && self.ml_dsa_key.is_some()
541    }
542
543    /// Load an Ed25519 signing key from file, or generate and persist a new one.
544    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    /// Append an event to the chain.
575    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    /// Create a checkpoint.
586    pub fn checkpoint(&self) -> ChainCheckpoint {
587        let mut chain = self.inner.lock().unwrap();
588        chain.create_checkpoint()
589    }
590
591    /// Get the current chain length.
592    pub fn len(&self) -> usize {
593        self.inner.lock().unwrap().events.len()
594    }
595
596    /// Check if the chain is empty (should never be after genesis).
597    pub fn is_empty(&self) -> bool {
598        self.inner.lock().unwrap().events.is_empty()
599    }
600
601    /// Get the current sequence number.
602    pub fn sequence(&self) -> u64 {
603        self.inner.lock().unwrap().sequence
604    }
605
606    /// Get the last hash.
607    pub fn last_hash(&self) -> [u8; 32] {
608        self.inner.lock().unwrap().last_hash
609    }
610
611    /// Get the chain ID.
612    pub fn chain_id(&self) -> u32 {
613        self.inner.lock().unwrap().chain_id
614    }
615
616    /// Get recent events (last n, or all if n=0).
617    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    /// Return events with sequence strictly greater than `after`.
627    /// Used for incremental replication in K6.4 chain sync.
628    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    /// Get the current head sequence number (sequence of the last event).
639    /// Returns 0 when the chain is empty.
640    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    /// Get the current head hash (hash of the last event).
646    /// Returns the all-zero hash when the chain is empty.
647    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    /// Get all checkpoints.
653    pub fn checkpoints(&self) -> Vec<ChainCheckpoint> {
654        self.inner.lock().unwrap().checkpoints.clone()
655    }
656
657    /// Get the number of witness entries.
658    pub fn witness_count(&self) -> usize {
659        self.inner.lock().unwrap().witness_entries.len()
660    }
661
662    /// Serialize the witness chain and verify it.
663    ///
664    /// Returns `Ok(entry_count)` if the witness chain is valid, or
665    /// `Err` if verification fails.
666    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    /// Verify the integrity of the entire chain.
680    ///
681    /// Walks all events and verifies:
682    /// 1. Each event's `prev_hash` matches the prior event's `hash`
683    /// 2. Each event's `payload_hash` matches the recomputed payload hash
684    /// 3. Each event's `hash` matches the recomputed event hash
685    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            // 1. Verify prev_hash linkage
691            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            // 2. Verify payload_hash
706            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            // 3. Recompute and verify event hash
717            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    /// Save the chain to a file (line-delimited JSON).
745    ///
746    /// Writes all events as newline-delimited JSON to the given path.
747    /// Creates parent directories if they don't exist.
748    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    /// Load a chain from a file (line-delimited JSON).
773    ///
774    /// Reads events, verifies integrity, and restores state so that
775    /// new events continue from the last sequence number.
776    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        // Verify integrity of the loaded chain
806        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    /// Save the chain as a concatenation of RVF segments.
829    ///
830    /// Each event is serialized as an ExochainEvent segment (subtype 0x40)
831    /// containing a 64-byte ExoChainHeader + CBOR payload. A trailing
832    /// ExochainCheckpoint segment (subtype 0x41) records the final chain
833    /// state for external verification.
834    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            // Build the ExoChainHeader from event fields.
848            let exo_header = ExoChainHeader {
849                magic: EXOCHAIN_MAGIC,
850                version: 1,
851                subtype: 0x40, // ExochainEvent
852                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            // Serialize the remaining fields as CBOR.
861            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            // Write the full RVF segment (header + exo header + cbor + padding).
874            let segment = write_exochain_event(&exo_header, &cbor_bytes, event.sequence);
875            output.extend_from_slice(&segment);
876        }
877
878        // Write a trailing checkpoint segment (subtype 0x41).
879        let checkpoint_header = ExoChainHeader {
880            magic: EXOCHAIN_MAGIC,
881            version: 1,
882            subtype: 0x41, // ExochainCheckpoint
883            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        // Serialize and include the witness chain in the checkpoint.
892        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, // use next sequence as segment_id
913        );
914        output.extend_from_slice(&cp_segment);
915
916        // Sign the checkpoint segment if a signing key is available.
917        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            // Dual-sign with ML-DSA-65 if the key is available.
925            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    /// Load a chain from an RVF segment file.
950    ///
951    /// Reads concatenated RVF segments, validates each segment's content
952    /// hash, decodes ExoChainHeader + CBOR payload, and reconstructs the
953    /// chain events. Checkpoint segments (subtype 0x41) are skipped.
954    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            // Need at least a segment header.
965            if data.len() - offset < SEGMENT_HEADER_SIZE {
966                break;
967            }
968
969            // Try reading the next segment. If it fails, the remaining
970            // bytes may be a signature footer — break out of the loop.
971            let (seg_header, seg_payload) = match read_segment(&data[offset..]) {
972                Ok(result) => result,
973                Err(_) => break,
974            };
975
976            // Validate the content hash.
977            validate_segment(&seg_header, seg_payload)
978                .map_err(|e| format!("validate segment at offset {offset}: {e}"))?;
979
980            // Decode the ExoChainHeader + CBOR from the segment payload.
981            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                // ExochainEvent -- deserialize the CBOR payload.
988                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                // Checkpoint — extract witness chain if present.
1019                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            // subtype 0x42 (Proof) is skipped.
1040
1041            // Advance past the segment: header + payload padded to 64 bytes.
1042            let padded = calculate_padded_size(
1043                SEGMENT_HEADER_SIZE,
1044                seg_header.payload_length as usize,
1045            );
1046            offset += padded;
1047        }
1048
1049        // Check for trailing signature footer(s).
1050        // There may be one (Ed25519) or two (Ed25519 + ML-DSA-65) footers.
1051        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        // Verify integrity of the loaded chain.
1082        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    // ── Lineage tracking ─────────────────────────────────────────
1107    //
1108    // DNA-style provenance for agent spawn and resource derivation.
1109    // Uses rvf-crypto's lineage module to create verifiable derivation
1110    // records that link parent → child with hash verification.
1111
1112    /// Record a lineage derivation event in the chain.
1113    ///
1114    /// Creates a `LineageRecord` from the given parameters, serializes it,
1115    /// adds a lineage witness entry, and appends a `lineage.derivation`
1116    /// chain event with the full record in the payload.
1117    ///
1118    /// - `child_id`: UUID of the derived entity (agent, resource)
1119    /// - `parent_id`: UUID of the parent entity (zero for root)
1120    /// - `parent_hash`: hash of the parent's state at derivation time
1121    /// - `derivation_type`: how the child was produced
1122    /// - `mutation_count`: number of mutations/changes applied
1123    /// - `description`: human-readable description (max 47 chars)
1124    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        // Serialize the record and create a witness entry.
1148        let record_bytes = lineage_record_to_bytes(&record);
1149
1150        // Add a lineage witness entry to the witness chain.
1151        {
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    /// Extract lineage records from chain events and verify the chain.
1176    ///
1177    /// Returns `Ok(count)` if all lineage records form a valid chain,
1178    /// or `Err` describing the verification failure.
1179    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            // Use the event hash as the manifest hash for this identity.
1223            identities.push((fi, event.hash));
1224        }
1225
1226        if identities.is_empty() {
1227            return Ok(0);
1228        }
1229
1230        // Lineage chain verification requires root → leaf ordering.
1231        // Our records are already in append order, so roots come first.
1232        // verify_lineage_chain expects consecutive parent→child pairs,
1233        // but our records may be from different lineage trees. Verify
1234        // each parent→child pair independently.
1235        let count = identities.len();
1236        for (fi, _hash) in &identities {
1237            if fi.is_root() {
1238                continue;
1239            }
1240            // Find the parent in identities.
1241            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    // ── Witness bundle integration ─────────────────────────────
1257    //
1258    // RVF witness bundles are the atomic proof unit for agent task
1259    // execution. These methods bridge the gap between the kernel's
1260    // event chain and the rvf-runtime WitnessBuilder.
1261
1262    /// Record a completed witness bundle as a chain event.
1263    ///
1264    /// Takes the raw bundle bytes and parsed header produced by
1265    /// `WitnessBuilder::build()`, stores them as a `witness.bundle`
1266    /// chain event with the bundle hex-encoded in the payload.
1267    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    /// Aggregate witness bundles from recent chain events into a scorecard.
1291    ///
1292    /// Scans the last `n` events (0 = all) for `witness.bundle` events,
1293    /// parses each bundle, and produces an aggregate `Scorecard`.
1294    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    /// Find the most recent tree root hash recorded in chain events.
1329    ///
1330    /// Scans backwards through the event log looking for payloads
1331    /// that contain `tree_root_hash` (shutdown, boot.ready, boot.manifest)
1332    /// or `root_hash` on tree-sourced events (tree.checkpoint, checkpoint).
1333    ///
1334    /// Returns the hash as a hex string if found.
1335    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            // Shutdown/boot events record "tree_root_hash" directly.
1342            if let Some(hash) = payload.get("tree_root_hash").and_then(|v| v.as_str()) {
1343                return Some(hash.to_string());
1344            }
1345            // Tree events record "root_hash".
1346            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    /// Get a status summary.
1357    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    /// Verify the Ed25519 signature on an RVF chain file.
1370    ///
1371    /// Reads the file, locates the checkpoint segment and trailing
1372    /// signature footer, and verifies the signature against the
1373    /// provided public key.
1374    ///
1375    /// Returns `Ok(true)` if signature is valid, `Ok(false)` if
1376    /// invalid, and `Err` if the file has no signature or can't be read.
1377    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        // Walk all segments to find the last one (checkpoint).
1386        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        // Remaining bytes should be the signature footer.
1403        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        // Re-read the checkpoint segment for verification.
1410        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    /// Verify dual (Ed25519 + ML-DSA-65) signatures on an RVF chain file.
1422    ///
1423    /// Returns `Ok(true)` if both signatures are valid, `Ok(false)` if
1424    /// either is invalid, and `Err` if the file has no dual signature.
1425    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    // ── Dual signing for cross-node chain events ───────────────
1475
1476    /// Sign data with both Ed25519 and ML-DSA-65 (if configured).
1477    ///
1478    /// Returns `Some(DualSignature)` when at least the Ed25519 key is
1479    /// present. The ML-DSA-65 half is populated only when the ML-DSA key
1480    /// has been set via [`set_ml_dsa_key`].
1481    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            // Use SHAKE-256 HMAC-like construction matching rvf-crypto placeholder.
1490            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    /// Verify a dual signature against the given data and public keys.
1500    ///
1501    /// Ed25519 verification is mandatory. ML-DSA-65 verification is
1502    /// performed only when both the signature and verification key are
1503    /// present. Returns `false` if any present signature is invalid.
1504    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        // Ed25519 is mandatory.
1513        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        // ML-DSA-65 when both signature and key are present.
1526        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
1536// ── Raw ML-DSA-65 placeholder signing for arbitrary data ──────────
1537//
1538// These mirror the HMAC-SHA3-256 placeholder in rvf-crypto's dual_sign
1539// module but operate on raw byte slices instead of RVF segments.
1540
1541/// ML-DSA-65 placeholder signature length (FIPS 204).
1542const ML_DSA_RAW_SIG_LEN: usize = 3309;
1543
1544/// Sign arbitrary data with the ML-DSA-65 placeholder (HMAC-SHAKE-256).
1545fn ml_dsa_sign_raw(key: &MlDsa65Key, data: &[u8]) -> Vec<u8> {
1546    // Extract 32-byte key material via verifying_key round-trip.
1547    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
1568/// Verify an ML-DSA-65 placeholder signature on arbitrary data.
1569fn 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
1591/// Extract the 32-byte key material from an `MlDsa65VerifyKey`.
1592///
1593/// Since `MlDsa65VerifyKey` does not expose its inner bytes directly,
1594/// we regenerate via the same seed path used in generate(). In
1595/// placeholder mode the signing key and verify key share the same
1596/// 32-byte SHAKE-256 digest, so `verifying_key()` round-trips cleanly.
1597fn ml_dsa_vk_bytes(vk: &MlDsa65VerifyKey) -> [u8; 32] {
1598    // MlDsa65VerifyKey is Clone; generate a fresh one from a known seed
1599    // and compare — but we cannot peek inside. Instead we use a simple
1600    // workaround: the test-time key generation uses `generate(seed)` which
1601    // produces `key = SHAKE-256(seed)`. The verify key holds the same bytes.
1602    // Since the struct is opaque, we sign a sentinel and derive the key
1603    // bytes from the output (the first 32 bytes of the HMAC chain are
1604    // `SHAKE-256(key || sentinel || key)` which IS deterministic).
1605    //
1606    // However, the simplest correct approach: we know in placeholder mode
1607    // signing_key bytes == verify_key bytes. The MlDsa65Key::generate
1608    // returns both with the same 32-byte value. So we reconstruct via
1609    // a zero-length sign and extract the block. This works because the
1610    // HMAC construction in rvf-crypto uses the key bytes directly.
1611    //
1612    // For a clean API, we add a compile-time assertion that verifying_key
1613    // is 32 bytes and access it through the known memory layout.
1614    //
1615    // In practice, both MlDsa65Key and MlDsa65VerifyKey wrap `key: [u8; 32]`.
1616    // We use unsafe transmute in a controlled, size-asserted way.
1617    assert_eq!(
1618        std::mem::size_of::<MlDsa65VerifyKey>(),
1619        32,
1620        "MlDsa65VerifyKey must be exactly 32 bytes"
1621    );
1622    // SAFETY: MlDsa65VerifyKey is a repr(Rust) struct containing only
1623    // `key: [u8; 32]`. We assert the size matches before transmuting.
1624    unsafe { std::mem::transmute_copy(vk) }
1625}
1626
1627// ── Cross-node dual signature types ───────────────────────────────
1628
1629/// Configuration for dual Ed25519 + ML-DSA-65 signing.
1630pub struct DualSigningConfig {
1631    /// Ed25519 signing key.
1632    pub ed25519_key: SigningKey,
1633    /// ML-DSA-65 signing key (if available).
1634    pub ml_dsa_key: Option<MlDsa65Key>,
1635}
1636
1637/// A dual signature (Ed25519 + optional ML-DSA-65).
1638#[derive(Debug, Clone, Serialize, Deserialize)]
1639pub struct DualSignature {
1640    /// Ed25519 signature (64 bytes).
1641    pub ed25519: Vec<u8>,
1642    /// ML-DSA-65 signature (optional, ~3309 bytes).
1643    pub ml_dsa65: Option<Vec<u8>>,
1644}
1645
1646/// Chain status summary.
1647#[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// ---------------------------------------------------------------------------
1669// K4 G1: ChainAnchor trait — external anchoring abstraction (K2 C7)
1670// ---------------------------------------------------------------------------
1671
1672/// Receipt returned by a successful [`ChainAnchor::anchor`] call.
1673#[derive(Debug, Clone, Serialize, Deserialize)]
1674pub struct AnchorReceipt {
1675    /// The hash that was anchored.
1676    pub hash: [u8; 32],
1677    /// Backend-specific transaction/receipt ID.
1678    pub tx_id: String,
1679    /// Timestamp of the anchoring operation.
1680    pub anchored_at: DateTime<Utc>,
1681}
1682
1683/// Trait for anchoring chain state to an external ledger or store.
1684///
1685/// Implementations might target Bitcoin (via OpenTimestamps), Ethereum,
1686/// a ruvector root chain, or simply a mock for testing.
1687pub trait ChainAnchor: Send + Sync {
1688    /// Anchor the given hash to the external backend.
1689    fn anchor(&self, hash: &[u8; 32]) -> Result<AnchorReceipt, String>;
1690
1691    /// Verify a previously anchored hash against its receipt.
1692    fn verify(&self, receipt: &AnchorReceipt) -> Result<bool, String>;
1693
1694    /// Return the backend name for display/logging.
1695    fn backend_name(&self) -> &str;
1696}
1697
1698/// A mock anchor that always succeeds (for testing).
1699pub 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
1719// ── ChainLoggable trait ─────────────────────────────────────────────
1720
1721/// Trait for types that can be logged to the ExoChain audit trail.
1722///
1723/// Implementors define how they map to a chain event kind string and
1724/// a JSON payload. The [`ChainManager::append_loggable`] convenience
1725/// method uses these to append an event without the caller needing to
1726/// hand-craft the source/kind/payload triple.
1727pub trait ChainLoggable {
1728    /// The source subsystem (e.g. "supervisor", "governance", "ipc").
1729    fn chain_event_source(&self) -> &str;
1730
1731    /// The event kind string (e.g. "agent.restart", "governance.deny").
1732    fn chain_event_kind(&self) -> &str;
1733
1734    /// Build the JSON payload for the chain event.
1735    fn chain_event_payload(&self) -> serde_json::Value;
1736}
1737
1738impl ChainManager {
1739    /// Append an event from any [`ChainLoggable`] implementor.
1740    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
1749// ── ChainLoggable implementations ───────────────────────────────────
1750
1751/// A restart event suitable for chain logging.
1752///
1753/// Created by the supervisor after successfully restarting an agent.
1754pub struct RestartEvent {
1755    /// The agent identifier that was restarted.
1756    pub agent_id: String,
1757    /// PID of the process that crashed / was stopped.
1758    pub old_pid: u64,
1759    /// PID of the newly spawned replacement process.
1760    pub new_pid: u64,
1761    /// Exit code that triggered the restart.
1762    pub exit_code: i32,
1763    /// Restart strategy that was applied.
1764    pub strategy: String,
1765    /// Backoff delay in milliseconds before the restart was attempted.
1766    pub backoff_ms: u64,
1767    /// Timestamp of the restart.
1768    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
1793/// A governance decision event suitable for chain logging.
1794///
1795/// Captures the result of `GovernanceEngine::evaluate` for audit.
1796pub struct GovernanceDecisionEvent {
1797    /// Agent that made the request.
1798    pub agent_id: String,
1799    /// Action that was evaluated.
1800    pub action: String,
1801    /// The governance decision outcome.
1802    pub decision: String,
1803    /// Effect vector magnitude.
1804    pub effect_magnitude: f64,
1805    /// Whether the risk threshold was exceeded.
1806    pub threshold_exceeded: bool,
1807    /// Rules that were evaluated.
1808    pub evaluated_rules: Vec<String>,
1809    /// Timestamp of the evaluation.
1810    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
1841/// An IPC dead-letter event suitable for chain logging.
1842///
1843/// Captures when a message is routed to the dead letter queue.
1844pub struct IpcDeadLetterEvent {
1845    /// Original message ID.
1846    pub message_id: String,
1847    /// Sender PID.
1848    pub from_pid: u64,
1849    /// Target description (formatted from MessageTarget).
1850    pub target: String,
1851    /// Payload type name.
1852    pub payload_type: String,
1853    /// Reason delivery failed.
1854    pub reason: String,
1855    /// Timestamp of the dead-lettering.
1856    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); // genesis consumed seq 0
1889        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        // tail_from(0) should skip genesis (seq 0) and return seq 1, 2
1926        // But actually: genesis is seq 0 so tail_from(0) returns events
1927        // with sequence > 0 i.e. the two appended events.
1928        let all = cm.tail_from(0);
1929        // We have genesis(0), event.one(1), event.two(2) — 2 events after 0
1930        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        // ChainManager::new always creates a genesis event, so we
1957        // verify head_sequence returns the genesis sequence (0).
1958        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        // genesis=0, a=1, b=2
1968        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); // checkpoint every 5 events
1982        // Genesis is event 0 (1 event since checkpoint)
1983        for i in 0..4 {
1984            cm.append("test", &format!("event.{i}"), None);
1985        }
1986        // 5 total events (genesis + 4) -> should auto-checkpoint
1987        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); // genesis + 3
2010        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        // Verify restored chain integrity
2035        let result = restored.verify_integrity();
2036        assert!(result.valid);
2037
2038        // New events continue from restored state
2039        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        // Clean up
2044        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); // genesis + 3
2070    }
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        // Verify restored chain integrity.
2098        let result = restored.verify_integrity();
2099        assert!(result.valid, "integrity errors: {:?}", result.errors);
2100        assert_eq!(result.event_count, original_len);
2101
2102        // New events continue from restored state.
2103        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        // Clean up.
2108        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        // Corrupt a byte in the first segment's payload area.
2121        let mut data = std::fs::read(&path).unwrap();
2122        // The payload starts at SEGMENT_HEADER_SIZE (64). Flip a byte
2123        // inside the ExoChainHeader portion of the payload.
2124        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        // Clean up.
2133        let _ = std::fs::remove_dir_all(&dir);
2134    }
2135
2136    #[test]
2137    fn rvf_migration_from_json() {
2138        // Create a chain via the normal API.
2139        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        // Save as JSON, load as JSON.
2153        cm.save_to_file(&json_path).unwrap();
2154        let from_json = ChainManager::load_from_file(&json_path, 1000).unwrap();
2155
2156        // Save the JSON-loaded chain as RVF.
2157        from_json.save_to_rvf(&rvf_path).unwrap();
2158
2159        // Load from RVF and compare.
2160        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        // Compare event-by-event.
2168        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        // Verify integrity of the RVF-loaded chain.
2183        let result = from_rvf.verify_integrity();
2184        assert!(result.valid, "integrity errors: {:?}", result.errors);
2185
2186        // Clean up.
2187        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        // File should be larger than unsigned (72 extra bytes for Ed25519 footer).
2212        let _file_size = std::fs::metadata(&path).unwrap().len();
2213
2214        // Load the signed file (should work even without a key).
2215        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        // Verify the signature.
2224        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        // Verify with wrong key fails.
2229        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        // Clean up.
2235        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; // Ed25519: 2 + 2 + 64 + 4
2253
2254        // Walk segments to find the checkpoint segment start.
2255        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        // Tamper with the first byte of the checkpoint segment's payload
2272        // (the ExoChainHeader magic). This is covered by the signature.
2273        data[last_seg_start + SEGMENT_HEADER_SIZE] ^= 0xFF;
2274        std::fs::write(&path, &data).unwrap();
2275
2276        // Signature verification should fail (checkpoint payload was tampered).
2277        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(_) => {} // Also acceptable — tampered segment can't be parsed
2281        }
2282
2283        let _ = std::fs::remove_dir_all(&dir);
2284    }
2285
2286    #[test]
2287    fn unsigned_rvf_loads_successfully() {
2288        // An unsigned chain should still load fine (no signing key).
2289        let cm = ChainManager::new(0, 1000); // no signing key
2290        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        // verify_rvf_signature should error (no footer present).
2304        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        // First call: generates a new key.
2318        let key1 = ChainManager::load_or_create_key(&key_path).unwrap();
2319        assert!(key_path.exists());
2320
2321        // Second call: loads the same key.
2322        let key2 = ChainManager::load_or_create_key(&key_path).unwrap();
2323        assert_eq!(key1.to_bytes(), key2.to_bytes());
2324
2325        // Clean up.
2326        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); // genesis
2333        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        // Verify the witness chain is internally consistent.
2339        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); // genesis + 3
2352
2353        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        // Load and verify witness chain was restored.
2358        let restored = ChainManager::load_from_rvf(&path, 1000).unwrap();
2359        assert_eq!(restored.witness_count(), original_witness_count);
2360
2361        // Verify the restored witness chain is valid.
2362        let count = restored.verify_witness().unwrap();
2363        assert_eq!(count, original_witness_count);
2364
2365        // Verify that witness action_hashes match event hashes.
2366        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        // Clean up.
2377        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); // genesis + 1
2391
2392        // Append new events — witness chain should grow.
2393        restored.append("test", "after.restore", None);
2394        assert_eq!(restored.witness_count(), 3);
2395
2396        // Verify the extended witness chain.
2397        let count = restored.verify_witness().unwrap();
2398        assert_eq!(count, 3);
2399
2400        // Clean up.
2401        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        // Verify payload contains expected fields.
2424        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        // Record 3 witness bundles: 2 solved, 1 failed.
2439        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        // Scorecard should aggregate this bundle.
2519        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],           // child_id
2533            [0x00; 16],           // parent_id (root)
2534            [0x00; 32],           // parent_hash (root)
2535            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        // Root agent.
2557        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        // Child agent derived from root.
2567        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        // Verify lineage chain.
2581        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        // One extra witness entry: the lineage witness + the chain append witness.
2602        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        // Extract and decode the record_hex from the payload.
2627        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); // LINEAGE_RECORD_SIZE
2631
2632        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        // No tree hash yet.
2649        assert!(cm.last_tree_root_hash().is_none());
2650
2651        // Simulate a shutdown event with tree_root_hash.
2652        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        // tree.checkpoint event uses "root_hash" key.
2670        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        // Older event.
2688        cm.append(
2689            "kernel",
2690            "boot.ready",
2691            Some(serde_json::json!({ "tree_root_hash": "old_hash" })),
2692        );
2693
2694        // More recent event.
2695        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        // Event with root_hash but wrong source/kind — should be ignored.
2709        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    // --- K4 G1: ChainAnchor tests ---
2718
2719    #[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        // Ed25519 only
2748        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        // Add ML-DSA key
2754        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        // Load the dual-signed file (should work without keys).
2785        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        // Verify Ed25519 signature alone.
2794        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        // Verify dual signatures.
2799        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        // Wrong ML-DSA key should fail dual verification.
2803        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        // Read file and verify it has two footers.
2828        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        // First footer: Ed25519
2843        let ed_footer = decode_signature_footer(&data[offset..]).unwrap();
2844        assert_eq!(ed_footer.sig_algo, 0); // Ed25519
2845        assert_eq!(ed_footer.sig_length, 64);
2846
2847        // Second footer: ML-DSA-65
2848        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); // ML-DSA-65
2851        assert_eq!(ml_footer.sig_length, 3309);
2852
2853        // Verify both independently.
2854        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        // This test demonstrates the "cryptographic filesystem" gate:
2864        // chain entries are hash-linked (SHAKE-256) and form a tamper-evident
2865        // append-only log with retrieval — proving cryptographic filesystem semantics.
2866
2867        let cm = ChainManager::new(0, 1000);
2868
2869        // Create a filesystem-style entry with content metadata
2870        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        // Verify hash linkage (cryptographic integrity)
2881        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        // Retrieve entry via tail_from (sequence > genesis)
2892        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        // Verify chain integrity covers this entry
2905        let result = cm.verify_integrity();
2906        assert!(result.valid, "chain integrity must hold after fs entry");
2907    }
2908
2909    // ── Cross-node dual signature tests ──────────────────────────
2910
2911    #[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        // No ML-DSA key set — dual_sign should still succeed with Ed25519 only.
2919
2920        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        // Tampered data should fail Ed25519 verification.
3002        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        // Tampered ML-DSA-65 signature should fail.
3009        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    // ── Sprint 09a: serde roundtrip tests for chain types ────────
3020
3021    #[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    // ── ChainLoggable trait tests ────────────────────────────────
3160
3161    #[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        // Test other decision kinds map to correct event kinds
3217        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}