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