lean_ctx/core/agents/
host_mirror.rs1use super::AgentRegistry;
17use crate::core::a2a::message::{MessagePriority, PrivacyLevel};
18
19pub const HOST_MIRROR_CATEGORY: &str = "direct";
21pub const HOST_MIRROR_MAX_BYTES: usize = 8 * 1024;
23const KEY_PREFIX: &str = "host-mirror:";
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct HostMessage {
28 pub project_root: String,
29 pub from_agent: String,
30 pub to_agent: String,
31 pub body: String,
32 pub tool_use_id: String,
33}
34
35#[must_use]
38pub fn cap_host_message(text: &str, max: usize) -> String {
39 if text.len() <= max {
40 return text.to_string();
41 }
42 let mut cut = max;
43 while !text.is_char_boundary(cut) {
44 cut -= 1;
45 }
46 format!(
47 "{}\n[lean-ctx: truncated, {} bytes omitted]",
48 &text[..cut],
49 text.len() - cut
50 )
51}
52
53pub fn mirror_host_message(message: &HostMessage) -> Result<String, String> {
57 let redacted = crate::core::redaction::redact_text(&message.body);
58 let body = cap_host_message(&redacted, HOST_MIRROR_MAX_BYTES);
59 let key = format!("{KEY_PREFIX}{}", message.tool_use_id);
60 AgentRegistry::mutate_locked(|registry| {
61 registry.post_message_keyed(
62 Some(&message.project_root),
63 &message.from_agent,
64 Some(&message.to_agent),
65 HOST_MIRROR_CATEGORY,
66 &body,
67 PrivacyLevel::Private,
68 MessagePriority::Normal,
69 None,
70 Some(&key),
71 )
72 })
73 .and_then(|(_, posted)| posted)
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 fn message(tool_use_id: &str, body: &str, root: &str) -> HostMessage {
81 HostMessage {
82 project_root: root.to_string(),
83 from_agent: "claude:s1".to_string(),
84 to_agent: "worker".to_string(),
85 body: body.to_string(),
86 tool_use_id: tool_use_id.to_string(),
87 }
88 }
89
90 fn mirrored() -> Vec<super::super::ScratchpadEntry> {
91 AgentRegistry::load_or_create()
92 .scratchpad
93 .into_iter()
94 .filter(|e| e.category == HOST_MIRROR_CATEGORY)
95 .collect()
96 }
97
98 #[test]
99 fn same_tool_use_id_is_stored_once() {
100 let _data = crate::core::data_dir::isolated_data_dir();
101 let _policy = crate::core::policy::runtime::TestPolicyOverride::set(None);
102 let dir = tempfile::tempdir().unwrap();
103 let root = dir.path().to_string_lossy().into_owned();
104 let first = mirror_host_message(&message("toolu_1", "build green", &root)).unwrap();
105 let again = mirror_host_message(&message("toolu_1", "build green", &root)).unwrap();
106 assert_eq!(first, again, "a repeated hook returns the first entry");
107 mirror_host_message(&message("toolu_2", "build green", &root)).unwrap();
108 let entries = mirrored();
109 assert_eq!(entries.len(), 2);
110 assert_eq!(entries[0].to_agent.as_deref(), Some("worker"));
111 assert_eq!(entries[0].from_agent, "claude:s1");
112 assert_eq!(
113 entries[0].privacy,
114 PrivacyLevel::Private,
115 "mirrors never reach Hive"
116 );
117 assert_eq!(entries[0].message, "build green");
118 }
119
120 #[test]
121 fn oversized_body_is_capped_with_a_marker() {
122 let _data = crate::core::data_dir::isolated_data_dir();
123 let _policy = crate::core::policy::runtime::TestPolicyOverride::set(None);
124 let dir = tempfile::tempdir().unwrap();
125 let root = dir.path().to_string_lossy().into_owned();
126 let body = "é".repeat(HOST_MIRROR_MAX_BYTES);
127 mirror_host_message(&message("toolu_big", &body, &root)).unwrap();
128 let stored = &mirrored()[0].message;
129 assert!(
130 stored.len() < HOST_MIRROR_MAX_BYTES + 64,
131 "{}",
132 stored.len()
133 );
134 assert!(stored.ends_with(&format!(
135 "[lean-ctx: truncated, {HOST_MIRROR_MAX_BYTES} bytes omitted]"
136 )));
137 assert_eq!(cap_host_message("short", 10), "short");
138 assert_eq!(
139 cap_host_message("aé", 2),
140 "a\n[lean-ctx: truncated, 2 bytes omitted]"
141 );
142 }
143
144 #[test]
145 fn a_credential_never_reaches_the_store() {
146 let _data = crate::core::data_dir::isolated_data_dir();
147 let _policy = crate::core::policy::runtime::TestPolicyOverride::set(None);
148 let dir = tempfile::tempdir().unwrap();
149 let root = dir.path().to_string_lossy().into_owned();
150 let key = concat!("AK", "IAIOSFODNN7EXAMPLE");
151 let body = format!("{}aws_key = {key}", "x".repeat(HOST_MIRROR_MAX_BYTES - 16));
153 mirror_host_message(&message("toolu_s", &body, &root)).unwrap();
154 let stored = &mirrored()[0].message;
155 assert!(!stored.contains(key));
156 assert!(
157 !stored.contains(&key[..8]),
158 "no secret prefix left by the cut"
159 );
160 let raw = std::fs::read_to_string(
161 crate::core::data_dir::lean_ctx_data_dir()
162 .unwrap()
163 .join("agents/registry.json"),
164 )
165 .unwrap();
166 assert!(!raw.contains(key));
167 }
168}