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    MutationEvent, MutationLog, NodeScoring, ResourceId, ResourceKind, ResourceTree,
21};
22
23use crate::capability::AgentCapabilities;
24use crate::chain::ChainManager;
25use crate::process::Pid;
26use crate::wasm_runner::{BuiltinToolSpec, ToolVersion, compute_module_hash};
27
28/// Statistics snapshot from the tree manager.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct TreeStats {
31    /// Number of nodes in the resource tree.
32    pub node_count: usize,
33    /// Number of mutation events recorded.
34    pub mutation_count: usize,
35    /// Hex-encoded root hash of the tree.
36    pub root_hash: String,
37}
38
39/// Serializable snapshot of the full tree state for cross-node sync.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct TreeSnapshot {
42    /// Hex-encoded Merkle root hash at snapshot time.
43    pub root_hash: String,
44    /// Number of nodes in the tree.
45    pub node_count: usize,
46    /// Serialized tree state (all nodes and their metadata).
47    pub nodes: Vec<TreeNodeSnapshot>,
48    /// Timestamp when snapshot was taken.
49    pub taken_at: chrono::DateTime<chrono::Utc>,
50}
51
52/// Snapshot of a single tree node.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct TreeNodeSnapshot {
55    /// Resource path (e.g., "/kernel/services/health").
56    pub path: String,
57    /// Resource kind.
58    pub kind: String,
59    /// Metadata key-value pairs.
60    pub metadata: std::collections::HashMap<String, String>,
61    /// Hex-encoded node hash.
62    pub hash: String,
63}
64
65/// Unified facade over ResourceTree + MutationLog + ChainManager.
66///
67/// Every mutating operation on the tree also:
68/// 1. Appends a [`MutationEvent`] to the mutation log
69/// 2. Appends a [`ChainEvent`](crate::chain::ChainEvent) to the chain
70/// 3. Stores `chain_seq` metadata on the affected tree node
71pub struct TreeManager {
72    tree: Mutex<ResourceTree>,
73    mutation_log: Mutex<MutationLog>,
74    chain: Arc<ChainManager>,
75    /// Optional Ed25519 signing key for mutation signatures.
76    #[cfg(feature = "exochain")]
77    signing_key: Option<ed25519_dalek::SigningKey>,
78}
79
80impl TreeManager {
81    /// Create a new TreeManager with an empty tree and mutation log.
82    pub fn new(chain: Arc<ChainManager>) -> Self {
83        Self {
84            tree: Mutex::new(ResourceTree::new()),
85            mutation_log: Mutex::new(MutationLog::new()),
86            chain,
87            #[cfg(feature = "exochain")]
88            signing_key: None,
89        }
90    }
91
92    /// Set the Ed25519 signing key for signing tree mutations.
93    /// When set, all mutations will have their signature field populated.
94    #[cfg(feature = "exochain")]
95    pub fn set_signing_key(&mut self, key: ed25519_dalek::SigningKey) {
96        self.signing_key = Some(key);
97    }
98
99    /// Sign arbitrary bytes with the configured signing key, if present.
100    /// Returns `None` when no key is configured.
101    #[cfg(feature = "exochain")]
102    fn sign_bytes(&self, data: &[u8]) -> Option<Vec<u8>> {
103        use ed25519_dalek::Signer;
104        self.signing_key.as_ref().map(|k| k.sign(data).to_bytes().to_vec())
105    }
106
107    /// Build the canonical bytes for a mutation signature.
108    ///
109    /// Format: `"<operation>|<path>|<timestamp_rfc3339>"` encoded as UTF-8.
110    #[cfg(feature = "exochain")]
111    fn mutation_signature(
112        &self,
113        operation: &str,
114        path: &str,
115        timestamp: &chrono::DateTime<Utc>,
116    ) -> Option<Vec<u8>> {
117        let canonical = format!("{operation}|{path}|{}", timestamp.to_rfc3339());
118        self.sign_bytes(canonical.as_bytes())
119    }
120
121    /// Bootstrap the tree with well-known WeftOS namespaces.
122    ///
123    /// Creates the standard namespace hierarchy (`/kernel`, `/kernel/services`,
124    /// etc.), logs a MutationEvent::Create for each bootstrapped node, and
125    /// appends a `tree.bootstrap` chain event with node count and root hash.
126    pub fn bootstrap(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
127        let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
128        exo_resource_tree::bootstrap_fresh(&mut tree)?;
129
130        // Log mutation events for each bootstrapped node (skip root, it's pre-existing)
131        let mut log = self.mutation_log.lock().map_err(|e| format!("log lock: {e}"))?;
132        let bootstrapped_paths = [
133            "/kernel",
134            "/kernel/services",
135            "/kernel/processes",
136            "/kernel/agents",
137            "/network",
138            "/network/peers",
139            "/apps",
140            "/environments",
141        ];
142        for path in &bootstrapped_paths {
143            let rid = ResourceId::new(*path);
144            let kind = ResourceKind::Namespace;
145            let parent = rid
146                .parent()
147                .unwrap_or_else(ResourceId::root);
148            let now = Utc::now();
149            #[cfg(feature = "exochain")]
150            let sig = self.mutation_signature("create", path, &now);
151            #[cfg(not(feature = "exochain"))]
152            let sig = None;
153            log.append(MutationEvent::Create {
154                id: rid,
155                kind,
156                parent,
157                timestamp: now,
158                signature: sig,
159            });
160        }
161
162        // Chain event
163        let hash_hex = hex_hash(&tree.root_hash());
164        self.chain.append(
165            "tree",
166            "bootstrap",
167            Some(serde_json::json!({
168                "node_count": tree.len(),
169                "root_hash": hash_hex,
170            })),
171        );
172
173        debug!(nodes = tree.len(), "tree bootstrapped with chain event");
174        Ok(())
175    }
176
177    /// Insert a new resource node into the tree.
178    ///
179    /// Creates the tree node, appends a MutationEvent::Create, appends a
180    /// `tree.insert` chain event, and stores the chain sequence number as
181    /// metadata on the node for two-way traceability.
182    pub fn insert(
183        &self,
184        id: ResourceId,
185        kind: ResourceKind,
186        parent: ResourceId,
187    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
188        let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
189        tree.insert(id.clone(), kind.clone(), parent.clone())?;
190
191        // Chain event (before recompute so we capture pre-mutation state reference)
192        let chain_event = self.chain.append(
193            "tree",
194            "insert",
195            Some(serde_json::json!({
196                "path": id.to_string(),
197                "kind": format!("{kind:?}"),
198                "parent": parent.to_string(),
199            })),
200        );
201
202        // Store chain_seq metadata on the node for traceability
203        if let Some(node) = tree.get_mut(&id) {
204            node.metadata
205                .insert("chain_seq".to_string(), serde_json::json!(chain_event.sequence));
206        }
207
208        // Recompute Merkle hashes
209        tree.recompute_all();
210
211        // Mutation log
212        let now = Utc::now();
213        #[cfg(feature = "exochain")]
214        let sig = self.mutation_signature("create", &id.to_string(), &now);
215        #[cfg(not(feature = "exochain"))]
216        let sig = None;
217        let mut log = self.mutation_log.lock().map_err(|e| format!("log lock: {e}"))?;
218        log.append(MutationEvent::Create {
219            id,
220            kind,
221            parent,
222            timestamp: now,
223            signature: sig,
224        });
225
226        Ok(())
227    }
228
229    /// Remove a leaf node from the tree.
230    ///
231    /// Removes the node, appends a MutationEvent::Remove and a
232    /// `tree.remove` chain event.
233    pub fn remove(
234        &self,
235        id: ResourceId,
236    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
237        let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
238        tree.remove(id.clone())?;
239        tree.recompute_all();
240
241        self.chain.append(
242            "tree",
243            "remove",
244            Some(serde_json::json!({
245                "path": id.to_string(),
246            })),
247        );
248
249        let now = Utc::now();
250        #[cfg(feature = "exochain")]
251        let sig = self.mutation_signature("remove", &id.to_string(), &now);
252        #[cfg(not(feature = "exochain"))]
253        let sig = None;
254        let mut log = self.mutation_log.lock().map_err(|e| format!("log lock: {e}"))?;
255        log.append(MutationEvent::Remove {
256            id,
257            timestamp: now,
258            signature: sig,
259        });
260
261        Ok(())
262    }
263
264    /// Update metadata on a resource node.
265    ///
266    /// Sets the key-value pair on the node, appends a MutationEvent::UpdateMeta
267    /// and a `tree.update_meta` chain event.
268    pub fn update_meta(
269        &self,
270        id: &ResourceId,
271        key: &str,
272        value: serde_json::Value,
273    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
274        let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
275        let node = tree
276            .get_mut(id)
277            .ok_or_else(|| format!("node not found: {id}"))?;
278        node.metadata.insert(key.to_string(), value.clone());
279        node.updated_at = Utc::now();
280        tree.recompute_all();
281
282        self.chain.append(
283            "tree",
284            "update_meta",
285            Some(serde_json::json!({
286                "path": id.to_string(),
287                "key": key,
288            })),
289        );
290
291        let now = Utc::now();
292        #[cfg(feature = "exochain")]
293        let sig = self.mutation_signature("update_meta", &id.to_string(), &now);
294        #[cfg(not(feature = "exochain"))]
295        let sig = None;
296        let mut log = self.mutation_log.lock().map_err(|e| format!("log lock: {e}"))?;
297        log.append(MutationEvent::UpdateMeta {
298            id: id.clone(),
299            key: key.to_string(),
300            value: Some(value),
301            timestamp: now,
302            signature: sig,
303        });
304
305        Ok(())
306    }
307
308    /// Register a service in the tree, creating `/kernel/services/{name}`.
309    ///
310    /// This is the unified path for service registration: it inserts the tree
311    /// node AND creates the chain event atomically.
312    pub fn register_service(
313        &self,
314        name: &str,
315    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
316        let service_id = ResourceId::new(format!("/kernel/services/{name}"));
317        let parent = ResourceId::new("/kernel/services");
318        self.insert(service_id, ResourceKind::Service, parent)
319    }
320
321    /// Register a service with a manifest chain event.
322    ///
323    /// Inserts the tree node and emits an additional `service.manifest`
324    /// chain event with structured metadata (name, type, tree path,
325    /// registration time). This produces an RVF-auditable registration
326    /// when the chain is persisted as RVF segments.
327    pub fn register_service_with_manifest(
328        &self,
329        name: &str,
330        service_type: &str,
331    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
332        self.register_service(name)?;
333
334        self.chain.append(
335            "service",
336            "service.manifest",
337            Some(serde_json::json!({
338                "name": name,
339                "service_type": service_type,
340                "tree_path": format!("/kernel/services/{name}"),
341                "registered_at": Utc::now().to_rfc3339(),
342            })),
343        );
344
345        Ok(())
346    }
347
348    /// Register an agent in the tree, creating `/kernel/agents/{agent_id}`.
349    ///
350    /// Creates the tree node with kind `Agent`, sets metadata (pid, state,
351    /// spawn_time), and emits an `agent.spawn` chain event.
352    pub fn register_agent(
353        &self,
354        agent_id: &str,
355        pid: Pid,
356        caps: &AgentCapabilities,
357    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
358        let agent_rid = ResourceId::new(format!("/kernel/agents/{agent_id}"));
359        let parent = ResourceId::new("/kernel/agents");
360
361        // Insert tree node
362        {
363            let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
364            tree.insert(agent_rid.clone(), ResourceKind::Agent, parent.clone())?;
365
366            // Set metadata
367            if let Some(node) = tree.get_mut(&agent_rid) {
368                node.metadata.insert("pid".into(), serde_json::json!(pid));
369                node.metadata
370                    .insert("state".into(), serde_json::json!("starting"));
371                node.metadata
372                    .insert("spawn_time".into(), serde_json::json!(Utc::now().to_rfc3339()));
373                node.metadata
374                    .insert("can_spawn".into(), serde_json::json!(caps.can_spawn));
375                node.metadata
376                    .insert("can_ipc".into(), serde_json::json!(caps.can_ipc));
377                node.metadata
378                    .insert("can_exec_tools".into(), serde_json::json!(caps.can_exec_tools));
379            }
380            tree.recompute_all();
381        }
382
383        // Chain event
384        let chain_event = self.chain.append(
385            "agent",
386            "agent.spawn",
387            Some(serde_json::json!({
388                "agent_id": agent_id,
389                "pid": pid,
390                "capabilities": {
391                    "can_spawn": caps.can_spawn,
392                    "can_ipc": caps.can_ipc,
393                    "can_exec_tools": caps.can_exec_tools,
394                    "can_network": caps.can_network,
395                },
396            })),
397        );
398
399        // Store chain_seq
400        {
401            let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
402            if let Some(node) = tree.get_mut(&agent_rid) {
403                node.metadata
404                    .insert("chain_seq".into(), serde_json::json!(chain_event.sequence));
405            }
406        }
407
408        // Mutation log
409        let now = Utc::now();
410        #[cfg(feature = "exochain")]
411        let sig = self.mutation_signature("create", &agent_rid.to_string(), &now);
412        #[cfg(not(feature = "exochain"))]
413        let sig = None;
414        let mut log = self
415            .mutation_log
416            .lock()
417            .map_err(|e| format!("log lock: {e}"))?;
418        log.append(MutationEvent::Create {
419            id: agent_rid,
420            kind: ResourceKind::Agent,
421            parent,
422            timestamp: now,
423            signature: sig,
424        });
425
426        debug!(agent_id, pid, "agent registered in tree");
427        Ok(())
428    }
429
430    /// Unregister an agent from the tree.
431    ///
432    /// Updates the tree node metadata (state=exited, exit_code, stop_time)
433    /// and emits an `agent.stop` chain event. Does NOT remove the node
434    /// (preserves audit trail).
435    pub fn unregister_agent(
436        &self,
437        agent_id: &str,
438        pid: Pid,
439        exit_code: i32,
440    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
441        let agent_rid = ResourceId::new(format!("/kernel/agents/{agent_id}"));
442
443        {
444            let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
445            if let Some(node) = tree.get_mut(&agent_rid) {
446                node.metadata
447                    .insert("state".into(), serde_json::json!("exited"));
448                node.metadata
449                    .insert("exit_code".into(), serde_json::json!(exit_code));
450                node.metadata
451                    .insert("stop_time".into(), serde_json::json!(Utc::now().to_rfc3339()));
452                node.updated_at = Utc::now();
453            }
454            tree.recompute_all();
455        }
456
457        self.chain.append(
458            "agent",
459            "agent.stop",
460            Some(serde_json::json!({
461                "agent_id": agent_id,
462                "pid": pid,
463                "exit_code": exit_code,
464            })),
465        );
466
467        let now = Utc::now();
468        #[cfg(feature = "exochain")]
469        let sig = self.mutation_signature("update_meta", &agent_rid.to_string(), &now);
470        #[cfg(not(feature = "exochain"))]
471        let sig = None;
472        let mut log = self
473            .mutation_log
474            .lock()
475            .map_err(|e| format!("log lock: {e}"))?;
476        log.append(MutationEvent::UpdateMeta {
477            id: agent_rid,
478            key: "state".into(),
479            value: Some(serde_json::json!("exited")),
480            timestamp: now,
481            signature: sig,
482        });
483
484        debug!(agent_id, pid, exit_code, "agent unregistered in tree");
485        Ok(())
486    }
487
488    /// Update an agent's state in the tree.
489    pub fn update_agent_state(
490        &self,
491        agent_id: &str,
492        state: &str,
493    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
494        let agent_rid = ResourceId::new(format!("/kernel/agents/{agent_id}"));
495        self.update_meta(&agent_rid, "state", serde_json::json!(state))
496    }
497
498    /// Get direct access to the resource tree (for read-only queries).
499    pub fn tree(&self) -> &Mutex<ResourceTree> {
500        &self.tree
501    }
502
503    /// Get direct access to the mutation log.
504    pub fn mutation_log(&self) -> &Mutex<MutationLog> {
505        &self.mutation_log
506    }
507
508    /// Get the chain manager.
509    pub fn chain(&self) -> &Arc<ChainManager> {
510        &self.chain
511    }
512
513    /// Get the root hash of the resource tree.
514    pub fn root_hash(&self) -> [u8; 32] {
515        self.tree
516            .lock()
517            .map(|t| t.root_hash())
518            .unwrap_or([0u8; 32])
519    }
520
521    /// Get a statistics snapshot.
522    pub fn stats(&self) -> TreeStats {
523        let tree = self.tree.lock().unwrap();
524        let log = self.mutation_log.lock().unwrap();
525        TreeStats {
526            node_count: tree.len(),
527            mutation_count: log.len(),
528            root_hash: hex_hash(&tree.root_hash()),
529        }
530    }
531
532    // --- Scoring API ---
533
534    /// Set the scoring vector for a node.
535    ///
536    /// Updates the tree, logs a MutationEvent::UpdateScoring, and emits
537    /// a `scoring.update` chain event.
538    pub fn update_scoring(
539        &self,
540        id: &ResourceId,
541        scoring: NodeScoring,
542    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
543        let old = {
544            let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
545            tree
546                .update_scoring(id, scoring)
547                .ok_or_else(|| format!("node not found: {id}"))?
548        };
549
550        self.chain.append(
551            "scoring",
552            "scoring.update",
553            Some(serde_json::json!({
554                "path": id.to_string(),
555                "old": old.as_array(),
556                "new": scoring.as_array(),
557            })),
558        );
559
560        let now = Utc::now();
561        #[cfg(feature = "exochain")]
562        let sig = self.mutation_signature("update_scoring", &id.to_string(), &now);
563        #[cfg(not(feature = "exochain"))]
564        let sig = None;
565        let mut log = self.mutation_log.lock().map_err(|e| format!("log lock: {e}"))?;
566        log.append(MutationEvent::UpdateScoring {
567            id: id.clone(),
568            old,
569            new: scoring,
570            timestamp: now,
571            signature: sig,
572        });
573
574        debug!(path = %id, "scoring updated");
575        Ok(())
576    }
577
578    /// EMA-blend an observation into a node's scoring.
579    ///
580    /// Emits a `scoring.blend` chain event.
581    pub fn blend_scoring(
582        &self,
583        id: &ResourceId,
584        observation: &NodeScoring,
585        alpha: f32,
586    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
587        let (old, new) = {
588            let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
589            let node = tree
590                .get(id)
591                .ok_or_else(|| format!("node not found: {id}"))?;
592            let old = node.scoring;
593
594            tree.blend_scoring(id, observation, alpha);
595
596            let node = tree
597                .get(id)
598                .ok_or_else(|| format!("node not found: {id}"))?;
599            let new = node.scoring;
600            (old, new)
601        };
602
603        self.chain.append(
604            "scoring",
605            "scoring.blend",
606            Some(serde_json::json!({
607                "path": id.to_string(),
608                "alpha": alpha,
609                "old": old.as_array(),
610                "new": new.as_array(),
611            })),
612        );
613
614        let now = Utc::now();
615        #[cfg(feature = "exochain")]
616        let sig = self.mutation_signature("update_scoring", &id.to_string(), &now);
617        #[cfg(not(feature = "exochain"))]
618        let sig = None;
619        let mut log = self.mutation_log.lock().map_err(|e| format!("log lock: {e}"))?;
620        log.append(MutationEvent::UpdateScoring {
621            id: id.clone(),
622            old,
623            new,
624            timestamp: now,
625            signature: sig,
626        });
627
628        debug!(path = %id, alpha, "scoring blended");
629        Ok(())
630    }
631
632    /// Get the scoring vector for a node.
633    pub fn get_scoring(
634        &self,
635        id: &ResourceId,
636    ) -> Option<NodeScoring> {
637        let tree = self.tree.lock().ok()?;
638        tree.get(id).map(|n| n.scoring)
639    }
640
641    /// Find nodes most similar to a target node by cosine similarity.
642    ///
643    /// Returns up to `count` `(ResourceId, similarity)` pairs sorted by
644    /// descending similarity.
645    pub fn find_similar(
646        &self,
647        target_id: &ResourceId,
648        count: usize,
649    ) -> Vec<(ResourceId, f32)> {
650        let tree = match self.tree.lock() {
651            Ok(t) => t,
652            Err(_) => return Vec::new(),
653        };
654        let target = match tree.get(target_id) {
655            Some(n) => n.scoring,
656            None => return Vec::new(),
657        };
658
659        let mut scored: Vec<(ResourceId, f32)> = tree
660            .iter()
661            .filter(|(id, _)| *id != target_id)
662            .map(|(id, node)| (id.clone(), target.cosine_similarity(&node.scoring)))
663            .collect();
664
665        scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
666        scored.truncate(count);
667        scored
668    }
669
670    /// Rank all nodes by weighted score.
671    ///
672    /// Returns up to `count` `(ResourceId, weighted_score)` pairs sorted
673    /// by descending score.
674    pub fn rank_by_score(
675        &self,
676        weights: &[f32; 6],
677        count: usize,
678    ) -> Vec<(ResourceId, f32)> {
679        let tree = match self.tree.lock() {
680            Ok(t) => t,
681            Err(_) => return Vec::new(),
682        };
683
684        let mut scored: Vec<(ResourceId, f32)> = tree
685            .iter()
686            .map(|(id, node)| (id.clone(), node.scoring.weighted_score(weights)))
687            .collect();
688
689        scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
690        scored.truncate(count);
691        scored
692    }
693
694    // --- Tool lifecycle API ---
695
696    /// Build a tool: validate WASM bytes, compute hash, sign with Ed25519.
697    ///
698    /// Returns a `ToolVersion` with the computed module hash and Ed25519
699    /// signature. Emits a `tool.build` chain event.
700    pub fn build_tool(
701        &self,
702        name: &str,
703        wasm_bytes: &[u8],
704        signing_key: &ed25519_dalek::SigningKey,
705    ) -> Result<ToolVersion, Box<dyn std::error::Error + Send + Sync>> {
706        let module_hash = compute_module_hash(wasm_bytes);
707
708        use ed25519_dalek::Signer;
709        let signature = signing_key.sign(&module_hash);
710        let sig_bytes: [u8; 64] = signature.to_bytes();
711
712        let chain_event = self.chain.append(
713            "tool",
714            "tool.build",
715            Some(serde_json::json!({
716                "name": name,
717                "module_hash": hex_hash(&module_hash),
718                "sig_algo": "Ed25519",
719            })),
720        );
721
722        let version = ToolVersion {
723            version: 1,
724            module_hash,
725            signature: sig_bytes,
726            deployed_at: Utc::now(),
727            revoked: false,
728            chain_seq: chain_event.sequence,
729        };
730
731        debug!(name, "tool built with hash and signature");
732        Ok(version)
733    }
734
735    /// Deploy a tool to the resource tree.
736    ///
737    /// Creates the tool node at `/kernel/tools/{category}/{name}`,
738    /// stores version metadata, and emits a `tool.deploy` chain event.
739    pub fn deploy_tool(
740        &self,
741        spec: &BuiltinToolSpec,
742        version: &ToolVersion,
743    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
744        let cat = match spec.category {
745            crate::wasm_runner::ToolCategory::Filesystem => "fs",
746            crate::wasm_runner::ToolCategory::Agent => "agent",
747            crate::wasm_runner::ToolCategory::System => "sys",
748            crate::wasm_runner::ToolCategory::Ecc => "ecc",
749            crate::wasm_runner::ToolCategory::User => "user",
750        };
751
752        // Ensure category namespace exists
753        let cat_path = format!("/kernel/tools/{cat}");
754        let cat_rid = ResourceId::new(&cat_path);
755        {
756            let tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
757            if tree.get(&cat_rid).is_none() {
758                drop(tree);
759                self.insert(
760                    cat_rid,
761                    ResourceKind::Namespace,
762                    ResourceId::new("/kernel/tools"),
763                )?;
764            }
765        }
766
767        // Tool short name (e.g. "read_file" from "fs.read_file")
768        let short_name = spec.name.rsplit('.').next().unwrap_or(&spec.name);
769        let tool_path = format!("/kernel/tools/{cat}/{short_name}");
770        let tool_rid = ResourceId::new(&tool_path);
771
772        // Insert the tool node
773        self.insert(
774            tool_rid.clone(),
775            ResourceKind::Tool,
776            ResourceId::new(&cat_path),
777        )?;
778
779        // Set metadata
780        self.update_meta(&tool_rid, "tool_version", serde_json::json!(version.version))?;
781        self.update_meta(
782            &tool_rid,
783            "module_hash",
784            serde_json::json!(hex_hash(&version.module_hash)),
785        )?;
786        self.update_meta(&tool_rid, "gate_action", serde_json::json!(&spec.gate_action))?;
787        self.update_meta(
788            &tool_rid,
789            "deployed_at",
790            serde_json::json!(version.deployed_at.to_rfc3339()),
791        )?;
792
793        // K4 B2: Persist version history array in tree metadata
794        let versions_array = serde_json::json!([{
795            "version": version.version,
796            "module_hash": hex_hash(&version.module_hash),
797            "deployed_at": version.deployed_at.to_rfc3339(),
798            "revoked": version.revoked,
799            "chain_seq": version.chain_seq,
800        }]);
801        self.update_meta(&tool_rid, "versions", versions_array)?;
802
803        self.chain.append(
804            "tool",
805            "tool.deploy",
806            Some(serde_json::json!({
807                "name": spec.name,
808                "version": version.version,
809                "tree_path": tool_path,
810                "module_hash": hex_hash(&version.module_hash),
811                "gate_action": spec.gate_action,
812            })),
813        );
814
815        debug!(tool = %spec.name, version = version.version, "tool deployed");
816        Ok(())
817    }
818
819    /// Update a tool to a new version.
820    ///
821    /// Updates the tool node's metadata with new version info and
822    /// emits a `tool.version.update` chain event linking old to new.
823    pub fn update_tool_version(
824        &self,
825        name: &str,
826        new_version: &ToolVersion,
827    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
828        let parts: Vec<&str> = name.splitn(2, '.').collect();
829        if parts.len() != 2 {
830            return Err(format!("invalid tool name: {name}").into());
831        }
832        let tool_path = format!("/kernel/tools/{}/{}", parts[0], parts[1]);
833        let tool_rid = ResourceId::new(&tool_path);
834
835        // Get old version info from metadata
836        let (old_version, old_hash) = {
837            let tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
838            let node = tree
839                .get(&tool_rid)
840                .ok_or_else(|| format!("tool not found: {tool_path}"))?;
841            let ver = node
842                .metadata
843                .get("tool_version")
844                .and_then(|v| v.as_u64())
845                .unwrap_or(0) as u32;
846            let hash = node
847                .metadata
848                .get("module_hash")
849                .and_then(|v| v.as_str())
850                .unwrap_or("")
851                .to_string();
852            (ver, hash)
853        };
854
855        self.update_meta(
856            &tool_rid,
857            "tool_version",
858            serde_json::json!(new_version.version),
859        )?;
860        self.update_meta(
861            &tool_rid,
862            "module_hash",
863            serde_json::json!(hex_hash(&new_version.module_hash)),
864        )?;
865        self.update_meta(
866            &tool_rid,
867            "deployed_at",
868            serde_json::json!(new_version.deployed_at.to_rfc3339()),
869        )?;
870
871        // K4 B2: Append to version history array
872        {
873            let tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
874            let node = tree
875                .get(&tool_rid)
876                .ok_or_else(|| format!("tool not found: {tool_path}"))?;
877            let mut versions: Vec<serde_json::Value> = node
878                .metadata
879                .get("versions")
880                .and_then(|v| serde_json::from_value(v.clone()).ok())
881                .unwrap_or_default();
882            versions.push(serde_json::json!({
883                "version": new_version.version,
884                "module_hash": hex_hash(&new_version.module_hash),
885                "deployed_at": new_version.deployed_at.to_rfc3339(),
886                "revoked": new_version.revoked,
887                "chain_seq": new_version.chain_seq,
888            }));
889            drop(tree);
890            self.update_meta(&tool_rid, "versions", serde_json::json!(versions))?;
891        }
892
893        self.chain.append(
894            "tool",
895            "tool.version.update",
896            Some(serde_json::json!({
897                "name": name,
898                "old_version": old_version,
899                "new_version": new_version.version,
900                "old_hash": old_hash,
901                "new_hash": hex_hash(&new_version.module_hash),
902            })),
903        );
904
905        debug!(tool = name, old = old_version, new = new_version.version, "tool version updated");
906        Ok(())
907    }
908
909    /// Revoke a tool version.
910    ///
911    /// Marks the specified version as revoked in metadata. Does NOT
912    /// delete the tree node (preserves audit trail). Emits a
913    /// `tool.version.revoke` chain event.
914    pub fn revoke_tool_version(
915        &self,
916        name: &str,
917        version: u32,
918    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
919        let parts: Vec<&str> = name.splitn(2, '.').collect();
920        if parts.len() != 2 {
921            return Err(format!("invalid tool name: {name}").into());
922        }
923        let tool_path = format!("/kernel/tools/{}/{}", parts[0], parts[1]);
924        let tool_rid = ResourceId::new(&tool_path);
925
926        {
927            let tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
928            tree.get(&tool_rid)
929                .ok_or_else(|| format!("tool not found: {tool_path}"))?;
930        }
931
932        let revoke_key = format!("v{version}_revoked");
933        let revoke_at_key = format!("v{version}_revoked_at");
934        self.update_meta(&tool_rid, &revoke_key, serde_json::json!(true))?;
935        self.update_meta(
936            &tool_rid,
937            &revoke_at_key,
938            serde_json::json!(Utc::now().to_rfc3339()),
939        )?;
940
941        let module_hash = {
942            let tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
943            let node = tree.get(&tool_rid).unwrap();
944            node.metadata
945                .get("module_hash")
946                .and_then(|v| v.as_str())
947                .unwrap_or("")
948                .to_string()
949        };
950
951        self.chain.append(
952            "tool",
953            "tool.version.revoke",
954            Some(serde_json::json!({
955                "name": name,
956                "version": version,
957                "module_hash": module_hash,
958            })),
959        );
960
961        debug!(tool = name, version, "tool version revoked");
962        Ok(())
963    }
964
965    /// Query version history for a tool (K4 B2).
966    ///
967    /// Returns the list of version entries persisted in the tree
968    /// metadata, or an empty vec if the tool has no versions.
969    pub fn get_tool_versions(
970        &self,
971        name: &str,
972    ) -> Vec<serde_json::Value> {
973        let parts: Vec<&str> = name.splitn(2, '.').collect();
974        if parts.len() != 2 {
975            return Vec::new();
976        }
977        let tool_path = format!("/kernel/tools/{}/{}", parts[0], parts[1]);
978        let tool_rid = ResourceId::new(&tool_path);
979
980        let tree = match self.tree.lock() {
981            Ok(t) => t,
982            Err(_) => return Vec::new(),
983        };
984        tree.get(&tool_rid)
985            .and_then(|node| node.metadata.get("versions"))
986            .and_then(|v| serde_json::from_value::<Vec<serde_json::Value>>(v.clone()).ok())
987            .unwrap_or_default()
988    }
989
990    /// Save tree state to a checkpoint file on disk.
991    ///
992    /// Serializes the tree via `exo_resource_tree::to_checkpoint()` and
993    /// writes the bytes to the given path.
994    pub fn save_checkpoint(
995        &self,
996        path: &std::path::Path,
997    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
998        let tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
999        let data = exo_resource_tree::to_checkpoint(&tree)?;
1000
1001        if let Some(parent) = path.parent() {
1002            std::fs::create_dir_all(parent)?;
1003        }
1004        std::fs::write(path, &data)?;
1005
1006        debug!(
1007            path = %path.display(),
1008            nodes = tree.len(),
1009            bytes = data.len(),
1010            "tree checkpoint saved"
1011        );
1012        Ok(())
1013    }
1014
1015    /// Load tree state from a checkpoint file.
1016    ///
1017    /// Reads the file, deserializes via `exo_resource_tree::from_checkpoint()`,
1018    /// and replaces the internal tree. The mutation log is cleared.
1019    pub fn load_checkpoint(
1020        &self,
1021        path: &std::path::Path,
1022    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1023        let data = std::fs::read(path)?;
1024        let restored = exo_resource_tree::from_checkpoint(&data)?;
1025
1026        let node_count = restored.len();
1027        let root_hash = hex_hash(&restored.root_hash());
1028
1029        let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
1030        *tree = restored;
1031
1032        // Clear mutation log since we loaded a fresh snapshot
1033        let mut log = self.mutation_log.lock().map_err(|e| format!("log lock: {e}"))?;
1034        *log = MutationLog::new();
1035
1036        debug!(
1037            path = %path.display(),
1038            nodes = node_count,
1039            root_hash = %root_hash,
1040            "tree checkpoint loaded"
1041        );
1042        Ok(())
1043    }
1044
1045    /// Create a combined checkpoint of tree + mutation log + chain state.
1046    ///
1047    /// Returns JSON with the tree checkpoint, mutation count, and chain
1048    /// checkpoint info. Full checkpoint persistence is a K1 concern.
1049    pub fn checkpoint(&self) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
1050        let tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
1051        let log = self.mutation_log.lock().map_err(|e| format!("log lock: {e}"))?;
1052
1053        let tree_data = exo_resource_tree::to_checkpoint(&tree)?;
1054        let chain_cp = self.chain.checkpoint();
1055
1056        let checkpoint = serde_json::json!({
1057            "tree": serde_json::from_slice::<serde_json::Value>(&tree_data)
1058                .unwrap_or(serde_json::Value::Null),
1059            "mutation_count": log.len(),
1060            "chain_checkpoint": {
1061                "chain_id": chain_cp.chain_id,
1062                "sequence": chain_cp.sequence,
1063                "timestamp": chain_cp.timestamp.to_rfc3339(),
1064            },
1065            "root_hash": hex_hash(&tree.root_hash()),
1066        });
1067
1068        // Log checkpoint event on chain
1069        drop(tree);
1070        drop(log);
1071        self.chain.append(
1072            "tree",
1073            "checkpoint",
1074            Some(serde_json::json!({
1075                "root_hash": checkpoint["root_hash"],
1076                "chain_seq": chain_cp.sequence,
1077            })),
1078        );
1079
1080        Ok(checkpoint)
1081    }
1082
1083    // --- K6 cross-node sync API ---
1084
1085    /// Create a serializable snapshot of the full tree state.
1086    /// Used for cross-node tree synchronization in K6.4.
1087    pub fn snapshot(&self) -> Result<TreeSnapshot, Box<dyn std::error::Error + Send + Sync>> {
1088        let tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
1089        let root_hash = hex_hash(&tree.root_hash());
1090        let node_count = tree.len();
1091
1092        let nodes: Vec<TreeNodeSnapshot> = tree
1093            .iter()
1094            .map(|(id, node)| {
1095                let metadata = node
1096                    .metadata
1097                    .iter()
1098                    .map(|(k, v)| (k.clone(), v.to_string()))
1099                    .collect();
1100                TreeNodeSnapshot {
1101                    path: id.to_string(),
1102                    kind: format!("{:?}", node.kind),
1103                    metadata,
1104                    hash: hex_hash(&node.merkle_hash),
1105                }
1106            })
1107            .collect();
1108
1109        Ok(TreeSnapshot {
1110            root_hash,
1111            node_count,
1112            nodes,
1113            taken_at: Utc::now(),
1114        })
1115    }
1116
1117    /// Apply a remote mutation received from a peer node.
1118    /// Records the mutation in the local log.
1119    ///
1120    /// **Security note** (K6.4): Full implementation should verify
1121    /// `event.signature` against the sending node's Ed25519 public key
1122    /// before applying. Currently signature verification is deferred to
1123    /// the mesh transport layer (Noise channel authenticates the peer).
1124    pub fn apply_remote_mutation(
1125        &self,
1126        event: MutationEvent,
1127    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1128        let mut tree = self.tree.lock().map_err(|e| format!("tree lock: {e}"))?;
1129        let mut log = self
1130            .mutation_log
1131            .lock()
1132            .map_err(|e| format!("log lock: {e}"))?;
1133
1134        // Apply based on mutation type
1135        match &event {
1136            MutationEvent::Create {
1137                id,
1138                kind,
1139                parent,
1140                ..
1141            } => {
1142                // Only insert if not already present (idempotent)
1143                if tree.get(id).is_none() {
1144                    tree.insert(id.clone(), kind.clone(), parent.clone())?;
1145                    tree.recompute_all();
1146                }
1147            }
1148            MutationEvent::Remove { id, .. } => {
1149                if tree.get(id).is_some() {
1150                    tree.remove(id.clone())?;
1151                    tree.recompute_all();
1152                }
1153            }
1154            MutationEvent::UpdateMeta { id, key, value, .. } => {
1155                if let Some(node) = tree.get_mut(id) {
1156                    if let Some(val) = value {
1157                        node.metadata.insert(key.clone(), val.clone());
1158                    } else {
1159                        node.metadata.remove(key);
1160                    }
1161                    node.updated_at = Utc::now();
1162                }
1163                tree.recompute_all();
1164            }
1165            MutationEvent::Move { .. } | MutationEvent::UpdateScoring { .. } => {
1166                // Move and scoring updates are recorded but not yet applied
1167                // in K6.0 — full support arrives in K6.4.
1168            }
1169            _ => {
1170                // Future MutationEvent variants -- record but do not apply.
1171            }
1172        }
1173
1174        // Record the mutation
1175        log.append(event);
1176
1177        Ok(())
1178    }
1179}
1180
1181impl std::fmt::Debug for TreeManager {
1182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1183        let stats = self.stats();
1184        f.debug_struct("TreeManager")
1185            .field("node_count", &stats.node_count)
1186            .field("mutation_count", &stats.mutation_count)
1187            .finish()
1188    }
1189}
1190
1191/// Format a 32-byte hash as a hex string.
1192fn hex_hash(hash: &[u8; 32]) -> String {
1193    hash.iter().map(|b| format!("{b:02x}")).collect()
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198    use super::*;
1199
1200    fn test_chain() -> Arc<ChainManager> {
1201        Arc::new(ChainManager::new(0, 1000))
1202    }
1203
1204    #[test]
1205    fn bootstrap_creates_nodes_and_chain_events() {
1206        let chain = test_chain();
1207        let tm = TreeManager::new(Arc::clone(&chain));
1208        tm.bootstrap().unwrap();
1209
1210        let tree = tm.tree().lock().unwrap();
1211        assert_eq!(tree.len(), 9); // root + 8 namespaces (incl /kernel/agents)
1212
1213        // Chain should have genesis + bootstrap event
1214        assert!(chain.len() >= 2);
1215        let events = chain.tail(0);
1216        assert!(events.iter().any(|e| e.kind == "bootstrap" && e.source == "tree"));
1217
1218        // Mutation log should have 8 entries (one per bootstrapped namespace)
1219        let log = tm.mutation_log().lock().unwrap();
1220        assert_eq!(log.len(), 8);
1221    }
1222
1223    #[test]
1224    fn insert_creates_node_and_chain_event() {
1225        let chain = test_chain();
1226        let tm = TreeManager::new(Arc::clone(&chain));
1227        tm.bootstrap().unwrap();
1228
1229        let before_len = chain.len();
1230        tm.insert(
1231            ResourceId::new("/kernel/services/cron"),
1232            ResourceKind::Service,
1233            ResourceId::new("/kernel/services"),
1234        )
1235        .unwrap();
1236
1237        let tree = tm.tree().lock().unwrap();
1238        assert!(tree.get(&ResourceId::new("/kernel/services/cron")).is_some());
1239
1240        // Chain should have one more event
1241        assert_eq!(chain.len(), before_len + 1);
1242
1243        // Node should have chain_seq metadata
1244        let node = tree.get(&ResourceId::new("/kernel/services/cron")).unwrap();
1245        assert!(node.metadata.contains_key("chain_seq"));
1246    }
1247
1248    #[test]
1249    fn remove_creates_chain_event() {
1250        let chain = test_chain();
1251        let tm = TreeManager::new(Arc::clone(&chain));
1252        tm.bootstrap().unwrap();
1253
1254        tm.insert(
1255            ResourceId::new("/kernel/services/test"),
1256            ResourceKind::Service,
1257            ResourceId::new("/kernel/services"),
1258        )
1259        .unwrap();
1260
1261        let before_len = chain.len();
1262        tm.remove(ResourceId::new("/kernel/services/test")).unwrap();
1263
1264        let tree = tm.tree().lock().unwrap();
1265        assert!(tree.get(&ResourceId::new("/kernel/services/test")).is_none());
1266        assert_eq!(chain.len(), before_len + 1);
1267    }
1268
1269    #[test]
1270    fn update_meta_creates_chain_event() {
1271        let chain = test_chain();
1272        let tm = TreeManager::new(Arc::clone(&chain));
1273        tm.bootstrap().unwrap();
1274
1275        let before_len = chain.len();
1276        tm.update_meta(
1277            &ResourceId::new("/kernel"),
1278            "version",
1279            serde_json::json!("0.1.0"),
1280        )
1281        .unwrap();
1282
1283        assert_eq!(chain.len(), before_len + 1);
1284        let tree = tm.tree().lock().unwrap();
1285        let node = tree.get(&ResourceId::new("/kernel")).unwrap();
1286        assert_eq!(node.metadata.get("version").unwrap(), &serde_json::json!("0.1.0"));
1287    }
1288
1289    #[test]
1290    fn register_service_creates_node() {
1291        let chain = test_chain();
1292        let tm = TreeManager::new(Arc::clone(&chain));
1293        tm.bootstrap().unwrap();
1294
1295        tm.register_service("cron").unwrap();
1296
1297        let tree = tm.tree().lock().unwrap();
1298        let node = tree.get(&ResourceId::new("/kernel/services/cron")).unwrap();
1299        assert_eq!(node.kind, ResourceKind::Service);
1300        assert!(node.metadata.contains_key("chain_seq"));
1301    }
1302
1303    #[test]
1304    fn stats_reports_correctly() {
1305        let chain = test_chain();
1306        let tm = TreeManager::new(Arc::clone(&chain));
1307        tm.bootstrap().unwrap();
1308
1309        let stats = tm.stats();
1310        assert_eq!(stats.node_count, 9); // root + 8 namespaces
1311        assert_eq!(stats.mutation_count, 8);
1312        assert_ne!(stats.root_hash, "0".repeat(64));
1313    }
1314
1315    #[test]
1316    fn checkpoint_includes_tree_and_chain() {
1317        let chain = test_chain();
1318        let tm = TreeManager::new(Arc::clone(&chain));
1319        tm.bootstrap().unwrap();
1320
1321        let cp = tm.checkpoint().unwrap();
1322        assert!(cp.get("tree").is_some());
1323        assert!(cp.get("mutation_count").is_some());
1324        assert!(cp.get("chain_checkpoint").is_some());
1325        assert!(cp.get("root_hash").is_some());
1326    }
1327
1328    #[test]
1329    fn register_agent_creates_node_and_chain_event() {
1330        let chain = test_chain();
1331        let tm = TreeManager::new(Arc::clone(&chain));
1332        tm.bootstrap().unwrap();
1333
1334        let caps = crate::capability::AgentCapabilities::default();
1335        let before_len = chain.len();
1336        tm.register_agent("test-agent", 42, &caps).unwrap();
1337
1338        // Node should exist
1339        let tree = tm.tree().lock().unwrap();
1340        let node = tree
1341            .get(&ResourceId::new("/kernel/agents/test-agent"))
1342            .unwrap();
1343        assert_eq!(node.kind, ResourceKind::Agent);
1344        assert_eq!(node.metadata["pid"], serde_json::json!(42));
1345        assert_eq!(node.metadata["state"], serde_json::json!("starting"));
1346        assert!(node.metadata.contains_key("chain_seq"));
1347        drop(tree);
1348
1349        // Chain event
1350        assert!(chain.len() > before_len);
1351        let events = chain.tail(2);
1352        assert!(events.iter().any(|e| e.kind == "agent.spawn"));
1353    }
1354
1355    #[test]
1356    fn unregister_agent_updates_node() {
1357        let chain = test_chain();
1358        let tm = TreeManager::new(Arc::clone(&chain));
1359        tm.bootstrap().unwrap();
1360
1361        let caps = crate::capability::AgentCapabilities::default();
1362        tm.register_agent("exit-agent", 10, &caps).unwrap();
1363        tm.unregister_agent("exit-agent", 10, 0).unwrap();
1364
1365        // Node still exists but state=exited
1366        let tree = tm.tree().lock().unwrap();
1367        let node = tree
1368            .get(&ResourceId::new("/kernel/agents/exit-agent"))
1369            .unwrap();
1370        assert_eq!(node.metadata["state"], serde_json::json!("exited"));
1371        assert_eq!(node.metadata["exit_code"], serde_json::json!(0));
1372        assert!(node.metadata.contains_key("stop_time"));
1373        drop(tree);
1374
1375        // Chain event
1376        let events = chain.tail(2);
1377        assert!(events.iter().any(|e| e.kind == "agent.stop"));
1378    }
1379
1380    #[test]
1381    fn update_agent_state() {
1382        let chain = test_chain();
1383        let tm = TreeManager::new(Arc::clone(&chain));
1384        tm.bootstrap().unwrap();
1385
1386        let caps = crate::capability::AgentCapabilities::default();
1387        tm.register_agent("state-agent", 20, &caps).unwrap();
1388        tm.update_agent_state("state-agent", "running").unwrap();
1389
1390        let tree = tm.tree().lock().unwrap();
1391        let node = tree
1392            .get(&ResourceId::new("/kernel/agents/state-agent"))
1393            .unwrap();
1394        assert_eq!(node.metadata["state"], serde_json::json!("running"));
1395    }
1396
1397    #[test]
1398    fn chain_integrity_after_operations() {
1399        let chain = test_chain();
1400        let tm = TreeManager::new(Arc::clone(&chain));
1401        tm.bootstrap().unwrap();
1402        tm.register_service("cron").unwrap();
1403
1404        let result = chain.verify_integrity();
1405        assert!(result.valid);
1406        assert!(result.event_count >= 3); // genesis + bootstrap + insert
1407    }
1408
1409    // --- Scoring API tests ---
1410
1411    #[test]
1412    fn update_scoring_creates_chain_event() {
1413        let chain = test_chain();
1414        let tm = TreeManager::new(Arc::clone(&chain));
1415        tm.bootstrap().unwrap();
1416
1417        let before_len = chain.len();
1418        let scoring = NodeScoring::new(0.9, 0.8, 0.7, 0.6, 0.5, 0.4);
1419        tm.update_scoring(&ResourceId::new("/kernel"), scoring).unwrap();
1420
1421        // Chain event emitted
1422        assert!(chain.len() > before_len);
1423        let events = chain.tail(2);
1424        assert!(events.iter().any(|e| e.kind == "scoring.update"));
1425
1426        // Scoring stored on node
1427        let s = tm.get_scoring(&ResourceId::new("/kernel")).unwrap();
1428        assert!((s.trust - 0.9).abs() < 1e-6);
1429
1430        // Mutation log has the entry
1431        let log = tm.mutation_log().lock().unwrap();
1432        let last = log.events().last().unwrap();
1433        assert!(matches!(last, MutationEvent::UpdateScoring { .. }));
1434    }
1435
1436    #[test]
1437    fn blend_scoring_creates_chain_event() {
1438        let chain = test_chain();
1439        let tm = TreeManager::new(Arc::clone(&chain));
1440        tm.bootstrap().unwrap();
1441
1442        let before_len = chain.len();
1443        let obs = NodeScoring::new(1.0, 1.0, 1.0, 1.0, 1.0, 1.0);
1444        tm.blend_scoring(&ResourceId::new("/kernel"), &obs, 0.5).unwrap();
1445
1446        assert!(chain.len() > before_len);
1447        let events = chain.tail(2);
1448        assert!(events.iter().any(|e| e.kind == "scoring.blend"));
1449
1450        // Should be EMA blended: 0.5*0.5 + 1.0*0.5 = 0.75
1451        let s = tm.get_scoring(&ResourceId::new("/kernel")).unwrap();
1452        assert!((s.trust - 0.75).abs() < 1e-6);
1453    }
1454
1455    #[test]
1456    fn get_scoring_nonexistent_returns_none() {
1457        let chain = test_chain();
1458        let tm = TreeManager::new(Arc::clone(&chain));
1459        assert!(tm.get_scoring(&ResourceId::new("/no/such/node")).is_none());
1460    }
1461
1462    #[test]
1463    fn find_similar_returns_ranked() {
1464        let chain = test_chain();
1465        let tm = TreeManager::new(Arc::clone(&chain));
1466        tm.bootstrap().unwrap();
1467
1468        // Set /kernel to a specific scoring
1469        let target = NodeScoring::new(0.9, 0.9, 0.9, 0.9, 0.9, 0.9);
1470        tm.update_scoring(&ResourceId::new("/kernel"), target).unwrap();
1471
1472        // /apps gets a similar scoring
1473        let similar = NodeScoring::new(0.85, 0.85, 0.85, 0.85, 0.85, 0.85);
1474        tm.update_scoring(&ResourceId::new("/apps"), similar).unwrap();
1475
1476        let results = tm.find_similar(&ResourceId::new("/kernel"), 3);
1477        assert!(!results.is_empty());
1478        // First result should have high similarity
1479        assert!(results[0].1 > 0.9);
1480    }
1481
1482    #[test]
1483    fn rank_by_score_returns_ordered() {
1484        let chain = test_chain();
1485        let tm = TreeManager::new(Arc::clone(&chain));
1486        tm.bootstrap().unwrap();
1487
1488        // High trust on /kernel
1489        tm.update_scoring(
1490            &ResourceId::new("/kernel"),
1491            NodeScoring::new(1.0, 0.0, 0.0, 0.0, 0.0, 0.0),
1492        ).unwrap();
1493
1494        // High performance on /apps
1495        tm.update_scoring(
1496            &ResourceId::new("/apps"),
1497            NodeScoring::new(0.0, 1.0, 0.0, 0.0, 0.0, 0.0),
1498        ).unwrap();
1499
1500        // Rank by trust weight only
1501        let weights = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0];
1502        let ranked = tm.rank_by_score(&weights, 3);
1503        assert!(!ranked.is_empty());
1504        assert_eq!(ranked[0].0, ResourceId::new("/kernel"));
1505    }
1506
1507    #[test]
1508    fn checkpoint_roundtrip_preserves_root_hash() {
1509        let chain = test_chain();
1510        let tm = TreeManager::new(Arc::clone(&chain));
1511        tm.bootstrap().unwrap();
1512        tm.register_service("cron").unwrap();
1513
1514        let original_hash = tm.stats().root_hash;
1515
1516        // Simulate shutdown: save tree checkpoint and record root hash in chain.
1517        let dir = std::env::temp_dir().join("clawft-tree-ckpt-hash-test");
1518        let tree_path = dir.join("tree.json");
1519        tm.save_checkpoint(&tree_path).unwrap();
1520        chain.append(
1521            "tree",
1522            "tree.checkpoint",
1523            Some(serde_json::json!({
1524                "path": tree_path.display().to_string(),
1525                "root_hash": original_hash,
1526            })),
1527        );
1528
1529        // Simulate restart: create new tree manager, load checkpoint.
1530        let tm2 = TreeManager::new(Arc::clone(&chain));
1531        tm2.load_checkpoint(&tree_path).unwrap();
1532
1533        // Root hash should match what the chain recorded.
1534        let restored_hash = tm2.stats().root_hash;
1535        let chain_hash = chain.last_tree_root_hash().unwrap();
1536        assert_eq!(restored_hash, chain_hash);
1537        assert_eq!(restored_hash, original_hash);
1538
1539        // Clean up.
1540        let _ = std::fs::remove_dir_all(&dir);
1541    }
1542
1543    #[test]
1544    fn checkpoint_hash_mismatch_detectable() {
1545        let chain = test_chain();
1546        let tm = TreeManager::new(Arc::clone(&chain));
1547        tm.bootstrap().unwrap();
1548
1549        let dir = std::env::temp_dir().join("clawft-tree-ckpt-mismatch-test");
1550        let tree_path = dir.join("tree.json");
1551        tm.save_checkpoint(&tree_path).unwrap();
1552
1553        // Record a fake root hash in the chain (simulating corruption).
1554        chain.append(
1555            "tree",
1556            "tree.checkpoint",
1557            Some(serde_json::json!({
1558                "root_hash": "0000000000000000000000000000000000000000000000000000000000000000",
1559            })),
1560        );
1561
1562        // Load checkpoint and compare — should detect mismatch.
1563        let tm2 = TreeManager::new(Arc::clone(&chain));
1564        tm2.load_checkpoint(&tree_path).unwrap();
1565
1566        let restored_hash = tm2.stats().root_hash;
1567        let chain_hash = chain.last_tree_root_hash().unwrap();
1568        assert_ne!(restored_hash, chain_hash, "should detect hash mismatch");
1569
1570        // Clean up.
1571        let _ = std::fs::remove_dir_all(&dir);
1572    }
1573
1574    // --- Tool lifecycle tests ---
1575
1576    fn test_signing_key() -> ed25519_dalek::SigningKey {
1577        ed25519_dalek::SigningKey::from_bytes(&[42u8; 32])
1578    }
1579
1580    fn setup_tool_tree() -> (Arc<ChainManager>, TreeManager) {
1581        let chain = test_chain();
1582        let tm = TreeManager::new(Arc::clone(&chain));
1583        tm.bootstrap().unwrap();
1584        // Create /kernel/tools namespace
1585        tm.insert(
1586            ResourceId::new("/kernel/tools"),
1587            ResourceKind::Namespace,
1588            ResourceId::new("/kernel"),
1589        )
1590        .unwrap();
1591        (chain, tm)
1592    }
1593
1594    #[test]
1595    fn tool_build_computes_hash_and_signs() {
1596        let (_chain, tm) = setup_tool_tree();
1597        let key = test_signing_key();
1598        let wasm_bytes = b"fake wasm module bytes for testing";
1599
1600        let tv = tm.build_tool("fs.read_file", wasm_bytes, &key).unwrap();
1601        assert_eq!(tv.version, 1);
1602        assert!(!tv.revoked);
1603        // Hash should match compute_module_hash
1604        let expected = compute_module_hash(wasm_bytes);
1605        assert_eq!(tv.module_hash, expected);
1606        // Signature should be non-zero
1607        assert_ne!(tv.signature, [0u8; 64]);
1608    }
1609
1610    #[test]
1611    fn tool_deploy_creates_tree_node() {
1612        let (_chain, tm) = setup_tool_tree();
1613        let spec = crate::wasm_runner::builtin_tool_catalog()
1614            .into_iter()
1615            .find(|s| s.name == "fs.read_file")
1616            .unwrap();
1617        let tv = ToolVersion {
1618            version: 1,
1619            module_hash: [0xAA; 32],
1620            signature: [0xBB; 64],
1621            deployed_at: Utc::now(),
1622            revoked: false,
1623            chain_seq: 10,
1624        };
1625
1626        tm.deploy_tool(&spec, &tv).unwrap();
1627
1628        let tree = tm.tree().lock().unwrap();
1629        let node = tree.get(&ResourceId::new("/kernel/tools/fs/read_file"));
1630        assert!(node.is_some(), "tool node should exist");
1631        let node = node.unwrap();
1632        assert_eq!(node.kind, ResourceKind::Tool);
1633        assert_eq!(node.metadata["tool_version"], serde_json::json!(1));
1634        assert!(node.metadata.contains_key("gate_action"));
1635    }
1636
1637    #[test]
1638    fn tool_deploy_emits_chain_event() {
1639        let (chain, tm) = setup_tool_tree();
1640        let spec = crate::wasm_runner::builtin_tool_catalog()
1641            .into_iter()
1642            .find(|s| s.name == "fs.read_file")
1643            .unwrap();
1644        let tv = ToolVersion {
1645            version: 1,
1646            module_hash: [0xAA; 32],
1647            signature: [0xBB; 64],
1648            deployed_at: Utc::now(),
1649            revoked: false,
1650            chain_seq: 10,
1651        };
1652
1653        let before = chain.len();
1654        tm.deploy_tool(&spec, &tv).unwrap();
1655        assert!(chain.len() > before);
1656
1657        let events = chain.tail(5);
1658        assert!(
1659            events.iter().any(|e| e.kind == "tool.deploy"),
1660            "expected tool.deploy chain event"
1661        );
1662    }
1663
1664    #[test]
1665    fn tool_version_update_chain_links() {
1666        let (chain, tm) = setup_tool_tree();
1667        let spec = crate::wasm_runner::builtin_tool_catalog()
1668            .into_iter()
1669            .find(|s| s.name == "fs.read_file")
1670            .unwrap();
1671        let v1 = ToolVersion {
1672            version: 1,
1673            module_hash: [0xAA; 32],
1674            signature: [0xBB; 64],
1675            deployed_at: Utc::now(),
1676            revoked: false,
1677            chain_seq: 10,
1678        };
1679        tm.deploy_tool(&spec, &v1).unwrap();
1680
1681        let v2 = ToolVersion {
1682            version: 2,
1683            module_hash: [0xCC; 32],
1684            signature: [0xDD; 64],
1685            deployed_at: Utc::now(),
1686            revoked: false,
1687            chain_seq: 20,
1688        };
1689        tm.update_tool_version("fs.read_file", &v2).unwrap();
1690
1691        // Verify chain event
1692        let events = chain.tail(5);
1693        let update_evt = events
1694            .iter()
1695            .find(|e| e.kind == "tool.version.update")
1696            .expect("expected tool.version.update event");
1697        let payload = update_evt.payload.as_ref().unwrap();
1698        assert_eq!(payload["old_version"], 1);
1699        assert_eq!(payload["new_version"], 2);
1700
1701        // Verify tree metadata updated
1702        let tree = tm.tree().lock().unwrap();
1703        let node = tree
1704            .get(&ResourceId::new("/kernel/tools/fs/read_file"))
1705            .unwrap();
1706        assert_eq!(node.metadata["tool_version"], serde_json::json!(2));
1707    }
1708
1709    #[test]
1710    fn tool_version_revoke_marks_revoked() {
1711        let (_chain, tm) = setup_tool_tree();
1712        let spec = crate::wasm_runner::builtin_tool_catalog()
1713            .into_iter()
1714            .find(|s| s.name == "fs.read_file")
1715            .unwrap();
1716        let v1 = ToolVersion {
1717            version: 1,
1718            module_hash: [0xAA; 32],
1719            signature: [0xBB; 64],
1720            deployed_at: Utc::now(),
1721            revoked: false,
1722            chain_seq: 10,
1723        };
1724        tm.deploy_tool(&spec, &v1).unwrap();
1725
1726        tm.revoke_tool_version("fs.read_file", 1).unwrap();
1727
1728        let tree = tm.tree().lock().unwrap();
1729        let node = tree
1730            .get(&ResourceId::new("/kernel/tools/fs/read_file"))
1731            .unwrap();
1732        assert_eq!(node.metadata["v1_revoked"], serde_json::json!(true));
1733        assert!(node.metadata.contains_key("v1_revoked_at"));
1734    }
1735
1736    // --- Version history tests (K4 B2) ---
1737
1738    #[test]
1739    fn version_history_persisted_in_tree() {
1740        let (_chain, tm) = setup_tool_tree();
1741        let spec = crate::wasm_runner::builtin_tool_catalog()
1742            .into_iter()
1743            .find(|s| s.name == "fs.read_file")
1744            .unwrap();
1745
1746        let v1 = ToolVersion {
1747            version: 1,
1748            module_hash: [0xAA; 32],
1749            signature: [0xBB; 64],
1750            deployed_at: Utc::now(),
1751            revoked: false,
1752            chain_seq: 10,
1753        };
1754        tm.deploy_tool(&spec, &v1).unwrap();
1755
1756        let v2 = ToolVersion {
1757            version: 2,
1758            module_hash: [0xCC; 32],
1759            signature: [0xDD; 64],
1760            deployed_at: Utc::now(),
1761            revoked: false,
1762            chain_seq: 20,
1763        };
1764        tm.update_tool_version("fs.read_file", &v2).unwrap();
1765
1766        let versions = tm.get_tool_versions("fs.read_file");
1767        assert_eq!(versions.len(), 2, "should have 2 versions");
1768        assert_eq!(versions[0]["version"], 1);
1769        assert_eq!(versions[1]["version"], 2);
1770    }
1771
1772    #[test]
1773    fn version_history_includes_revoked() {
1774        let (_chain, tm) = setup_tool_tree();
1775        let spec = crate::wasm_runner::builtin_tool_catalog()
1776            .into_iter()
1777            .find(|s| s.name == "fs.read_file")
1778            .unwrap();
1779
1780        let v1 = ToolVersion {
1781            version: 1,
1782            module_hash: [0xAA; 32],
1783            signature: [0xBB; 64],
1784            deployed_at: Utc::now(),
1785            revoked: false,
1786            chain_seq: 10,
1787        };
1788        tm.deploy_tool(&spec, &v1).unwrap();
1789        tm.revoke_tool_version("fs.read_file", 1).unwrap();
1790
1791        let versions = tm.get_tool_versions("fs.read_file");
1792        assert_eq!(versions.len(), 1, "version entry should persist after revoke");
1793        // The revoke flag in the version array entry is as deployed (false),
1794        // but the per-version metadata key v1_revoked=true is set separately.
1795        // Version history records deploy-time state.
1796    }
1797
1798    #[test]
1799    fn tool_revoke_emits_chain_event() {
1800        let (chain, tm) = setup_tool_tree();
1801        let spec = crate::wasm_runner::builtin_tool_catalog()
1802            .into_iter()
1803            .find(|s| s.name == "fs.read_file")
1804            .unwrap();
1805        let v1 = ToolVersion {
1806            version: 1,
1807            module_hash: [0xAA; 32],
1808            signature: [0xBB; 64],
1809            deployed_at: Utc::now(),
1810            revoked: false,
1811            chain_seq: 10,
1812        };
1813        tm.deploy_tool(&spec, &v1).unwrap();
1814
1815        let before = chain.len();
1816        tm.revoke_tool_version("fs.read_file", 1).unwrap();
1817        assert!(chain.len() > before);
1818
1819        let events = chain.tail(5);
1820        assert!(
1821            events.iter().any(|e| e.kind == "tool.version.revoke"),
1822            "expected tool.version.revoke chain event"
1823        );
1824    }
1825
1826    #[test]
1827    fn snapshot_on_bootstrapped_tree() {
1828        let chain = test_chain();
1829        let tm = TreeManager::new(Arc::clone(&chain));
1830        tm.bootstrap().unwrap();
1831
1832        let snap = tm.snapshot().unwrap();
1833        assert!(snap.node_count > 0);
1834        assert!(!snap.nodes.is_empty());
1835        // The snapshot should contain at least the root + bootstrapped namespaces
1836        assert!(snap.nodes.len() >= 9);
1837    }
1838
1839    #[test]
1840    fn snapshot_root_hash_matches_stats() {
1841        let chain = test_chain();
1842        let tm = TreeManager::new(Arc::clone(&chain));
1843        tm.bootstrap().unwrap();
1844
1845        let snap = tm.snapshot().unwrap();
1846        let stats = tm.stats();
1847        assert_eq!(snap.root_hash, stats.root_hash);
1848    }
1849
1850    #[test]
1851    fn apply_remote_mutation_records_in_log() {
1852        let chain = test_chain();
1853        let tm = TreeManager::new(Arc::clone(&chain));
1854        tm.bootstrap().unwrap();
1855
1856        let before = tm.mutation_log().lock().unwrap().len();
1857
1858        let event = MutationEvent::Create {
1859            id: ResourceId::new("/kernel/services/remote_svc"),
1860            kind: ResourceKind::Service,
1861            parent: ResourceId::new("/kernel/services"),
1862            timestamp: Utc::now(),
1863            signature: None,
1864        };
1865        tm.apply_remote_mutation(event).unwrap();
1866
1867        let after = tm.mutation_log().lock().unwrap().len();
1868        assert_eq!(after, before + 1);
1869
1870        // The node should exist in the tree
1871        let tree = tm.tree().lock().unwrap();
1872        assert!(tree.get(&ResourceId::new("/kernel/services/remote_svc")).is_some());
1873    }
1874
1875    #[test]
1876    fn mutations_signed_when_key_set() {
1877        let chain = test_chain();
1878        let mut tm = TreeManager::new(Arc::clone(&chain));
1879        let key = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
1880        tm.set_signing_key(key.clone());
1881        tm.bootstrap().unwrap();
1882
1883        // Bootstrap mutations should all be signed
1884        {
1885            let log = tm.mutation_log().lock().unwrap();
1886            for evt in log.events() {
1887                match evt {
1888                    MutationEvent::Create { signature, .. } => {
1889                        assert!(signature.is_some(), "bootstrap Create should be signed");
1890                        assert_eq!(signature.as_ref().unwrap().len(), 64);
1891                    }
1892                    _ => {}
1893                }
1894            }
1895        }
1896
1897        // Insert should produce signed mutation
1898        tm.insert(
1899            ResourceId::new("/kernel/services/signed_svc"),
1900            ResourceKind::Service,
1901            ResourceId::new("/kernel/services"),
1902        )
1903        .unwrap();
1904
1905        let log = tm.mutation_log().lock().unwrap();
1906        let last = log.events().last().unwrap();
1907        match last {
1908            MutationEvent::Create { signature, .. } => {
1909                let sig_bytes = signature.as_ref().expect("insert should be signed");
1910                assert_eq!(sig_bytes.len(), 64);
1911                // Verify the signature bytes form a valid Ed25519 signature
1912                let sig = ed25519_dalek::Signature::from_bytes(
1913                    sig_bytes.as_slice().try_into().unwrap(),
1914                );
1915                assert_eq!(sig.to_bytes().len(), 64);
1916            }
1917            _ => panic!("expected Create variant"),
1918        }
1919    }
1920
1921    #[test]
1922    fn mutations_unsigned_without_key() {
1923        let chain = test_chain();
1924        let tm = TreeManager::new(Arc::clone(&chain));
1925        tm.bootstrap().unwrap();
1926
1927        tm.insert(
1928            ResourceId::new("/kernel/services/unsigned_svc"),
1929            ResourceKind::Service,
1930            ResourceId::new("/kernel/services"),
1931        )
1932        .unwrap();
1933
1934        let log = tm.mutation_log().lock().unwrap();
1935        // All mutations should have signature = None when no key is set
1936        for evt in log.events() {
1937            match evt {
1938                MutationEvent::Create { signature, .. } => {
1939                    assert!(signature.is_none(), "should be None without signing key");
1940                }
1941                _ => {}
1942            }
1943        }
1944    }
1945}