fix: allow Medulla to message accepted contacts (#4824)

This commit is contained in:
Steven Enamakel
2026-07-13 19:15:42 +04:00
committed by GitHub
parent e38e81d008
commit d8a706fdb6
2 changed files with 54 additions and 3 deletions
@@ -251,6 +251,17 @@ async fn contact_status(agent_id: &str) -> Result<String, String> {
Ok(remote_status(&remote).unwrap_or_else(|| "none".to_string()))
}
/// Whether the peer is an accepted tiny.place contact.
///
/// This is intentionally a live relay check rather than a read of the local
/// orchestration pairing store. An accepted contact is already a
/// user-consented recipient for an outbound message, even when it has not been
/// linked as an inbound orchestration session in the current workspace.
pub(crate) async fn is_accepted_contact(agent_id: &str) -> Result<bool, String> {
let agent_id = normalize_agent_id(agent_id)?;
Ok(contact_status(&agent_id).await? == "accepted")
}
fn remote_status(value: &Value) -> Option<String> {
value
.get("status")
+43 -3
View File
@@ -92,6 +92,14 @@ fn required_str(args: &Value, field: &str) -> Result<String, ToolResult> {
}
}
fn recipient_may_receive_message(
locally_linked: bool,
has_known_session: bool,
accepted_contact: bool,
) -> bool {
locally_linked || has_known_session || accepted_contact
}
// ── Reasoning-core session-history read tools (Master chat) ──────────────────
/// Default / cap on how many messages a `orchestration_read_session` call returns.
@@ -473,15 +481,39 @@ impl Tool for SendToAgentTool {
let workspace = self.config.workspace_dir.clone();
// Guardrail: linked peer OR an existing session with them. Never cold-DM
// an arbitrary new address from this un-gated background origin.
// Guardrail: locally linked peer, accepted tiny.place contact, OR an
// existing session with them. Never cold-DM an arbitrary new address
// from this un-gated background origin. The live contact check matters
// after login changes the workspace: the relay contact remains accepted
// while the workspace-local orchestration pairing/session stores start
// empty.
let linked =
crate::openhuman::agent_orchestration::pairing::linked_agent_ids(&workspace).await;
let known_session = store::with_connection(&workspace, |conn| {
store::latest_session_for_agent(conn, &recipient)
})
.map_err(|e| anyhow::anyhow!("lookup session: {e}"))?;
if !linked.contains(&recipient) && known_session.is_none() {
let accepted_contact = if linked.contains(&recipient) || known_session.is_some() {
false
} else {
match crate::openhuman::agent_orchestration::pairing::is_accepted_contact(&recipient)
.await
{
Ok(accepted) => accepted,
Err(e) => {
log::warn!(
target: "orchestration",
"[orchestration] tool.send_to_agent contact check failed: {e}"
);
false
}
}
};
if !recipient_may_receive_message(
linked.contains(&recipient),
known_session.is_some(),
accepted_contact,
) {
log::debug!(
target: "orchestration",
"[orchestration] tool.send_to_agent refused unlinked recipient",
@@ -827,4 +859,12 @@ mod tests {
assert!(out.is_error);
assert!(out.text().contains("not a linked"));
}
#[test]
fn accepted_contact_can_receive_without_workspace_pairing_or_session() {
assert!(recipient_may_receive_message(false, false, true));
assert!(recipient_may_receive_message(true, false, false));
assert!(recipient_may_receive_message(false, true, false));
assert!(!recipient_may_receive_message(false, false, false));
}
}