use super::AgentRegistry;
use crate::core::a2a::message::{MessagePriority, PrivacyLevel};
pub const HOST_MIRROR_CATEGORY: &str = "direct";
pub const HOST_MIRROR_MAX_BYTES: usize = 8 * 1024;
const KEY_PREFIX: &str = "host-mirror:";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostMessage {
pub project_root: String,
pub from_agent: String,
pub to_agent: String,
pub body: String,
pub tool_use_id: String,
}
#[must_use]
pub fn cap_host_message(text: &str, max: usize) -> String {
if text.len() <= max {
return text.to_string();
}
let mut cut = max;
while !text.is_char_boundary(cut) {
cut -= 1;
}
format!(
"{}\n[lean-ctx: truncated, {} bytes omitted]",
&text[..cut],
text.len() - cut
)
}
pub fn mirror_host_message(message: &HostMessage) -> Result<String, String> {
let redacted = crate::core::redaction::redact_text(&message.body);
let body = cap_host_message(&redacted, HOST_MIRROR_MAX_BYTES);
let key = format!("{KEY_PREFIX}{}", message.tool_use_id);
AgentRegistry::mutate_locked(|registry| {
registry.post_message_keyed(
Some(&message.project_root),
&message.from_agent,
Some(&message.to_agent),
HOST_MIRROR_CATEGORY,
&body,
PrivacyLevel::Private,
MessagePriority::Normal,
None,
Some(&key),
)
})
.and_then(|(_, posted)| posted)
}
#[cfg(test)]
mod tests {
use super::*;
fn message(tool_use_id: &str, body: &str, root: &str) -> HostMessage {
HostMessage {
project_root: root.to_string(),
from_agent: "claude:s1".to_string(),
to_agent: "worker".to_string(),
body: body.to_string(),
tool_use_id: tool_use_id.to_string(),
}
}
fn mirrored() -> Vec<super::super::ScratchpadEntry> {
AgentRegistry::load_or_create()
.scratchpad
.into_iter()
.filter(|e| e.category == HOST_MIRROR_CATEGORY)
.collect()
}
#[test]
fn same_tool_use_id_is_stored_once() {
let _data = crate::core::data_dir::isolated_data_dir();
let _policy = crate::core::policy::runtime::TestPolicyOverride::set(None);
let dir = tempfile::tempdir().unwrap();
let root = dir.path().to_string_lossy().into_owned();
let first = mirror_host_message(&message("toolu_1", "build green", &root)).unwrap();
let again = mirror_host_message(&message("toolu_1", "build green", &root)).unwrap();
assert_eq!(first, again, "a repeated hook returns the first entry");
mirror_host_message(&message("toolu_2", "build green", &root)).unwrap();
let entries = mirrored();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].to_agent.as_deref(), Some("worker"));
assert_eq!(entries[0].from_agent, "claude:s1");
assert_eq!(
entries[0].privacy,
PrivacyLevel::Private,
"mirrors never reach Hive"
);
assert_eq!(entries[0].message, "build green");
}
#[test]
fn oversized_body_is_capped_with_a_marker() {
let _data = crate::core::data_dir::isolated_data_dir();
let _policy = crate::core::policy::runtime::TestPolicyOverride::set(None);
let dir = tempfile::tempdir().unwrap();
let root = dir.path().to_string_lossy().into_owned();
let body = "é".repeat(HOST_MIRROR_MAX_BYTES);
mirror_host_message(&message("toolu_big", &body, &root)).unwrap();
let stored = &mirrored()[0].message;
assert!(
stored.len() < HOST_MIRROR_MAX_BYTES + 64,
"{}",
stored.len()
);
assert!(stored.ends_with(&format!(
"[lean-ctx: truncated, {HOST_MIRROR_MAX_BYTES} bytes omitted]"
)));
assert_eq!(cap_host_message("short", 10), "short");
assert_eq!(
cap_host_message("aé", 2),
"a\n[lean-ctx: truncated, 2 bytes omitted]"
);
}
#[test]
fn a_credential_never_reaches_the_store() {
let _data = crate::core::data_dir::isolated_data_dir();
let _policy = crate::core::policy::runtime::TestPolicyOverride::set(None);
let dir = tempfile::tempdir().unwrap();
let root = dir.path().to_string_lossy().into_owned();
let key = concat!("AK", "IAIOSFODNN7EXAMPLE");
let body = format!("{}aws_key = {key}", "x".repeat(HOST_MIRROR_MAX_BYTES - 16));
mirror_host_message(&message("toolu_s", &body, &root)).unwrap();
let stored = &mirrored()[0].message;
assert!(!stored.contains(key));
assert!(
!stored.contains(&key[..8]),
"no secret prefix left by the cut"
);
let raw = std::fs::read_to_string(
crate::core::data_dir::lean_ctx_data_dir()
.unwrap()
.join("agents/registry.json"),
)
.unwrap();
assert!(!raw.contains(key));
}
}