Skip to main content

clawft_kernel/
tree_manager.rs

1//! Unified tree-chain facade for the exo-resource-tree subsystem.
2//!
3//! [`TreeManager`] holds a [`ResourceTree`], [`MutationLog`], and
4//! [`ChainManager`] together, ensuring every tree mutation produces
5//! both a mutation event and a chain event atomically.
6//!
7//! # K0 Scope
8//! Bootstrap, insert, remove, update_meta, checkpoint, register_service.
9//!
10//! # K1 Scope
11//! Checkpoint persistence to disk, permission-gated mutations.
12
13use std::sync::{Arc, Mutex};
14
15use chrono::Utc;
16use serde::{Deserialize, Serialize};
17use tracing::debug;
18
19use exo_resource_tree::{
20    MerkleDiff, MerkleDiffKind, MerkleInclusionProof, MutationEvent, MutationLog, NodeScoring,
21    ResourceId, ResourceKind, ResourceTree,
22};
23
24use crate::capability::AgentCapabilities;
25use crate::chain::ChainManager;
26use crate::process::Pid;
27use crate::wasm_runner::{BuiltinToolSpec, ToolVersion, compute_module_hash};
28
29/// Statistics snapshot from the tree manager.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct TreeStats {
32    /// Number of nodes in the resource tree.
33    pub node_count: usize,
34    /// Number of mutation events recorded.
35    pub mutation_count: usize,
36    /// Hex-encoded root hash of the tree.
37    pub root_hash: String,
38}
39
40/// Serializable snapshot of the full tree state for cross-node sync.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct TreeSnapshot {
43    /// Hex-encoded Merkle root hash at snapshot time.
44    pub root_hash: String,
45    /// Number of nodes in the tree.
46    pub node_count: usize,
47    /// Serialized tree state (all nodes and their metadata).
48    pub nodes: Vec<TreeNodeSnapshot>,
49    /// Timestamp when snapshot was taken.
50    pub taken_at: chrono::DateTime<chrono::Utc>,
51}
52
53/// Snapshot of a single tree node.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct TreeNodeSnapshot {
56    /// Resource path (e.g., "/kernel/services/health").
57    pub path: String,
58    /// Resource kind.
59    pub kind: String,
60    /// Metadata key-value pairs.
61    pub metadata: std::collections::HashMap<String, String>,
62    /// Hex-encoded node hash.
63    pub hash: String,
64}
65
66/// Unified facade over ResourceTree + MutationLog + ChainManager.
67///
68/// Every mutating operation on the tree also:
69/// 1. Appends a [`MutationEvent`] to the mutation log
70/// 2. Appends a [`ChainEvent`](crate::chain::ChainEvent) to the chain
71/// 3. Stores `chain_seq` metadata on the affected tree node
72pub struct TreeManager {
73    tree: Mutex<ResourceTree>,
74    mutation_log: Mutex<MutationLog>,
75    chain: Arc<ChainManager>,
76    /// Optional Ed25519 signing key for mutation signatures.
77    #[cfg(feature = "exochain")]
78    signing_key: Option<ed25519_dalek::SigningKey>,
79}
80
81/// `(operation, path, timestamp, signature)` extracted from a mutation event.
82#[cfg(feature = "exochain")]
83pub type MutationSignParts = (String, String, chrono::DateTime<Utc>, Option<Vec<u8>>);
84
85impl TreeManager {
86    /// Create a new TreeManager with an empty tree and mutation log.
87    pub fn new(chain: Arc<ChainManager>) -> Self {
88        Self {
89            tree: Mutex::new(ResourceTree::new()),
90            mutation_log: Mutex::new(MutationLog::new()),
91            chain,
92            #[cfg(feature = "exochain")]
93            signing_key: None,
94        }
95    }
96
97    /// Set the Ed25519 signing key for signing tree mutations.
98    /// When set, all mutations will have their signature field populated.
99    #[cfg(feature = "exochain")]
100    pub fn set_signing_key(&mut self, key: ed25519_dalek::SigningKey) {
101        self.signing_key = Some(key);
102    }
103
104    /// Sign arbitrary bytes with the configured signing key, if present.
105    /// Returns `None` when no key is configured.
106    #[cfg(feature = "exochain")]
107    fn sign_bytes(&self, data: &[u8]) -> Option<Vec<u8>> {
108        use ed25519_dalek::Signer;
109        if crate::chain::signs_project_domain(data) {
110            return None;
111        }
112        self.signing_key
113            .as_ref()
114            .map(|k| k.sign(data).to_bytes().to_vec())
115    }
116
117    /// Build the canonical bytes for a mutation signature.
118    ///
119    /// Format: `"<operation>|<path>|<timestamp_rfc3339>"` encoded as UTF-8.
120    #[cfg(feature = "exochain")]
121    pub fn mutation_canonical_bytes(
122        operation: &str,
123        path: &str,
124        timestamp: &chrono::DateTime<Utc>,
125    ) -> Vec<u8> {
126        format!("{operation}|{path}|{}", timestamp.to_rfc3339()).into_bytes()
127    }
128
129    /// Build the canonical bytes for a mutation signature.
130    ///
131    /// Format: `"<operation>|<path>|<timestamp_rfc3339>"` encoded as UTF-8.
132    #[cfg(feature = "exochain")]
133    fn mutation_signature(
134        &self,
135        operation: &str,
136        path: &str,
137        timestamp: &chrono::DateTime<Utc>,
138    ) -> Option<Vec<u8>> {
139        let canonical = Self::mutation_canonical_bytes(operation, path, timestamp);
140        self.sign_bytes(&canonical)
141    }
142
143    /// Extract (operation, path, timestamp, signature) from a mutation event.
144    #[cfg(feature = "exochain")]
145    pub fn mutation_sign_parts(
146        event: &MutationEvent,
147    ) -> Option<MutationSignParts> {
148        match event {
149            MutationEvent::Create {
150                id,
151                timestamp,
152                signature,
153                ..
154            } => Some((
155                "create".into(),
156                id.to_string(),
157                *timestamp,
158                signature.clone(),
159            )),
160            MutationEvent::Remove {
161                id,
162                timestamp,
163                signature,
164            } => Some((
165                "remove".into(),
166                id.to_string(),
167                *timestamp,
168                signature.clone(),
169            )),
170            MutationEvent::UpdateMeta {
171                id,
172                timestamp,
173                signature,
174                ..
175            } => Some((
176                "update_meta".into(),
177                id.to_string(),
178                *timestamp,
179                signature.clone(),
180            )),
181            MutationEvent::Move {
182                id,
183                timestamp,
184                signature,
185                ..
186            } => Some((
187                "move".into(),
188                id.to_string(),
189                *timestamp,
190                signature.clone(),
191            )),
192            MutationEvent::UpdateScoring {
193                id,
194                timestamp,
195                signature,
196                ..
197            } => Some((
198                "update_scoring".into(),
199                id.to_string(),
200                *timestamp,
201                signature.clone(),
202            )),
203            _ => None,
204        }
205    }
206
207    /// Verify an Ed25519 signature on a mutation event (WEFT-144).
208    ///
209    /// Returns `Ok(())` when the signature is present and valid under
210    /// `pubkey`. Errors when the signature is missing, malformed, or
211    /// does not verify.
212    #[cfg(feature = "exochain")]
213    pub fn verify_mutation_signature(
214        event: &MutationEvent,
215        pubkey: &ed25519_dalek::VerifyingKey,
216    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
217        use ed25519_dalek::{Signature, Verifier};
218
219        let (op, path, ts, sig_opt) = Self::mutation_sign_parts(event)
220            .ok_or("unsupported mutation variant for signature verification")?;
221        let sig_bytes = sig_opt.ok_or("mutation event has no signature")?;
222        if sig_bytes.len() != 64 {
223            return Err(format!(
224                "mutation signature length {}, expected 64",
225                sig_bytes.len()
226            )
227            .into());
228        }
229        let mut arr = [0u8; 64];
230        arr.copy_from_slice(&sig_bytes);
231        let sig = Signature::from_bytes(&arr);
232        let canonical = Self::mutation_canonical_bytes(&op, &path, &ts);
233        pubkey
234            .verify(&canonical, &sig)
235            .map_err(|e| format!("mutation signature verify failed: {e}").into())
236    }
237
238    /// Bootstrap the tree with well-known WeftOS namespaces.
239    ///
240    /// Creates the standard namespace hierarchy (`/kernel`, `/kernel/services`,
241    /// etc.), logs a MutationEvent::Create for each bootstrapped node, and
242    /// appends a `tree.bootstrap` chain event with node count and root hash.
243    pub fn bootstrap(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
244        let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
245        exo_resource_tree::bootstrap_fresh(&mut tree)?;
246
247        // Log mutation events for each bootstrapped node (skip root, it's pre-existing)
248        let mut log = self
249            .mutation_log
250            .lock()
251            .map_err(|e| format!("log lock: {e}"))?;
252        let bootstrapped_paths = [
253            "/kernel",
254            "/kernel/services",
255            "/kernel/processes",
256            "/kernel/agents",
257            "/network",
258            "/network/peers",
259            "/apps",
260            "/environments",
261        ];
262        for path in &bootstrapped_paths {
263            let rid = ResourceId::new(*path);
264            let kind = ResourceKind::Namespace;
265            let parent = rid.parent().unwrap_or_else(ResourceId::root);
266            let now = Utc::now();
267            #[cfg(feature = "exochain")]
268            let sig = self.mutation_signature("create", path, &now);
269            #[cfg(not(feature = "exochain"))]
270            let sig = None;
271            log.append(MutationEvent::Create {
272                id: rid,
273                kind,
274                parent,
275                timestamp: now,
276                signature: sig,
277            });
278        }
279
280        // Chain event
281        let hash_hex = hex_hash(&tree.root_hash());
282        self.chain.append(
283            "tree",
284            "bootstrap",
285            Some(serde_json::json!({
286                "node_count": tree.len(),
287                "root_hash": hash_hex,
288            })),
289        );
290
291        debug!(nodes = tree.len(), "tree bootstrapped with chain event");
292        Ok(())
293    }
294
295    /// Insert a new resource node into the tree.
296    ///
297    /// Creates the tree node, appends a MutationEvent::Create, appends a
298    /// `tree.insert` chain event, and stores the chain sequence number as
299    /// metadata on the node for two-way traceability.
300    pub fn insert(
301        &self,
302        id: ResourceId,
303        kind: ResourceKind,
304        parent: ResourceId,
305    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
306        let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
307        tree.insert(id.clone(), kind.clone(), parent.clone())?;
308
309        // Chain event (before recompute so we capture pre-mutation state reference)
310        let chain_event = self.chain.append(
311            "tree",
312            "insert",
313            Some(serde_json::json!({
314                "path": id.to_string(),
315                "kind": format!("{kind:?}"),
316                "parent": parent.to_string(),
317            })),
318        );
319
320        // Store chain_seq metadata on the node for traceability
321        if let Some(node) = tree.get_mut(&id) {
322            node.metadata.insert(
323                "chain_seq".to_string(),
324                serde_json::json!(chain_event.sequence),
325            );
326        }
327
328        // Incremental Merkle update: O(depth) path recompute (WEFT-145)
329        tree.recompute_path(&id);
330
331        // Mutation log
332        let now = Utc::now();
333        #[cfg(feature = "exochain")]
334        let sig = self.mutation_signature("create", &id.to_string(), &now);
335        #[cfg(not(feature = "exochain"))]
336        let sig = None;
337        let mut log = self
338            .mutation_log
339            .lock()
340            .map_err(|e| format!("log lock: {e}"))?;
341        log.append(MutationEvent::Create {
342            id,
343            kind,
344            parent,
345            timestamp: now,
346            signature: sig,
347        });
348
349        Ok(())
350    }
351
352    /// Remove a leaf node from the tree.
353    ///
354    /// Removes the node, appends a MutationEvent::Remove and a
355    /// `tree.remove` chain event.
356    pub fn remove(&self, id: ResourceId) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
357        let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
358        let removed = tree.remove(id.clone())?;
359        // Recompute from former parent upward (node is gone) — O(depth)
360        if let Some(parent) = removed.parent {
361            tree.recompute_path(&parent);
362        }
363
364        self.chain.append(
365            "tree",
366            "remove",
367            Some(serde_json::json!({
368                "path": id.to_string(),
369            })),
370        );
371
372        let now = Utc::now();
373        #[cfg(feature = "exochain")]
374        let sig = self.mutation_signature("remove", &id.to_string(), &now);
375        #[cfg(not(feature = "exochain"))]
376        let sig = None;
377        let mut log = self
378            .mutation_log
379            .lock()
380            .map_err(|e| format!("log lock: {e}"))?;
381        log.append(MutationEvent::Remove {
382            id,
383            timestamp: now,
384            signature: sig,
385        });
386
387        Ok(())
388    }
389
390    /// Update metadata on a resource node.
391    ///
392    /// Sets the key-value pair on the node, appends a MutationEvent::UpdateMeta
393    /// and a `tree.update_meta` chain event.
394    pub fn update_meta(
395        &self,
396        id: &ResourceId,
397        key: &str,
398        value: serde_json::Value,
399    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
400        let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
401        let node = tree
402            .get_mut(id)
403            .ok_or_else(|| format!("node not found: {id}"))?;
404        node.metadata.insert(key.to_string(), value.clone());
405        node.updated_at = Utc::now();
406        // Incremental Merkle update: O(depth) path recompute (WEFT-145)
407        tree.recompute_path(id);
408
409        self.chain.append(
410            "tree",
411            "update_meta",
412            Some(serde_json::json!({
413                "path": id.to_string(),
414                "key": key,
415            })),
416        );
417
418        let now = Utc::now();
419        #[cfg(feature = "exochain")]
420        let sig = self.mutation_signature("update_meta", &id.to_string(), &now);
421        #[cfg(not(feature = "exochain"))]
422        let sig = None;
423        let mut log = self
424            .mutation_log
425            .lock()
426            .map_err(|e| format!("log lock: {e}"))?;
427        log.append(MutationEvent::UpdateMeta {
428            id: id.clone(),
429            key: key.to_string(),
430            value: Some(value),
431            timestamp: now,
432            signature: sig,
433        });
434
435        Ok(())
436    }
437
438    /// Register a service in the tree, creating `/kernel/services/{name}`.
439    ///
440    /// This is the unified path for service registration: it inserts the tree
441    /// node AND creates the chain event atomically.
442    pub fn register_service(
443        &self,
444        name: &str,
445    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
446        let service_id = ResourceId::new(format!("/kernel/services/{name}"));
447        let parent = ResourceId::new("/kernel/services");
448        self.insert(service_id, ResourceKind::Service, parent)
449    }
450
451    /// Register a service with a manifest chain event.
452    ///
453    /// Inserts the tree node and emits an additional `service.manifest`
454    /// chain event with structured metadata (name, type, tree path,
455    /// registration time). This produces an RVF-auditable registration
456    /// when the chain is persisted as RVF segments.
457    pub fn register_service_with_manifest(
458        &self,
459        name: &str,
460        service_type: &str,
461    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
462        self.register_service(name)?;
463
464        self.chain.append(
465            "service",
466            "service.manifest",
467            Some(serde_json::json!({
468                "name": name,
469                "service_type": service_type,
470                "tree_path": format!("/kernel/services/{name}"),
471                "registered_at": Utc::now().to_rfc3339(),
472            })),
473        );
474
475        Ok(())
476    }
477
478    /// Register an agent in the tree, creating `/kernel/agents/{agent_id}`.
479    ///
480    /// Creates the tree node with kind `Agent`, sets metadata (pid, state,
481    /// spawn_time), and emits an `agent.spawn` chain event.
482    pub fn register_agent(
483        &self,
484        agent_id: &str,
485        pid: Pid,
486        caps: &AgentCapabilities,
487    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
488        let agent_rid = ResourceId::new(format!("/kernel/agents/{agent_id}"));
489        let parent = ResourceId::new("/kernel/agents");
490
491        // Insert tree node
492        {
493            let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
494            tree.insert(agent_rid.clone(), ResourceKind::Agent, parent.clone())?;
495
496            // Set metadata
497            if let Some(node) = tree.get_mut(&agent_rid) {
498                node.metadata.insert("pid".into(), serde_json::json!(pid));
499                node.metadata
500                    .insert("state".into(), serde_json::json!("starting"));
501                node.metadata.insert(
502                    "spawn_time".into(),
503                    serde_json::json!(Utc::now().to_rfc3339()),
504                );
505                node.metadata
506                    .insert("can_spawn".into(), serde_json::json!(caps.can_spawn));
507                node.metadata
508                    .insert("can_ipc".into(), serde_json::json!(caps.can_ipc));
509                node.metadata.insert(
510                    "can_exec_tools".into(),
511                    serde_json::json!(caps.can_exec_tools),
512                );
513            }
514            // Incremental Merkle update: O(depth) path recompute (WEFT-145)
515            tree.recompute_path(&agent_rid);
516        }
517
518        // Chain event
519        let chain_event = self.chain.append(
520            "agent",
521            "agent.spawn",
522            Some(serde_json::json!({
523                "agent_id": agent_id,
524                "pid": pid,
525                "capabilities": {
526                    "can_spawn": caps.can_spawn,
527                    "can_ipc": caps.can_ipc,
528                    "can_exec_tools": caps.can_exec_tools,
529                    "can_network": caps.can_network,
530                },
531            })),
532        );
533
534        // Store chain_seq
535        {
536            let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
537            if let Some(node) = tree.get_mut(&agent_rid) {
538                node.metadata
539                    .insert("chain_seq".into(), serde_json::json!(chain_event.sequence));
540            }
541        }
542
543        // Mutation log
544        let now = Utc::now();
545        #[cfg(feature = "exochain")]
546        let sig = self.mutation_signature("create", &agent_rid.to_string(), &now);
547        #[cfg(not(feature = "exochain"))]
548        let sig = None;
549        let mut log = self
550            .mutation_log
551            .lock()
552            .map_err(|e| format!("log lock: {e}"))?;
553        log.append(MutationEvent::Create {
554            id: agent_rid,
555            kind: ResourceKind::Agent,
556            parent,
557            timestamp: now,
558            signature: sig,
559        });
560
561        debug!(agent_id, pid, "agent registered in tree");
562        Ok(())
563    }
564
565    /// Unregister an agent from the tree.
566    ///
567    /// Updates the tree node metadata (state=exited, exit_code, stop_time)
568    /// and emits an `agent.stop` chain event. Does NOT remove the node
569    /// (preserves audit trail).
570    pub fn unregister_agent(
571        &self,
572        agent_id: &str,
573        pid: Pid,
574        exit_code: i32,
575    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
576        let agent_rid = ResourceId::new(format!("/kernel/agents/{agent_id}"));
577
578        {
579            let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
580            if let Some(node) = tree.get_mut(&agent_rid) {
581                node.metadata
582                    .insert("state".into(), serde_json::json!("exited"));
583                node.metadata
584                    .insert("exit_code".into(), serde_json::json!(exit_code));
585                node.metadata.insert(
586                    "stop_time".into(),
587                    serde_json::json!(Utc::now().to_rfc3339()),
588                );
589                node.updated_at = Utc::now();
590            }
591            // Incremental Merkle update: O(depth) path recompute (WEFT-145)
592            tree.recompute_path(&agent_rid);
593        }
594
595        self.chain.append(
596            "agent",
597            "agent.stop",
598            Some(serde_json::json!({
599                "agent_id": agent_id,
600                "pid": pid,
601                "exit_code": exit_code,
602            })),
603        );
604
605        let now = Utc::now();
606        #[cfg(feature = "exochain")]
607        let sig = self.mutation_signature("update_meta", &agent_rid.to_string(), &now);
608        #[cfg(not(feature = "exochain"))]
609        let sig = None;
610        let mut log = self
611            .mutation_log
612            .lock()
613            .map_err(|e| format!("log lock: {e}"))?;
614        log.append(MutationEvent::UpdateMeta {
615            id: agent_rid,
616            key: "state".into(),
617            value: Some(serde_json::json!("exited")),
618            timestamp: now,
619            signature: sig,
620        });
621
622        debug!(agent_id, pid, exit_code, "agent unregistered in tree");
623        Ok(())
624    }
625
626    /// Update an agent's state in the tree.
627    pub fn update_agent_state(
628        &self,
629        agent_id: &str,
630        state: &str,
631    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
632        let agent_rid = ResourceId::new(format!("/kernel/agents/{agent_id}"));
633        self.update_meta(&agent_rid, "state", serde_json::json!(state))
634    }
635
636    /// Get direct access to the resource tree (for read-only queries).
637    pub fn tree(&self) -> &Mutex<ResourceTree> {
638        &self.tree
639    }
640
641    /// Get direct access to the mutation log.
642    pub fn mutation_log(&self) -> &Mutex<MutationLog> {
643        &self.mutation_log
644    }
645
646    /// Get the chain manager.
647    pub fn chain(&self) -> &Arc<ChainManager> {
648        &self.chain
649    }
650
651    /// Get the root hash of the resource tree.
652    pub fn root_hash(&self) -> [u8; 32] {
653        self.tree.lock().map(|t| t.root_hash()).unwrap_or([0u8; 32])
654    }
655
656    /// Get a statistics snapshot.
657    pub fn stats(&self) -> TreeStats {
658        let tree = self.tree.lock().unwrap();
659        let log = self.mutation_log.lock().unwrap();
660        TreeStats {
661            node_count: tree.len(),
662            mutation_count: log.len(),
663            root_hash: hex_hash(&tree.root_hash()),
664        }
665    }
666
667    // --- Scoring API ---
668
669    /// Set the scoring vector for a node.
670    ///
671    /// Updates the tree, logs a MutationEvent::UpdateScoring, and emits
672    /// a `scoring.update` chain event.
673    pub fn update_scoring(
674        &self,
675        id: &ResourceId,
676        scoring: NodeScoring,
677    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
678        let old = {
679            let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
680            tree.update_scoring(id, scoring)
681                .ok_or_else(|| format!("node not found: {id}"))?
682        };
683
684        self.chain.append(
685            "scoring",
686            "scoring.update",
687            Some(serde_json::json!({
688                "path": id.to_string(),
689                "old": old.as_array(),
690                "new": scoring.as_array(),
691            })),
692        );
693
694        let now = Utc::now();
695        #[cfg(feature = "exochain")]
696        let sig = self.mutation_signature("update_scoring", &id.to_string(), &now);
697        #[cfg(not(feature = "exochain"))]
698        let sig = None;
699        let mut log = self
700            .mutation_log
701            .lock()
702            .map_err(|e| format!("log lock: {e}"))?;
703        log.append(MutationEvent::UpdateScoring {
704            id: id.clone(),
705            old,
706            new: scoring,
707            timestamp: now,
708            signature: sig,
709        });
710
711        debug!(path = %id, "scoring updated");
712        Ok(())
713    }
714
715    /// EMA-blend an observation into a node's scoring.
716    ///
717    /// Emits a `scoring.blend` chain event.
718    pub fn blend_scoring(
719        &self,
720        id: &ResourceId,
721        observation: &NodeScoring,
722        alpha: f32,
723    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
724        let (old, new) = {
725            let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
726            let node = tree
727                .get(id)
728                .ok_or_else(|| format!("node not found: {id}"))?;
729            let old = node.scoring;
730
731            tree.blend_scoring(id, observation, alpha);
732
733            let node = tree
734                .get(id)
735                .ok_or_else(|| format!("node not found: {id}"))?;
736            let new = node.scoring;
737            (old, new)
738        };
739
740        self.chain.append(
741            "scoring",
742            "scoring.blend",
743            Some(serde_json::json!({
744                "path": id.to_string(),
745                "alpha": alpha,
746                "old": old.as_array(),
747                "new": new.as_array(),
748            })),
749        );
750
751        let now = Utc::now();
752        #[cfg(feature = "exochain")]
753        let sig = self.mutation_signature("update_scoring", &id.to_string(), &now);
754        #[cfg(not(feature = "exochain"))]
755        let sig = None;
756        let mut log = self
757            .mutation_log
758            .lock()
759            .map_err(|e| format!("log lock: {e}"))?;
760        log.append(MutationEvent::UpdateScoring {
761            id: id.clone(),
762            old,
763            new,
764            timestamp: now,
765            signature: sig,
766        });
767
768        debug!(path = %id, alpha, "scoring blended");
769        Ok(())
770    }
771
772    /// Get the scoring vector for a node.
773    pub fn get_scoring(&self, id: &ResourceId) -> Option<NodeScoring> {
774        let tree = self.tree.lock().ok()?;
775        tree.get(id).map(|n| n.scoring)
776    }
777
778    /// Find nodes most similar to a target node by cosine similarity.
779    ///
780    /// Returns up to `count` `(ResourceId, similarity)` pairs sorted by
781    /// descending similarity.
782    pub fn find_similar(&self, target_id: &ResourceId, count: usize) -> Vec<(ResourceId, f32)> {
783        let tree = match self.tree.lock() {
784            Ok(t) => t,
785            Err(_) => return Vec::new(),
786        };
787        let target = match tree.get(target_id) {
788            Some(n) => n.scoring,
789            None => return Vec::new(),
790        };
791
792        let mut scored: Vec<(ResourceId, f32)> = tree
793            .iter()
794            .filter(|(id, _)| *id != target_id)
795            .map(|(id, node)| (id.clone(), target.cosine_similarity(&node.scoring)))
796            .collect();
797
798        scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
799        scored.truncate(count);
800        scored
801    }
802
803    /// Rank all nodes by weighted score.
804    ///
805    /// Returns up to `count` `(ResourceId, weighted_score)` pairs sorted
806    /// by descending score.
807    pub fn rank_by_score(&self, weights: &[f32; 6], count: usize) -> Vec<(ResourceId, f32)> {
808        let tree = match self.tree.lock() {
809            Ok(t) => t,
810            Err(_) => return Vec::new(),
811        };
812
813        let mut scored: Vec<(ResourceId, f32)> = tree
814            .iter()
815            .map(|(id, node)| (id.clone(), node.scoring.weighted_score(weights)))
816            .collect();
817
818        scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
819        scored.truncate(count);
820        scored
821    }
822
823    // --- Tool lifecycle API ---
824
825    /// Build a tool: validate WASM bytes, compute hash, sign with Ed25519.
826    ///
827    /// Returns a `ToolVersion` with the computed module hash and Ed25519
828    /// signature. Emits a `tool.build` chain event.
829    pub fn build_tool(
830        &self,
831        name: &str,
832        wasm_bytes: &[u8],
833        signing_key: &ed25519_dalek::SigningKey,
834    ) -> Result<ToolVersion, Box<dyn std::error::Error + Send + Sync>> {
835        let module_hash = compute_module_hash(wasm_bytes);
836
837        use ed25519_dalek::Signer;
838        let signature = signing_key.sign(&module_hash);
839        let sig_bytes: [u8; 64] = signature.to_bytes();
840
841        let chain_event = self.chain.append(
842            "tool",
843            "tool.build",
844            Some(serde_json::json!({
845                "name": name,
846                "module_hash": hex_hash(&module_hash),
847                "sig_algo": "Ed25519",
848            })),
849        );
850
851        let version = ToolVersion {
852            version: 1,
853            module_hash,
854            signature: sig_bytes,
855            deployed_at: Utc::now(),
856            revoked: false,
857            chain_seq: chain_event.sequence,
858        };
859
860        debug!(name, "tool built with hash and signature");
861        Ok(version)
862    }
863
864    /// Deploy a tool to the resource tree.
865    ///
866    /// Creates the tool node at `/kernel/tools/{category}/{name}`,
867    /// stores version metadata, and emits a `tool.deploy` chain event.
868    pub fn deploy_tool(
869        &self,
870        spec: &BuiltinToolSpec,
871        version: &ToolVersion,
872    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
873        let cat = match spec.category {
874            crate::wasm_runner::ToolCategory::Filesystem => "fs",
875            crate::wasm_runner::ToolCategory::Agent => "agent",
876            crate::wasm_runner::ToolCategory::System => "sys",
877            crate::wasm_runner::ToolCategory::Ecc => "ecc",
878            crate::wasm_runner::ToolCategory::User => "user",
879        };
880
881        // Ensure category namespace exists
882        let cat_path = format!("/kernel/tools/{cat}");
883        let cat_rid = ResourceId::new(&cat_path);
884        {
885            let tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
886            if tree.get(&cat_rid).is_none() {
887                drop(tree);
888                self.insert(
889                    cat_rid,
890                    ResourceKind::Namespace,
891                    ResourceId::new("/kernel/tools"),
892                )?;
893            }
894        }
895
896        // Tool short name (e.g. "read_file" from "fs.read_file")
897        let short_name = spec.name.rsplit('.').next().unwrap_or(&spec.name);
898        let tool_path = format!("/kernel/tools/{cat}/{short_name}");
899        let tool_rid = ResourceId::new(&tool_path);
900
901        // Insert the tool node
902        self.insert(
903            tool_rid.clone(),
904            ResourceKind::Tool,
905            ResourceId::new(&cat_path),
906        )?;
907
908        // Set metadata
909        self.update_meta(
910            &tool_rid,
911            "tool_version",
912            serde_json::json!(version.version),
913        )?;
914        self.update_meta(
915            &tool_rid,
916            "module_hash",
917            serde_json::json!(hex_hash(&version.module_hash)),
918        )?;
919        self.update_meta(
920            &tool_rid,
921            "gate_action",
922            serde_json::json!(&spec.gate_action),
923        )?;
924        self.update_meta(
925            &tool_rid,
926            "deployed_at",
927            serde_json::json!(version.deployed_at.to_rfc3339()),
928        )?;
929
930        // K4 B2: Persist version history array in tree metadata
931        let versions_array = serde_json::json!([{
932            "version": version.version,
933            "module_hash": hex_hash(&version.module_hash),
934            "deployed_at": version.deployed_at.to_rfc3339(),
935            "revoked": version.revoked,
936            "chain_seq": version.chain_seq,
937        }]);
938        self.update_meta(&tool_rid, "versions", versions_array)?;
939
940        self.chain.append(
941            "tool",
942            "tool.deploy",
943            Some(serde_json::json!({
944                "name": spec.name,
945                "version": version.version,
946                "tree_path": tool_path,
947                "module_hash": hex_hash(&version.module_hash),
948                "gate_action": spec.gate_action,
949            })),
950        );
951
952        debug!(tool = %spec.name, version = version.version, "tool deployed");
953        Ok(())
954    }
955
956    /// Update a tool to a new version.
957    ///
958    /// Updates the tool node's metadata with new version info and
959    /// emits a `tool.version.update` chain event linking old to new.
960    pub fn update_tool_version(
961        &self,
962        name: &str,
963        new_version: &ToolVersion,
964    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
965        let parts: Vec<&str> = name.splitn(2, '.').collect();
966        if parts.len() != 2 {
967            return Err(format!("invalid tool name: {name}").into());
968        }
969        let tool_path = format!("/kernel/tools/{}/{}", parts[0], parts[1]);
970        let tool_rid = ResourceId::new(&tool_path);
971
972        // Get old version info from metadata
973        let (old_version, old_hash) = {
974            let tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
975            let node = tree
976                .get(&tool_rid)
977                .ok_or_else(|| format!("tool not found: {tool_path}"))?;
978            let ver = node
979                .metadata
980                .get("tool_version")
981                .and_then(|v| v.as_u64())
982                .unwrap_or(0) as u32;
983            let hash = node
984                .metadata
985                .get("module_hash")
986                .and_then(|v| v.as_str())
987                .unwrap_or("")
988                .to_string();
989            (ver, hash)
990        };
991
992        self.update_meta(
993            &tool_rid,
994            "tool_version",
995            serde_json::json!(new_version.version),
996        )?;
997        self.update_meta(
998            &tool_rid,
999            "module_hash",
1000            serde_json::json!(hex_hash(&new_version.module_hash)),
1001        )?;
1002        self.update_meta(
1003            &tool_rid,
1004            "deployed_at",
1005            serde_json::json!(new_version.deployed_at.to_rfc3339()),
1006        )?;
1007
1008        // K4 B2: Append to version history array
1009        {
1010            let tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
1011            let node = tree
1012                .get(&tool_rid)
1013                .ok_or_else(|| format!("tool not found: {tool_path}"))?;
1014            let mut versions: Vec<serde_json::Value> = node
1015                .metadata
1016                .get("versions")
1017                .and_then(|v| serde_json::from_value(v.clone()).ok())
1018                .unwrap_or_default();
1019            versions.push(serde_json::json!({
1020                "version": new_version.version,
1021                "module_hash": hex_hash(&new_version.module_hash),
1022                "deployed_at": new_version.deployed_at.to_rfc3339(),
1023                "revoked": new_version.revoked,
1024                "chain_seq": new_version.chain_seq,
1025            }));
1026            drop(tree);
1027            self.update_meta(&tool_rid, "versions", serde_json::json!(versions))?;
1028        }
1029
1030        self.chain.append(
1031            "tool",
1032            "tool.version.update",
1033            Some(serde_json::json!({
1034                "name": name,
1035                "old_version": old_version,
1036                "new_version": new_version.version,
1037                "old_hash": old_hash,
1038                "new_hash": hex_hash(&new_version.module_hash),
1039            })),
1040        );
1041
1042        debug!(
1043            tool = name,
1044            old = old_version,
1045            new = new_version.version,
1046            "tool version updated"
1047        );
1048        Ok(())
1049    }
1050
1051    /// Revoke a tool version.
1052    ///
1053    /// Marks the specified version as revoked in metadata. Does NOT
1054    /// delete the tree node (preserves audit trail). Emits a
1055    /// `tool.version.revoke` chain event.
1056    pub fn revoke_tool_version(
1057        &self,
1058        name: &str,
1059        version: u32,
1060    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1061        let parts: Vec<&str> = name.splitn(2, '.').collect();
1062        if parts.len() != 2 {
1063            return Err(format!("invalid tool name: {name}").into());
1064        }
1065        let tool_path = format!("/kernel/tools/{}/{}", parts[0], parts[1]);
1066        let tool_rid = ResourceId::new(&tool_path);
1067
1068        {
1069            let tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
1070            tree.get(&tool_rid)
1071                .ok_or_else(|| format!("tool not found: {tool_path}"))?;
1072        }
1073
1074        let revoke_key = format!("v{version}_revoked");
1075        let revoke_at_key = format!("v{version}_revoked_at");
1076        self.update_meta(&tool_rid, &revoke_key, serde_json::json!(true))?;
1077        self.update_meta(
1078            &tool_rid,
1079            &revoke_at_key,
1080            serde_json::json!(Utc::now().to_rfc3339()),
1081        )?;
1082
1083        let module_hash = {
1084            let tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
1085            let node = tree.get(&tool_rid).unwrap();
1086            node.metadata
1087                .get("module_hash")
1088                .and_then(|v| v.as_str())
1089                .unwrap_or("")
1090                .to_string()
1091        };
1092
1093        self.chain.append(
1094            "tool",
1095            "tool.version.revoke",
1096            Some(serde_json::json!({
1097                "name": name,
1098                "version": version,
1099                "module_hash": module_hash,
1100            })),
1101        );
1102
1103        debug!(tool = name, version, "tool version revoked");
1104        Ok(())
1105    }
1106
1107    /// Query version history for a tool (K4 B2).
1108    ///
1109    /// Returns the list of version entries persisted in the tree
1110    /// metadata, or an empty vec if the tool has no versions.
1111    pub fn get_tool_versions(&self, name: &str) -> Vec<serde_json::Value> {
1112        let parts: Vec<&str> = name.splitn(2, '.').collect();
1113        if parts.len() != 2 {
1114            return Vec::new();
1115        }
1116        let tool_path = format!("/kernel/tools/{}/{}", parts[0], parts[1]);
1117        let tool_rid = ResourceId::new(&tool_path);
1118
1119        let tree = match self.tree.lock() {
1120            Ok(t) => t,
1121            Err(_) => return Vec::new(),
1122        };
1123        tree.get(&tool_rid)
1124            .and_then(|node| node.metadata.get("versions"))
1125            .and_then(|v| serde_json::from_value::<Vec<serde_json::Value>>(v.clone()).ok())
1126            .unwrap_or_default()
1127    }
1128
1129    /// Save tree state to a checkpoint file on disk.
1130    ///
1131    /// Serializes the tree via `exo_resource_tree::to_checkpoint()` and
1132    /// writes the bytes to the given path.
1133    pub fn save_checkpoint(
1134        &self,
1135        path: &std::path::Path,
1136    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1137        let tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
1138        let data = exo_resource_tree::to_checkpoint(&tree)?;
1139
1140        if let Some(parent) = path.parent() {
1141            std::fs::create_dir_all(parent)?;
1142        }
1143        std::fs::write(path, &data)?;
1144
1145        debug!(
1146            path = %path.display(),
1147            nodes = tree.len(),
1148            bytes = data.len(),
1149            "tree checkpoint saved"
1150        );
1151        Ok(())
1152    }
1153
1154    /// Load tree state from a checkpoint file.
1155    ///
1156    /// Reads the file, deserializes via `exo_resource_tree::from_checkpoint()`,
1157    /// and replaces the internal tree. The mutation log is cleared.
1158    pub fn load_checkpoint(
1159        &self,
1160        path: &std::path::Path,
1161    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1162        let data = std::fs::read(path)?;
1163        let restored = exo_resource_tree::from_checkpoint(&data)?;
1164
1165        let node_count = restored.len();
1166        let root_hash = hex_hash(&restored.root_hash());
1167
1168        let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
1169        *tree = restored;
1170
1171        // Clear mutation log since we loaded a fresh snapshot
1172        let mut log = self
1173            .mutation_log
1174            .lock()
1175            .map_err(|e| format!("log lock: {e}"))?;
1176        *log = MutationLog::new();
1177
1178        debug!(
1179            path = %path.display(),
1180            nodes = node_count,
1181            root_hash = %root_hash,
1182            "tree checkpoint loaded"
1183        );
1184        Ok(())
1185    }
1186
1187    /// Create a combined checkpoint of tree + mutation log + chain state.
1188    ///
1189    /// Returns JSON with the tree checkpoint, mutation count, and chain
1190    /// checkpoint info. Full checkpoint persistence is a K1 concern.
1191    pub fn checkpoint(
1192        &self,
1193    ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
1194        let tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
1195        let log = self
1196            .mutation_log
1197            .lock()
1198            .map_err(|e| format!("log lock: {e}"))?;
1199
1200        let tree_data = exo_resource_tree::to_checkpoint(&tree)?;
1201        let chain_cp = self.chain.checkpoint();
1202
1203        let checkpoint = serde_json::json!({
1204            "tree": serde_json::from_slice::<serde_json::Value>(&tree_data)
1205                .unwrap_or(serde_json::Value::Null),
1206            "mutation_count": log.len(),
1207            "chain_checkpoint": {
1208                "chain_id": chain_cp.chain_id,
1209                "sequence": chain_cp.sequence,
1210                "timestamp": chain_cp.timestamp.to_rfc3339(),
1211            },
1212            "root_hash": hex_hash(&tree.root_hash()),
1213        });
1214
1215        // Log checkpoint event on chain
1216        drop(tree);
1217        drop(log);
1218        self.chain.append(
1219            "tree",
1220            "checkpoint",
1221            Some(serde_json::json!({
1222                "root_hash": checkpoint["root_hash"],
1223                "chain_seq": chain_cp.sequence,
1224            })),
1225        );
1226
1227        Ok(checkpoint)
1228    }
1229
1230    // --- K6 cross-node sync API ---
1231
1232    /// Create a serializable snapshot of the full tree state.
1233    /// Used for cross-node tree synchronization in K6.4.
1234    pub fn snapshot(&self) -> Result<TreeSnapshot, Box<dyn std::error::Error + Send + Sync>> {
1235        let tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
1236        let root_hash = hex_hash(&tree.root_hash());
1237        let node_count = tree.len();
1238
1239        let nodes: Vec<TreeNodeSnapshot> = tree
1240            .iter()
1241            .map(|(id, node)| {
1242                let metadata = node
1243                    .metadata
1244                    .iter()
1245                    .map(|(k, v)| (k.clone(), v.to_string()))
1246                    .collect();
1247                TreeNodeSnapshot {
1248                    path: id.to_string(),
1249                    kind: format!("{:?}", node.kind),
1250                    metadata,
1251                    hash: hex_hash(&node.merkle_hash),
1252                }
1253            })
1254            .collect();
1255
1256        Ok(TreeSnapshot {
1257            root_hash,
1258            node_count,
1259            nodes,
1260            taken_at: Utc::now(),
1261        })
1262    }
1263
1264    /// Apply a remote mutation received from a peer node.
1265    /// Records the mutation in the local log.
1266    ///
1267    /// Does **not** verify signatures — prefer
1268    /// [`Self::apply_remote_mutation_verified`] for cross-node events
1269    /// (WEFT-144). Kept for local/unsigned bootstrap paths.
1270    pub fn apply_remote_mutation(
1271        &self,
1272        event: MutationEvent,
1273    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1274        self.apply_remote_mutation_inner(event)
1275    }
1276
1277    /// Apply a remote mutation after verifying its Ed25519 signature
1278    /// against the peer's public key (WEFT-144).
1279    ///
1280    /// Rejects missing, malformed, and invalid signatures. Create is
1281    /// idempotent when the node already exists (replay-safe).
1282    #[cfg(feature = "exochain")]
1283    pub fn apply_remote_mutation_verified(
1284        &self,
1285        event: MutationEvent,
1286        peer_pubkey: &ed25519_dalek::VerifyingKey,
1287    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1288        Self::verify_mutation_signature(&event, peer_pubkey)?;
1289        self.apply_remote_mutation_inner(event)
1290    }
1291
1292    fn apply_remote_mutation_inner(
1293        &self,
1294        event: MutationEvent,
1295    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1296        let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
1297        let mut log = self
1298            .mutation_log
1299            .lock()
1300            .map_err(|e| format!("log lock: {e}"))?;
1301
1302        // Replay defense: reject exact duplicate of the latest event
1303        // for the same operation identity (kind + id + timestamp).
1304        if let Some(last) = log.events().last()
1305            && mutation_event_eq(last, &event)
1306        {
1307            return Err("mutation replay rejected: exact duplicate of last event".into());
1308        }
1309
1310        // Apply based on mutation type
1311        match &event {
1312            // Only insert if not already present (idempotent); otherwise
1313            // fall through to the no-op arm below.
1314            MutationEvent::Create {
1315                id, kind, parent, ..
1316            } if tree.get(id).is_none() => {
1317                tree.insert(id.clone(), kind.clone(), parent.clone())?;
1318                tree.recompute_path(id);
1319            }
1320            MutationEvent::Remove { id, .. } if tree.get(id).is_some() => {
1321                let removed = tree.remove(id.clone())?;
1322                if let Some(parent) = removed.parent {
1323                    tree.recompute_path(&parent);
1324                }
1325            }
1326            MutationEvent::UpdateMeta { id, key, value, .. } => {
1327                if let Some(node) = tree.get_mut(id) {
1328                    if let Some(val) = value {
1329                        node.metadata.insert(key.clone(), val.clone());
1330                    } else {
1331                        node.metadata.remove(key);
1332                    }
1333                    node.updated_at = Utc::now();
1334                }
1335                tree.recompute_path(id);
1336            }
1337            // K6.4 (WEFT-106): apply scoring so Merkle roots converge.
1338            MutationEvent::UpdateScoring { id, new, .. } if tree.get(id).is_some() => {
1339                tree.update_scoring(id, *new);
1340            }
1341            MutationEvent::Move { .. } => {
1342                // Structural move still deferred (no ResourceTree::move yet).
1343            }
1344            _ => {
1345                // Future MutationEvent variants -- record but do not apply.
1346            }
1347        }
1348
1349        // Record the mutation
1350        log.append(event);
1351
1352        Ok(())
1353    }
1354
1355    // ── WEFT-106 / K6.4: Merkle diff + signed tree sync ────────────────────
1356
1357    /// Compute a minimal Merkle diff of `self` against `other`.
1358    ///
1359    /// Only subtrees whose hashes differ are visited (O(changed), not O(tree)).
1360    pub fn merkle_diff(
1361        &self,
1362        other: &TreeManager,
1363    ) -> Result<MerkleDiff, Box<dyn std::error::Error + Send + Sync>> {
1364        let local = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
1365        let remote = other
1366            .tree
1367            .lock()
1368            .map_err(|e| format!("other tree lock: {e}"))?;
1369        Ok(local.merkle_diff(&remote))
1370    }
1371
1372    /// Generate a Merkle inclusion proof for a resource path.
1373    pub fn merkle_proof(
1374        &self,
1375        id: &ResourceId,
1376    ) -> Result<Option<MerkleInclusionProof>, Box<dyn std::error::Error + Send + Sync>> {
1377        let tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
1378        Ok(tree.merkle_proof(id))
1379    }
1380
1381    /// Verify a Merkle inclusion proof against this tree's root.
1382    pub fn verify_merkle_proof(
1383        &self,
1384        proof: &MerkleInclusionProof,
1385    ) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
1386        let tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
1387        Ok(tree.verify_proof(proof))
1388    }
1389
1390    /// Collect signed mutation events from the local log that touch paths in
1391    /// `diff` (Added / Modified / Removed). Used to ship authenticated deltas
1392    /// alongside a MerkleDiff (WEFT-106 + WEFT-144).
1393    pub fn signed_events_for_diff(
1394        &self,
1395        diff: &MerkleDiff,
1396    ) -> Result<Vec<MutationEvent>, Box<dyn std::error::Error + Send + Sync>> {
1397        let paths: std::collections::HashSet<String> = diff
1398            .entries
1399            .iter()
1400            .map(|e| e.id.to_string())
1401            .collect();
1402        let log = self
1403            .mutation_log
1404            .lock()
1405            .map_err(|e| format!("log lock: {e}"))?;
1406
1407        let mut out = Vec::new();
1408        for evt in log.events() {
1409            let path = match evt {
1410                MutationEvent::Create { id, .. }
1411                | MutationEvent::Remove { id, .. }
1412                | MutationEvent::UpdateMeta { id, .. }
1413                | MutationEvent::Move { id, .. }
1414                | MutationEvent::UpdateScoring { id, .. } => id.to_string(),
1415                _ => continue,
1416            };
1417            if paths.contains(&path) {
1418                out.push(evt.clone());
1419            }
1420        }
1421        Ok(out)
1422    }
1423
1424    /// Apply a peer's Merkle diff using verified signed mutation events
1425    /// (WEFT-106 wired to WEFT-144 `apply_remote_mutation_verified`).
1426    ///
1427    /// Events are applied in log order. Each must carry a valid Ed25519
1428    /// signature under `peer_pubkey`. After structural events, node
1429    /// payloads from the diff (metadata / scoring) are reconciled so
1430    /// Merkle roots converge — `MutationEvent::Create` does not carry
1431    /// full metadata, so the minimal-witness payload is authoritative
1432    /// for those fields once the path is authenticated by a signed
1433    /// Create/Update.
1434    #[cfg(feature = "exochain")]
1435    pub fn apply_merkle_diff_verified(
1436        &self,
1437        diff: &MerkleDiff,
1438        signed_events: &[MutationEvent],
1439        peer_pubkey: &ed25519_dalek::VerifyingKey,
1440    ) -> Result<usize, Box<dyn std::error::Error + Send + Sync>> {
1441        if diff.in_sync() {
1442            return Ok(0);
1443        }
1444        let mut applied = 0usize;
1445        for event in signed_events {
1446            // Skip exact duplicates already in our log (idempotent catch-up).
1447            {
1448                let log = self
1449                    .mutation_log
1450                    .lock()
1451                    .map_err(|e| format!("log lock: {e}"))?;
1452                if log.events().iter().any(|e| mutation_event_eq(e, event)) {
1453                    continue;
1454                }
1455            }
1456            match self.apply_remote_mutation_verified(event.clone(), peer_pubkey) {
1457                Ok(()) => applied += 1,
1458                Err(e) => {
1459                    let msg = e.to_string();
1460                    // Replay of the last event is non-fatal during catch-up.
1461                    if msg.contains("replay") {
1462                        continue;
1463                    }
1464                    return Err(e);
1465                }
1466            }
1467        }
1468
1469        // Reconcile metadata / scoring from diff payloads (hash convergence).
1470        {
1471            let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
1472            for entry in &diff.entries {
1473                let Some(payload) = &entry.node else {
1474                    continue;
1475                };
1476                if matches!(entry.diff_kind, MerkleDiffKind::Removed) {
1477                    continue;
1478                }
1479                // Ensure node exists (Create may have been in the event set).
1480                if tree.get(&payload.id).is_none()
1481                    && let Some(parent) = &payload.parent
1482                {
1483                    let _ = tree.insert(payload.id.clone(), payload.kind.clone(), parent.clone());
1484                }
1485                if let Some(node) = tree.get_mut(&payload.id) {
1486                    node.metadata = payload.metadata.clone();
1487                    node.scoring = payload.scoring;
1488                    node.kind = payload.kind.clone();
1489                }
1490                tree.recompute_path(&payload.id);
1491            }
1492        }
1493
1494        Ok(applied)
1495    }
1496
1497    /// Build a mesh [`crate::mesh_tree::TreeSyncResponse`] from a MerkleDiff.
1498    #[cfg(feature = "mesh")]
1499    pub fn tree_sync_response_from_diff(
1500        &self,
1501        diff: &MerkleDiff,
1502    ) -> Result<crate::mesh_tree::TreeSyncResponse, Box<dyn std::error::Error + Send + Sync>> {
1503        use crate::mesh_tree::{TreeDiffType, TreeNodeDiff, TreeSyncResponse};
1504
1505        let tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
1506        let mut diff_nodes = Vec::new();
1507        let mut deleted_paths = Vec::new();
1508
1509        for entry in &diff.entries {
1510            match entry.diff_kind {
1511                MerkleDiffKind::Removed => {
1512                    deleted_paths.push(entry.id.to_string());
1513                }
1514                MerkleDiffKind::Added | MerkleDiffKind::Modified => {
1515                    let (kind, hash, metadata, chain_seq) = if let Some(payload) = &entry.node {
1516                        let meta: std::collections::HashMap<String, String> = payload
1517                            .metadata
1518                            .iter()
1519                            .map(|(k, v)| (k.clone(), v.to_string()))
1520                            .collect();
1521                        let chain_seq = payload
1522                            .metadata
1523                            .get("chain_seq")
1524                            .and_then(|v| v.as_u64());
1525                        (
1526                            format!("{:?}", payload.kind),
1527                            hex_hash(&payload.merkle_hash),
1528                            meta,
1529                            chain_seq,
1530                        )
1531                    } else if let Some(node) = tree.get(&entry.id) {
1532                        let meta: std::collections::HashMap<String, String> = node
1533                            .metadata
1534                            .iter()
1535                            .map(|(k, v)| (k.clone(), v.to_string()))
1536                            .collect();
1537                        let chain_seq = node
1538                            .metadata
1539                            .get("chain_seq")
1540                            .and_then(|v| v.as_u64());
1541                        (
1542                            format!("{:?}", node.kind),
1543                            hex_hash(&node.merkle_hash),
1544                            meta,
1545                            chain_seq,
1546                        )
1547                    } else {
1548                        continue;
1549                    };
1550                    let diff_type = match entry.diff_kind {
1551                        MerkleDiffKind::Added => TreeDiffType::Added,
1552                        MerkleDiffKind::Modified => TreeDiffType::Modified,
1553                        MerkleDiffKind::Removed => TreeDiffType::Removed,
1554                        _ => TreeDiffType::Modified,
1555                    };
1556                    diff_nodes.push(TreeNodeDiff {
1557                        path: entry.id.to_string(),
1558                        kind,
1559                        hash,
1560                        metadata,
1561                        chain_seq,
1562                        diff_type,
1563                    });
1564                }
1565                _ => {}
1566            }
1567        }
1568
1569        Ok(TreeSyncResponse {
1570            remote_root_hash: hex_hash(&diff.local_root),
1571            in_sync: diff.in_sync(),
1572            diff_nodes,
1573            deleted_paths,
1574        })
1575    }
1576}
1577
1578/// Structural equality for mutation replay detection (ignores signature
1579/// byte-identity only when both sides carry the same fields).
1580fn mutation_event_eq(a: &MutationEvent, b: &MutationEvent) -> bool {
1581    use MutationEvent::*;
1582    match (a, b) {
1583        (
1584            Create {
1585                id: id_a,
1586                kind: k_a,
1587                parent: p_a,
1588                timestamp: t_a,
1589                ..
1590            },
1591            Create {
1592                id: id_b,
1593                kind: k_b,
1594                parent: p_b,
1595                timestamp: t_b,
1596                ..
1597            },
1598        ) => id_a == id_b && k_a == k_b && p_a == p_b && t_a == t_b,
1599        (
1600            Remove {
1601                id: id_a,
1602                timestamp: t_a,
1603                ..
1604            },
1605            Remove {
1606                id: id_b,
1607                timestamp: t_b,
1608                ..
1609            },
1610        ) => id_a == id_b && t_a == t_b,
1611        (
1612            UpdateMeta {
1613                id: id_a,
1614                key: key_a,
1615                value: v_a,
1616                timestamp: t_a,
1617                ..
1618            },
1619            UpdateMeta {
1620                id: id_b,
1621                key: key_b,
1622                value: v_b,
1623                timestamp: t_b,
1624                ..
1625            },
1626        ) => id_a == id_b && key_a == key_b && v_a == v_b && t_a == t_b,
1627        _ => false,
1628    }
1629}
1630
1631impl std::fmt::Debug for TreeManager {
1632    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1633        let stats = self.stats();
1634        f.debug_struct("TreeManager")
1635            .field("node_count", &stats.node_count)
1636            .field("mutation_count", &stats.mutation_count)
1637            .finish()
1638    }
1639}
1640
1641/// Format a 32-byte hash as a hex string.
1642fn hex_hash(hash: &[u8; 32]) -> String {
1643    hash.iter().map(|b| format!("{b:02x}")).collect()
1644}
1645
1646#[cfg(test)]
1647mod tests {
1648    use super::*;
1649
1650    fn test_chain() -> Arc<ChainManager> {
1651        Arc::new(ChainManager::new(0, 1000))
1652    }
1653
1654    #[test]
1655    fn bootstrap_creates_nodes_and_chain_events() {
1656        let chain = test_chain();
1657        let tm = TreeManager::new(Arc::clone(&chain));
1658        tm.bootstrap().unwrap();
1659
1660        let tree = tm.tree().lock().unwrap();
1661        assert_eq!(tree.len(), 9); // root + 8 namespaces (incl /kernel/agents)
1662
1663        // Chain should have genesis + bootstrap event
1664        assert!(chain.len() >= 2);
1665        let events = chain.tail(0);
1666        assert!(
1667            events
1668                .iter()
1669                .any(|e| e.kind == "bootstrap" && e.source == "tree")
1670        );
1671
1672        // Mutation log should have 8 entries (one per bootstrapped namespace)
1673        let log = tm.mutation_log().lock().unwrap();
1674        assert_eq!(log.len(), 8);
1675    }
1676
1677    #[test]
1678    fn insert_creates_node_and_chain_event() {
1679        let chain = test_chain();
1680        let tm = TreeManager::new(Arc::clone(&chain));
1681        tm.bootstrap().unwrap();
1682
1683        let before_len = chain.len();
1684        tm.insert(
1685            ResourceId::new("/kernel/services/cron"),
1686            ResourceKind::Service,
1687            ResourceId::new("/kernel/services"),
1688        )
1689        .unwrap();
1690
1691        let tree = tm.tree().lock().unwrap();
1692        assert!(
1693            tree.get(&ResourceId::new("/kernel/services/cron"))
1694                .is_some()
1695        );
1696
1697        // Chain should have one more event
1698        assert_eq!(chain.len(), before_len + 1);
1699
1700        // Node should have chain_seq metadata
1701        let node = tree.get(&ResourceId::new("/kernel/services/cron")).unwrap();
1702        assert!(node.metadata.contains_key("chain_seq"));
1703    }
1704
1705    #[test]
1706    fn remove_creates_chain_event() {
1707        let chain = test_chain();
1708        let tm = TreeManager::new(Arc::clone(&chain));
1709        tm.bootstrap().unwrap();
1710
1711        tm.insert(
1712            ResourceId::new("/kernel/services/test"),
1713            ResourceKind::Service,
1714            ResourceId::new("/kernel/services"),
1715        )
1716        .unwrap();
1717
1718        let before_len = chain.len();
1719        tm.remove(ResourceId::new("/kernel/services/test")).unwrap();
1720
1721        let tree = tm.tree().lock().unwrap();
1722        assert!(
1723            tree.get(&ResourceId::new("/kernel/services/test"))
1724                .is_none()
1725        );
1726        assert_eq!(chain.len(), before_len + 1);
1727    }
1728
1729    #[test]
1730    fn update_meta_creates_chain_event() {
1731        let chain = test_chain();
1732        let tm = TreeManager::new(Arc::clone(&chain));
1733        tm.bootstrap().unwrap();
1734
1735        let before_len = chain.len();
1736        tm.update_meta(
1737            &ResourceId::new("/kernel"),
1738            "version",
1739            serde_json::json!("0.1.0"),
1740        )
1741        .unwrap();
1742
1743        assert_eq!(chain.len(), before_len + 1);
1744        let tree = tm.tree().lock().unwrap();
1745        let node = tree.get(&ResourceId::new("/kernel")).unwrap();
1746        assert_eq!(
1747            node.metadata.get("version").unwrap(),
1748            &serde_json::json!("0.1.0")
1749        );
1750    }
1751
1752    #[test]
1753    fn register_service_creates_node() {
1754        let chain = test_chain();
1755        let tm = TreeManager::new(Arc::clone(&chain));
1756        tm.bootstrap().unwrap();
1757
1758        tm.register_service("cron").unwrap();
1759
1760        let tree = tm.tree().lock().unwrap();
1761        let node = tree.get(&ResourceId::new("/kernel/services/cron")).unwrap();
1762        assert_eq!(node.kind, ResourceKind::Service);
1763        assert!(node.metadata.contains_key("chain_seq"));
1764    }
1765
1766    #[test]
1767    fn stats_reports_correctly() {
1768        let chain = test_chain();
1769        let tm = TreeManager::new(Arc::clone(&chain));
1770        tm.bootstrap().unwrap();
1771
1772        let stats = tm.stats();
1773        assert_eq!(stats.node_count, 9); // root + 8 namespaces
1774        assert_eq!(stats.mutation_count, 8);
1775        assert_ne!(stats.root_hash, "0".repeat(64));
1776    }
1777
1778    #[test]
1779    fn checkpoint_includes_tree_and_chain() {
1780        let chain = test_chain();
1781        let tm = TreeManager::new(Arc::clone(&chain));
1782        tm.bootstrap().unwrap();
1783
1784        let cp = tm.checkpoint().unwrap();
1785        assert!(cp.get("tree").is_some());
1786        assert!(cp.get("mutation_count").is_some());
1787        assert!(cp.get("chain_checkpoint").is_some());
1788        assert!(cp.get("root_hash").is_some());
1789    }
1790
1791    #[test]
1792    fn register_agent_creates_node_and_chain_event() {
1793        let chain = test_chain();
1794        let tm = TreeManager::new(Arc::clone(&chain));
1795        tm.bootstrap().unwrap();
1796
1797        let caps = crate::capability::AgentCapabilities::default();
1798        let before_len = chain.len();
1799        tm.register_agent("test-agent", 42, &caps).unwrap();
1800
1801        // Node should exist
1802        let tree = tm.tree().lock().unwrap();
1803        let node = tree
1804            .get(&ResourceId::new("/kernel/agents/test-agent"))
1805            .unwrap();
1806        assert_eq!(node.kind, ResourceKind::Agent);
1807        assert_eq!(node.metadata["pid"], serde_json::json!(42));
1808        assert_eq!(node.metadata["state"], serde_json::json!("starting"));
1809        assert!(node.metadata.contains_key("chain_seq"));
1810        drop(tree);
1811
1812        // Chain event
1813        assert!(chain.len() > before_len);
1814        let events = chain.tail(2);
1815        assert!(events.iter().any(|e| e.kind == "agent.spawn"));
1816    }
1817
1818    #[test]
1819    fn unregister_agent_updates_node() {
1820        let chain = test_chain();
1821        let tm = TreeManager::new(Arc::clone(&chain));
1822        tm.bootstrap().unwrap();
1823
1824        let caps = crate::capability::AgentCapabilities::default();
1825        tm.register_agent("exit-agent", 10, &caps).unwrap();
1826        tm.unregister_agent("exit-agent", 10, 0).unwrap();
1827
1828        // Node still exists but state=exited
1829        let tree = tm.tree().lock().unwrap();
1830        let node = tree
1831            .get(&ResourceId::new("/kernel/agents/exit-agent"))
1832            .unwrap();
1833        assert_eq!(node.metadata["state"], serde_json::json!("exited"));
1834        assert_eq!(node.metadata["exit_code"], serde_json::json!(0));
1835        assert!(node.metadata.contains_key("stop_time"));
1836        drop(tree);
1837
1838        // Chain event
1839        let events = chain.tail(2);
1840        assert!(events.iter().any(|e| e.kind == "agent.stop"));
1841    }
1842
1843    #[test]
1844    fn update_agent_state() {
1845        let chain = test_chain();
1846        let tm = TreeManager::new(Arc::clone(&chain));
1847        tm.bootstrap().unwrap();
1848
1849        let caps = crate::capability::AgentCapabilities::default();
1850        tm.register_agent("state-agent", 20, &caps).unwrap();
1851        tm.update_agent_state("state-agent", "running").unwrap();
1852
1853        let tree = tm.tree().lock().unwrap();
1854        let node = tree
1855            .get(&ResourceId::new("/kernel/agents/state-agent"))
1856            .unwrap();
1857        assert_eq!(node.metadata["state"], serde_json::json!("running"));
1858    }
1859
1860    #[test]
1861    fn chain_integrity_after_operations() {
1862        let chain = test_chain();
1863        let tm = TreeManager::new(Arc::clone(&chain));
1864        tm.bootstrap().unwrap();
1865        tm.register_service("cron").unwrap();
1866
1867        let result = chain.verify_integrity();
1868        assert!(result.valid);
1869        assert!(result.event_count >= 3); // genesis + bootstrap + insert
1870    }
1871
1872    // --- Scoring API tests ---
1873
1874    #[test]
1875    fn update_scoring_creates_chain_event() {
1876        let chain = test_chain();
1877        let tm = TreeManager::new(Arc::clone(&chain));
1878        tm.bootstrap().unwrap();
1879
1880        let before_len = chain.len();
1881        let scoring = NodeScoring::new(0.9, 0.8, 0.7, 0.6, 0.5, 0.4);
1882        tm.update_scoring(&ResourceId::new("/kernel"), scoring)
1883            .unwrap();
1884
1885        // Chain event emitted
1886        assert!(chain.len() > before_len);
1887        let events = chain.tail(2);
1888        assert!(events.iter().any(|e| e.kind == "scoring.update"));
1889
1890        // Scoring stored on node
1891        let s = tm.get_scoring(&ResourceId::new("/kernel")).unwrap();
1892        assert!((s.trust - 0.9).abs() < 1e-6);
1893
1894        // Mutation log has the entry
1895        let log = tm.mutation_log().lock().unwrap();
1896        let last = log.events().last().unwrap();
1897        assert!(matches!(last, MutationEvent::UpdateScoring { .. }));
1898    }
1899
1900    #[test]
1901    fn blend_scoring_creates_chain_event() {
1902        let chain = test_chain();
1903        let tm = TreeManager::new(Arc::clone(&chain));
1904        tm.bootstrap().unwrap();
1905
1906        let before_len = chain.len();
1907        let obs = NodeScoring::new(1.0, 1.0, 1.0, 1.0, 1.0, 1.0);
1908        tm.blend_scoring(&ResourceId::new("/kernel"), &obs, 0.5)
1909            .unwrap();
1910
1911        assert!(chain.len() > before_len);
1912        let events = chain.tail(2);
1913        assert!(events.iter().any(|e| e.kind == "scoring.blend"));
1914
1915        // Should be EMA blended: 0.5*0.5 + 1.0*0.5 = 0.75
1916        let s = tm.get_scoring(&ResourceId::new("/kernel")).unwrap();
1917        assert!((s.trust - 0.75).abs() < 1e-6);
1918    }
1919
1920    #[test]
1921    fn get_scoring_nonexistent_returns_none() {
1922        let chain = test_chain();
1923        let tm = TreeManager::new(Arc::clone(&chain));
1924        assert!(tm.get_scoring(&ResourceId::new("/no/such/node")).is_none());
1925    }
1926
1927    #[test]
1928    fn find_similar_returns_ranked() {
1929        let chain = test_chain();
1930        let tm = TreeManager::new(Arc::clone(&chain));
1931        tm.bootstrap().unwrap();
1932
1933        // Set /kernel to a specific scoring
1934        let target = NodeScoring::new(0.9, 0.9, 0.9, 0.9, 0.9, 0.9);
1935        tm.update_scoring(&ResourceId::new("/kernel"), target)
1936            .unwrap();
1937
1938        // /apps gets a similar scoring
1939        let similar = NodeScoring::new(0.85, 0.85, 0.85, 0.85, 0.85, 0.85);
1940        tm.update_scoring(&ResourceId::new("/apps"), similar)
1941            .unwrap();
1942
1943        let results = tm.find_similar(&ResourceId::new("/kernel"), 3);
1944        assert!(!results.is_empty());
1945        // First result should have high similarity
1946        assert!(results[0].1 > 0.9);
1947    }
1948
1949    #[test]
1950    fn rank_by_score_returns_ordered() {
1951        let chain = test_chain();
1952        let tm = TreeManager::new(Arc::clone(&chain));
1953        tm.bootstrap().unwrap();
1954
1955        // High trust on /kernel
1956        tm.update_scoring(
1957            &ResourceId::new("/kernel"),
1958            NodeScoring::new(1.0, 0.0, 0.0, 0.0, 0.0, 0.0),
1959        )
1960        .unwrap();
1961
1962        // High performance on /apps
1963        tm.update_scoring(
1964            &ResourceId::new("/apps"),
1965            NodeScoring::new(0.0, 1.0, 0.0, 0.0, 0.0, 0.0),
1966        )
1967        .unwrap();
1968
1969        // Rank by trust weight only
1970        let weights = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0];
1971        let ranked = tm.rank_by_score(&weights, 3);
1972        assert!(!ranked.is_empty());
1973        assert_eq!(ranked[0].0, ResourceId::new("/kernel"));
1974    }
1975
1976    #[test]
1977    fn checkpoint_roundtrip_preserves_root_hash() {
1978        let chain = test_chain();
1979        let tm = TreeManager::new(Arc::clone(&chain));
1980        tm.bootstrap().unwrap();
1981        tm.register_service("cron").unwrap();
1982
1983        let original_hash = tm.stats().root_hash;
1984
1985        // Simulate shutdown: save tree checkpoint and record root hash in chain.
1986        let dir = std::env::temp_dir().join("clawft-tree-ckpt-hash-test");
1987        let tree_path = dir.join("tree.json");
1988        tm.save_checkpoint(&tree_path).unwrap();
1989        chain.append(
1990            "tree",
1991            "tree.checkpoint",
1992            Some(serde_json::json!({
1993                "path": tree_path.display().to_string(),
1994                "root_hash": original_hash,
1995            })),
1996        );
1997
1998        // Simulate restart: create new tree manager, load checkpoint.
1999        let tm2 = TreeManager::new(Arc::clone(&chain));
2000        tm2.load_checkpoint(&tree_path).unwrap();
2001
2002        // Root hash should match what the chain recorded.
2003        let restored_hash = tm2.stats().root_hash;
2004        let chain_hash = chain.last_tree_root_hash().unwrap();
2005        assert_eq!(restored_hash, chain_hash);
2006        assert_eq!(restored_hash, original_hash);
2007
2008        // Clean up.
2009        let _ = std::fs::remove_dir_all(&dir);
2010    }
2011
2012    #[test]
2013    fn checkpoint_hash_mismatch_detectable() {
2014        let chain = test_chain();
2015        let tm = TreeManager::new(Arc::clone(&chain));
2016        tm.bootstrap().unwrap();
2017
2018        let dir = std::env::temp_dir().join("clawft-tree-ckpt-mismatch-test");
2019        let tree_path = dir.join("tree.json");
2020        tm.save_checkpoint(&tree_path).unwrap();
2021
2022        // Record a fake root hash in the chain (simulating corruption).
2023        chain.append(
2024            "tree",
2025            "tree.checkpoint",
2026            Some(serde_json::json!({
2027                "root_hash": "0000000000000000000000000000000000000000000000000000000000000000",
2028            })),
2029        );
2030
2031        // Load checkpoint and compare — should detect mismatch.
2032        let tm2 = TreeManager::new(Arc::clone(&chain));
2033        tm2.load_checkpoint(&tree_path).unwrap();
2034
2035        let restored_hash = tm2.stats().root_hash;
2036        let chain_hash = chain.last_tree_root_hash().unwrap();
2037        assert_ne!(restored_hash, chain_hash, "should detect hash mismatch");
2038
2039        // Clean up.
2040        let _ = std::fs::remove_dir_all(&dir);
2041    }
2042
2043    // --- Tool lifecycle tests ---
2044
2045    fn test_signing_key() -> ed25519_dalek::SigningKey {
2046        ed25519_dalek::SigningKey::from_bytes(&[42u8; 32])
2047    }
2048
2049    fn setup_tool_tree() -> (Arc<ChainManager>, TreeManager) {
2050        let chain = test_chain();
2051        let tm = TreeManager::new(Arc::clone(&chain));
2052        tm.bootstrap().unwrap();
2053        // Create /kernel/tools namespace
2054        tm.insert(
2055            ResourceId::new("/kernel/tools"),
2056            ResourceKind::Namespace,
2057            ResourceId::new("/kernel"),
2058        )
2059        .unwrap();
2060        (chain, tm)
2061    }
2062
2063    #[test]
2064    fn tool_build_computes_hash_and_signs() {
2065        let (_chain, tm) = setup_tool_tree();
2066        let key = test_signing_key();
2067        let wasm_bytes = b"fake wasm module bytes for testing";
2068
2069        let tv = tm.build_tool("fs.read_file", wasm_bytes, &key).unwrap();
2070        assert_eq!(tv.version, 1);
2071        assert!(!tv.revoked);
2072        // Hash should match compute_module_hash
2073        let expected = compute_module_hash(wasm_bytes);
2074        assert_eq!(tv.module_hash, expected);
2075        // Signature should be non-zero
2076        assert_ne!(tv.signature, [0u8; 64]);
2077    }
2078
2079    #[test]
2080    fn tool_deploy_creates_tree_node() {
2081        let (_chain, tm) = setup_tool_tree();
2082        let spec = crate::wasm_runner::builtin_tool_catalog()
2083            .into_iter()
2084            .find(|s| s.name == "fs.read_file")
2085            .unwrap();
2086        let tv = ToolVersion {
2087            version: 1,
2088            module_hash: [0xAA; 32],
2089            signature: [0xBB; 64],
2090            deployed_at: Utc::now(),
2091            revoked: false,
2092            chain_seq: 10,
2093        };
2094
2095        tm.deploy_tool(&spec, &tv).unwrap();
2096
2097        let tree = tm.tree().lock().unwrap();
2098        let node = tree.get(&ResourceId::new("/kernel/tools/fs/read_file"));
2099        assert!(node.is_some(), "tool node should exist");
2100        let node = node.unwrap();
2101        assert_eq!(node.kind, ResourceKind::Tool);
2102        assert_eq!(node.metadata["tool_version"], serde_json::json!(1));
2103        assert!(node.metadata.contains_key("gate_action"));
2104    }
2105
2106    #[test]
2107    fn tool_deploy_emits_chain_event() {
2108        let (chain, tm) = setup_tool_tree();
2109        let spec = crate::wasm_runner::builtin_tool_catalog()
2110            .into_iter()
2111            .find(|s| s.name == "fs.read_file")
2112            .unwrap();
2113        let tv = ToolVersion {
2114            version: 1,
2115            module_hash: [0xAA; 32],
2116            signature: [0xBB; 64],
2117            deployed_at: Utc::now(),
2118            revoked: false,
2119            chain_seq: 10,
2120        };
2121
2122        let before = chain.len();
2123        tm.deploy_tool(&spec, &tv).unwrap();
2124        assert!(chain.len() > before);
2125
2126        let events = chain.tail(5);
2127        assert!(
2128            events.iter().any(|e| e.kind == "tool.deploy"),
2129            "expected tool.deploy chain event"
2130        );
2131    }
2132
2133    #[test]
2134    fn tool_version_update_chain_links() {
2135        let (chain, tm) = setup_tool_tree();
2136        let spec = crate::wasm_runner::builtin_tool_catalog()
2137            .into_iter()
2138            .find(|s| s.name == "fs.read_file")
2139            .unwrap();
2140        let v1 = ToolVersion {
2141            version: 1,
2142            module_hash: [0xAA; 32],
2143            signature: [0xBB; 64],
2144            deployed_at: Utc::now(),
2145            revoked: false,
2146            chain_seq: 10,
2147        };
2148        tm.deploy_tool(&spec, &v1).unwrap();
2149
2150        let v2 = ToolVersion {
2151            version: 2,
2152            module_hash: [0xCC; 32],
2153            signature: [0xDD; 64],
2154            deployed_at: Utc::now(),
2155            revoked: false,
2156            chain_seq: 20,
2157        };
2158        tm.update_tool_version("fs.read_file", &v2).unwrap();
2159
2160        // Verify chain event
2161        let events = chain.tail(5);
2162        let update_evt = events
2163            .iter()
2164            .find(|e| e.kind == "tool.version.update")
2165            .expect("expected tool.version.update event");
2166        let payload = update_evt.payload.as_ref().unwrap();
2167        assert_eq!(payload["old_version"], 1);
2168        assert_eq!(payload["new_version"], 2);
2169
2170        // Verify tree metadata updated
2171        let tree = tm.tree().lock().unwrap();
2172        let node = tree
2173            .get(&ResourceId::new("/kernel/tools/fs/read_file"))
2174            .unwrap();
2175        assert_eq!(node.metadata["tool_version"], serde_json::json!(2));
2176    }
2177
2178    #[test]
2179    fn tool_version_revoke_marks_revoked() {
2180        let (_chain, tm) = setup_tool_tree();
2181        let spec = crate::wasm_runner::builtin_tool_catalog()
2182            .into_iter()
2183            .find(|s| s.name == "fs.read_file")
2184            .unwrap();
2185        let v1 = ToolVersion {
2186            version: 1,
2187            module_hash: [0xAA; 32],
2188            signature: [0xBB; 64],
2189            deployed_at: Utc::now(),
2190            revoked: false,
2191            chain_seq: 10,
2192        };
2193        tm.deploy_tool(&spec, &v1).unwrap();
2194
2195        tm.revoke_tool_version("fs.read_file", 1).unwrap();
2196
2197        let tree = tm.tree().lock().unwrap();
2198        let node = tree
2199            .get(&ResourceId::new("/kernel/tools/fs/read_file"))
2200            .unwrap();
2201        assert_eq!(node.metadata["v1_revoked"], serde_json::json!(true));
2202        assert!(node.metadata.contains_key("v1_revoked_at"));
2203    }
2204
2205    // --- Version history tests (K4 B2) ---
2206
2207    #[test]
2208    fn version_history_persisted_in_tree() {
2209        let (_chain, tm) = setup_tool_tree();
2210        let spec = crate::wasm_runner::builtin_tool_catalog()
2211            .into_iter()
2212            .find(|s| s.name == "fs.read_file")
2213            .unwrap();
2214
2215        let v1 = ToolVersion {
2216            version: 1,
2217            module_hash: [0xAA; 32],
2218            signature: [0xBB; 64],
2219            deployed_at: Utc::now(),
2220            revoked: false,
2221            chain_seq: 10,
2222        };
2223        tm.deploy_tool(&spec, &v1).unwrap();
2224
2225        let v2 = ToolVersion {
2226            version: 2,
2227            module_hash: [0xCC; 32],
2228            signature: [0xDD; 64],
2229            deployed_at: Utc::now(),
2230            revoked: false,
2231            chain_seq: 20,
2232        };
2233        tm.update_tool_version("fs.read_file", &v2).unwrap();
2234
2235        let versions = tm.get_tool_versions("fs.read_file");
2236        assert_eq!(versions.len(), 2, "should have 2 versions");
2237        assert_eq!(versions[0]["version"], 1);
2238        assert_eq!(versions[1]["version"], 2);
2239    }
2240
2241    #[test]
2242    fn version_history_includes_revoked() {
2243        let (_chain, tm) = setup_tool_tree();
2244        let spec = crate::wasm_runner::builtin_tool_catalog()
2245            .into_iter()
2246            .find(|s| s.name == "fs.read_file")
2247            .unwrap();
2248
2249        let v1 = ToolVersion {
2250            version: 1,
2251            module_hash: [0xAA; 32],
2252            signature: [0xBB; 64],
2253            deployed_at: Utc::now(),
2254            revoked: false,
2255            chain_seq: 10,
2256        };
2257        tm.deploy_tool(&spec, &v1).unwrap();
2258        tm.revoke_tool_version("fs.read_file", 1).unwrap();
2259
2260        let versions = tm.get_tool_versions("fs.read_file");
2261        assert_eq!(
2262            versions.len(),
2263            1,
2264            "version entry should persist after revoke"
2265        );
2266        // The revoke flag in the version array entry is as deployed (false),
2267        // but the per-version metadata key v1_revoked=true is set separately.
2268        // Version history records deploy-time state.
2269    }
2270
2271    #[test]
2272    fn tool_revoke_emits_chain_event() {
2273        let (chain, tm) = setup_tool_tree();
2274        let spec = crate::wasm_runner::builtin_tool_catalog()
2275            .into_iter()
2276            .find(|s| s.name == "fs.read_file")
2277            .unwrap();
2278        let v1 = ToolVersion {
2279            version: 1,
2280            module_hash: [0xAA; 32],
2281            signature: [0xBB; 64],
2282            deployed_at: Utc::now(),
2283            revoked: false,
2284            chain_seq: 10,
2285        };
2286        tm.deploy_tool(&spec, &v1).unwrap();
2287
2288        let before = chain.len();
2289        tm.revoke_tool_version("fs.read_file", 1).unwrap();
2290        assert!(chain.len() > before);
2291
2292        let events = chain.tail(5);
2293        assert!(
2294            events.iter().any(|e| e.kind == "tool.version.revoke"),
2295            "expected tool.version.revoke chain event"
2296        );
2297    }
2298
2299    #[test]
2300    fn snapshot_on_bootstrapped_tree() {
2301        let chain = test_chain();
2302        let tm = TreeManager::new(Arc::clone(&chain));
2303        tm.bootstrap().unwrap();
2304
2305        let snap = tm.snapshot().unwrap();
2306        assert!(snap.node_count > 0);
2307        assert!(!snap.nodes.is_empty());
2308        // The snapshot should contain at least the root + bootstrapped namespaces
2309        assert!(snap.nodes.len() >= 9);
2310    }
2311
2312    #[test]
2313    fn snapshot_root_hash_matches_stats() {
2314        let chain = test_chain();
2315        let tm = TreeManager::new(Arc::clone(&chain));
2316        tm.bootstrap().unwrap();
2317
2318        let snap = tm.snapshot().unwrap();
2319        let stats = tm.stats();
2320        assert_eq!(snap.root_hash, stats.root_hash);
2321    }
2322
2323    #[test]
2324    fn apply_remote_mutation_records_in_log() {
2325        let chain = test_chain();
2326        let tm = TreeManager::new(Arc::clone(&chain));
2327        tm.bootstrap().unwrap();
2328
2329        let before = tm.mutation_log().lock().unwrap().len();
2330
2331        let event = MutationEvent::Create {
2332            id: ResourceId::new("/kernel/services/remote_svc"),
2333            kind: ResourceKind::Service,
2334            parent: ResourceId::new("/kernel/services"),
2335            timestamp: Utc::now(),
2336            signature: None,
2337        };
2338        tm.apply_remote_mutation(event).unwrap();
2339
2340        let after = tm.mutation_log().lock().unwrap().len();
2341        assert_eq!(after, before + 1);
2342
2343        // The node should exist in the tree
2344        let tree = tm.tree().lock().unwrap();
2345        assert!(
2346            tree.get(&ResourceId::new("/kernel/services/remote_svc"))
2347                .is_some()
2348        );
2349    }
2350
2351    #[test]
2352    fn mutations_signed_when_key_set() {
2353        let chain = test_chain();
2354        let mut tm = TreeManager::new(Arc::clone(&chain));
2355        let key = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
2356        tm.set_signing_key(key.clone());
2357        tm.bootstrap().unwrap();
2358
2359        // Bootstrap mutations should all be signed
2360        {
2361            let log = tm.mutation_log().lock().unwrap();
2362            for evt in log.events() {
2363                if let MutationEvent::Create { signature, .. } = evt {
2364                    assert!(signature.is_some(), "bootstrap Create should be signed");
2365                    assert_eq!(signature.as_ref().unwrap().len(), 64);
2366                }
2367            }
2368        }
2369
2370        // Insert should produce signed mutation
2371        tm.insert(
2372            ResourceId::new("/kernel/services/signed_svc"),
2373            ResourceKind::Service,
2374            ResourceId::new("/kernel/services"),
2375        )
2376        .unwrap();
2377
2378        let log = tm.mutation_log().lock().unwrap();
2379        let last = log.events().last().unwrap();
2380        match last {
2381            MutationEvent::Create { signature, .. } => {
2382                let sig_bytes = signature.as_ref().expect("insert should be signed");
2383                assert_eq!(sig_bytes.len(), 64);
2384                // Verify the signature bytes form a valid Ed25519 signature
2385                let sig =
2386                    ed25519_dalek::Signature::from_bytes(sig_bytes.as_slice().try_into().unwrap());
2387                assert_eq!(sig.to_bytes().len(), 64);
2388            }
2389            _ => panic!("expected Create variant"),
2390        }
2391    }
2392
2393    #[test]
2394    fn mutations_unsigned_without_key() {
2395        let chain = test_chain();
2396        let tm = TreeManager::new(Arc::clone(&chain));
2397        tm.bootstrap().unwrap();
2398
2399        tm.insert(
2400            ResourceId::new("/kernel/services/unsigned_svc"),
2401            ResourceKind::Service,
2402            ResourceId::new("/kernel/services"),
2403        )
2404        .unwrap();
2405
2406        let log = tm.mutation_log().lock().unwrap();
2407        // All mutations should have signature = None when no key is set
2408        for evt in log.events() {
2409            if let MutationEvent::Create { signature, .. } = evt {
2410                assert!(signature.is_none(), "should be None without signing key");
2411            }
2412        }
2413    }
2414
2415    /// WEFT-144: valid signature applies; tampered rejects; replay rejects.
2416    #[test]
2417    fn remote_mutation_signature_valid_tampered_replay() {
2418        use ed25519_dalek::Signer;
2419
2420        let chain = test_chain();
2421        let mut tm = TreeManager::new(Arc::clone(&chain));
2422        let key = ed25519_dalek::SigningKey::from_bytes(&[11u8; 32]);
2423        let vk = key.verifying_key();
2424        tm.set_signing_key(key.clone());
2425        tm.bootstrap().unwrap();
2426
2427        let ts = Utc::now();
2428        let path = "/kernel/services/verified_svc";
2429        let canonical = TreeManager::mutation_canonical_bytes("create", path, &ts);
2430        let sig = key.sign(&canonical).to_bytes().to_vec();
2431
2432        let event = MutationEvent::Create {
2433            id: ResourceId::new(path),
2434            kind: ResourceKind::Service,
2435            parent: ResourceId::new("/kernel/services"),
2436            timestamp: ts,
2437            signature: Some(sig.clone()),
2438        };
2439
2440        // Valid signature applies.
2441        tm.apply_remote_mutation_verified(event.clone(), &vk)
2442            .unwrap();
2443        {
2444            let tree = tm.tree().lock().unwrap();
2445            assert!(tree.get(&ResourceId::new(path)).is_some());
2446        }
2447
2448        // Replay of the exact same event is rejected.
2449        let replay_err = tm
2450            .apply_remote_mutation_verified(event.clone(), &vk)
2451            .unwrap_err();
2452        assert!(
2453            replay_err.to_string().contains("replay"),
2454            "expected replay reject, got {replay_err}"
2455        );
2456
2457        // Tampered signature rejected.
2458        let mut bad_sig = sig;
2459        bad_sig[0] ^= 0xFF;
2460        let tampered = MutationEvent::Create {
2461            id: ResourceId::new("/kernel/services/tampered_svc"),
2462            kind: ResourceKind::Service,
2463            parent: ResourceId::new("/kernel/services"),
2464            timestamp: ts,
2465            signature: Some(bad_sig),
2466        };
2467        let err = tm
2468            .apply_remote_mutation_verified(tampered, &vk)
2469            .unwrap_err();
2470        assert!(
2471            err.to_string().contains("verify") || err.to_string().contains("signature"),
2472            "expected verify failure, got {err}"
2473        );
2474
2475        // Missing signature rejected by verified path.
2476        let unsigned = MutationEvent::Create {
2477            id: ResourceId::new("/kernel/services/unsigned_remote"),
2478            kind: ResourceKind::Service,
2479            parent: ResourceId::new("/kernel/services"),
2480            timestamp: Utc::now(),
2481            signature: None,
2482        };
2483        let err = tm
2484            .apply_remote_mutation_verified(unsigned, &vk)
2485            .unwrap_err();
2486        assert!(err.to_string().contains("no signature"));
2487    }
2488
2489    /// WEFT-106: two-node tree Merkle diff + signed mutation catch-up.
2490    #[test]
2491    fn two_node_tree_diff_signed_convergence() {
2492        let chain_a = test_chain();
2493        let chain_b = test_chain();
2494        let mut node_a = TreeManager::new(Arc::clone(&chain_a));
2495        let node_b = TreeManager::new(Arc::clone(&chain_b));
2496
2497        let key = ed25519_dalek::SigningKey::from_bytes(&[42u8; 32]);
2498        let vk = key.verifying_key();
2499        node_a.set_signing_key(key);
2500
2501        node_a.bootstrap().unwrap();
2502        node_b.bootstrap().unwrap();
2503
2504        // Shared baseline roots match.
2505        {
2506            let ha = node_a.tree().lock().unwrap().root_hash();
2507            let hb = node_b.tree().lock().unwrap().root_hash();
2508            assert_eq!(ha, hb);
2509        }
2510
2511        // Node A mutates: insert + meta update (both signed).
2512        node_a
2513            .insert(
2514                ResourceId::new("/kernel/services/mesh_sync_svc"),
2515                ResourceKind::Service,
2516                ResourceId::new("/kernel/services"),
2517            )
2518            .unwrap();
2519        node_a
2520            .update_meta(
2521                &ResourceId::new("/kernel/services/mesh_sync_svc"),
2522                "version",
2523                serde_json::json!("1.0.0"),
2524            )
2525            .unwrap();
2526
2527        // Diff is non-empty; payload is minimal (only the new service node as
2528        // Added — ancestors that differ solely via children are not emitted).
2529        // O(changed) walk cost is covered by exo-resource-tree unit tests on
2530        // wide trees (WEFT-106).
2531        let diff = node_a.merkle_diff(&node_b).unwrap();
2532        assert!(!diff.in_sync());
2533        let added: Vec<_> = diff
2534            .entries
2535            .iter()
2536            .filter(|e| e.diff_kind == MerkleDiffKind::Added)
2537            .collect();
2538        assert_eq!(
2539            added.len(),
2540            1,
2541            "expected single Added entry, got {:?}",
2542            diff.entries
2543                .iter()
2544                .map(|e| (e.id.to_string(), e.diff_kind))
2545                .collect::<Vec<_>>()
2546        );
2547        assert_eq!(
2548            added[0].id,
2549            ResourceId::new("/kernel/services/mesh_sync_svc")
2550        );
2551
2552        // Inclusion proof for the new node verifies under A's root.
2553        let proof = node_a
2554            .merkle_proof(&ResourceId::new("/kernel/services/mesh_sync_svc"))
2555            .unwrap()
2556            .expect("proof");
2557        assert!(proof.verify());
2558        assert!(node_a.verify_merkle_proof(&proof).unwrap());
2559
2560        // Ship signed events for the diff paths and apply on B.
2561        let events = node_a.signed_events_for_diff(&diff).unwrap();
2562        assert!(
2563            !events.is_empty(),
2564            "expected signed events for diff paths"
2565        );
2566        for e in &events {
2567            match e {
2568                MutationEvent::Create { signature, .. }
2569                | MutationEvent::UpdateMeta { signature, .. } => {
2570                    assert!(signature.is_some(), "event must be signed");
2571                }
2572                _ => {}
2573            }
2574        }
2575
2576        let applied = node_b
2577            .apply_merkle_diff_verified(&diff, &events, &vk)
2578            .unwrap();
2579        assert!(applied >= 1, "applied {applied}");
2580
2581        // Roots converge.
2582        let ha = node_a.tree().lock().unwrap().root_hash();
2583        let hb = node_b.tree().lock().unwrap().root_hash();
2584        assert_eq!(ha, hb, "roots must converge after signed diff apply");
2585
2586        // B now has the service node.
2587        assert!(
2588            node_b
2589                .tree()
2590                .lock()
2591                .unwrap()
2592                .get(&ResourceId::new("/kernel/services/mesh_sync_svc"))
2593                .is_some()
2594        );
2595
2596        // Second diff is empty.
2597        let diff2 = node_a.merkle_diff(&node_b).unwrap();
2598        assert!(diff2.in_sync());
2599
2600        // Mesh wire response builds cleanly.
2601        #[cfg(feature = "mesh")]
2602        {
2603            let resp = node_a.tree_sync_response_from_diff(&diff).unwrap();
2604            assert!(!resp.in_sync || !resp.diff_nodes.is_empty() || !resp.deleted_paths.is_empty()
2605                || diff.changed_count() == 0);
2606            // Original pre-apply diff had content:
2607            assert!(!diff.in_sync());
2608            assert!(!resp.diff_nodes.is_empty() || !resp.deleted_paths.is_empty());
2609        }
2610    }
2611
2612    #[test]
2613    fn merkle_diff_identical_managers_in_sync() {
2614        let chain = test_chain();
2615        let a = TreeManager::new(Arc::clone(&chain));
2616        let b = TreeManager::new(test_chain());
2617        a.bootstrap().unwrap();
2618        b.bootstrap().unwrap();
2619        let diff = a.merkle_diff(&b).unwrap();
2620        assert!(diff.in_sync());
2621        assert_eq!(diff.nodes_visited, 1);
2622    }
2623}