1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct TreeStats {
31 pub node_count: usize,
33 pub mutation_count: usize,
35 pub root_hash: String,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct TreeSnapshot {
42 pub root_hash: String,
44 pub node_count: usize,
46 pub nodes: Vec<TreeNodeSnapshot>,
48 pub taken_at: chrono::DateTime<chrono::Utc>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct TreeNodeSnapshot {
55 pub path: String,
57 pub kind: String,
59 pub metadata: std::collections::HashMap<String, String>,
61 pub hash: String,
63}
64
65pub struct TreeManager {
72 tree: Mutex<ResourceTree>,
73 mutation_log: Mutex<MutationLog>,
74 chain: Arc<ChainManager>,
75 #[cfg(feature = "exochain")]
77 signing_key: Option<ed25519_dalek::SigningKey>,
78}
79
80impl TreeManager {
81 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 #[cfg(feature = "exochain")]
95 pub fn set_signing_key(&mut self, key: ed25519_dalek::SigningKey) {
96 self.signing_key = Some(key);
97 }
98
99 #[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 #[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 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 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 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 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 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 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 tree.recompute_all();
210
211 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 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 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 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 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 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 {
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 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 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 {
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 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 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 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 pub fn tree(&self) -> &Mutex<ResourceTree> {
500 &self.tree
501 }
502
503 pub fn mutation_log(&self) -> &Mutex<MutationLog> {
505 &self.mutation_log
506 }
507
508 pub fn chain(&self) -> &Arc<ChainManager> {
510 &self.chain
511 }
512
513 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 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 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 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 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 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 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 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 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 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 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 self.insert(
774 tool_rid.clone(),
775 ResourceKind::Tool,
776 ResourceId::new(&cat_path),
777 )?;
778
779 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 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 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 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 {
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 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 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 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 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 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 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 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 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 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 match &event {
1136 MutationEvent::Create {
1137 id,
1138 kind,
1139 parent,
1140 ..
1141 } => {
1142 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 }
1169 _ => {
1170 }
1172 }
1173
1174 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
1191fn 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); assert!(chain.len() >= 2);
1215 let events = chain.tail(0);
1216 assert!(events.iter().any(|e| e.kind == "bootstrap" && e.source == "tree"));
1217
1218 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 assert_eq!(chain.len(), before_len + 1);
1242
1243 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); 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 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 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 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 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); }
1408
1409 #[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 assert!(chain.len() > before_len);
1423 let events = chain.tail(2);
1424 assert!(events.iter().any(|e| e.kind == "scoring.update"));
1425
1426 let s = tm.get_scoring(&ResourceId::new("/kernel")).unwrap();
1428 assert!((s.trust - 0.9).abs() < 1e-6);
1429
1430 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 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 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 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 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 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 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 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 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 let tm2 = TreeManager::new(Arc::clone(&chain));
1531 tm2.load_checkpoint(&tree_path).unwrap();
1532
1533 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 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 chain.append(
1555 "tree",
1556 "tree.checkpoint",
1557 Some(serde_json::json!({
1558 "root_hash": "0000000000000000000000000000000000000000000000000000000000000000",
1559 })),
1560 );
1561
1562 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 let _ = std::fs::remove_dir_all(&dir);
1572 }
1573
1574 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 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 let expected = compute_module_hash(wasm_bytes);
1605 assert_eq!(tv.module_hash, expected);
1606 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 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 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 #[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 }
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 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 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 {
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 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 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 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}