mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(orchestration): recognize linked agents regardless of agent-id encoding (#4627)
This commit is contained in:
@@ -4,8 +4,11 @@
|
||||
//! websocket recv loop), filtered to conversation/DM streams. Never logs message
|
||||
//! bodies or seeds.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
|
||||
use base64::Engine as _;
|
||||
|
||||
use crate::core::event_bus::{publish_global, DomainEvent};
|
||||
use crate::openhuman::config::Config;
|
||||
use crate::openhuman::tinyplace::{acknowledge_message, decrypt_envelope};
|
||||
@@ -15,6 +18,49 @@ use super::types::{ChatKind, OrchestrationMessage, OrchestrationSession, Session
|
||||
|
||||
const LOG: &str = "orchestration";
|
||||
|
||||
/// True when the DM sender `from` is one of the linked (paired) agents.
|
||||
///
|
||||
/// tiny.place identifies the *same* Ed25519 key two ways: the orchestration
|
||||
/// pairing store keeps the **base58** Solana address (that's what the
|
||||
/// contacts/directory API returns and what the pairing UI stores), while an
|
||||
/// inbound `MessageEnvelope.from` carries the **base64** Ed25519 public key (the
|
||||
/// relay's raw-key form). A plain string compare therefore treats a
|
||||
/// legitimately-linked agent as unpaired and silently drops every DM it sends —
|
||||
/// so the message never lands in the orchestration view. Compare by the decoded
|
||||
/// 32-byte key so the two encodings unify. Fall back to the exact-string check
|
||||
/// first (cheap, and covers ids that aren't 32-byte keys, e.g. a handle).
|
||||
fn agent_id_in_linked_set(from: &str, linked: &HashSet<String>) -> bool {
|
||||
if linked.contains(from) {
|
||||
return true;
|
||||
}
|
||||
let Some(from_key) = decode_agent_key(from) else {
|
||||
return false;
|
||||
};
|
||||
linked
|
||||
.iter()
|
||||
.any(|id| decode_agent_key(id) == Some(from_key))
|
||||
}
|
||||
|
||||
/// Decode a tiny.place agent identifier to its raw 32-byte Ed25519 public key,
|
||||
/// accepting either the base64 (envelope `from`) or base58 (pairing store)
|
||||
/// encoding. Returns `None` for anything that isn't a 32-byte key (handles,
|
||||
/// malformed ids) so callers fall back to a string compare.
|
||||
fn decode_agent_key(value: &str) -> Option<[u8; 32]> {
|
||||
// base64 (Ed25519 raw key) — the inbound `MessageEnvelope.from` form.
|
||||
if let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(value) {
|
||||
if let Ok(arr) = <[u8; 32]>::try_from(bytes.as_slice()) {
|
||||
return Some(arr);
|
||||
}
|
||||
}
|
||||
// base58 (Solana address) — the pairing-store form.
|
||||
if let Ok(bytes) = bs58::decode(value).into_vec() {
|
||||
if let Ok(arr) = <[u8; 32]>::try_from(bytes.as_slice()) {
|
||||
return Some(arr);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// A decrypted DM turned into the fields we persist. Pure result of
|
||||
/// [`classify_message`] — no IO, so it is unit-testable without a live client.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -201,8 +247,12 @@ async fn ingest_one(
|
||||
// Messaging UI via messages.list / signal.decryptMessage.
|
||||
let linked =
|
||||
crate::openhuman::agent_orchestration::pairing::linked_agent_ids(&workspace_dir).await;
|
||||
if !linked.contains(&agent_id) {
|
||||
log::debug!(target: LOG, "[orchestration] ingest.skip_unpaired from={agent_id}");
|
||||
if !agent_id_in_linked_set(&agent_id, &linked) {
|
||||
log::debug!(
|
||||
target: LOG,
|
||||
"[orchestration] ingest.skip_unpaired from={agent_id} linked_count={}",
|
||||
linked.len()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -336,6 +386,46 @@ mod tests {
|
||||
assert!(!is_dm_stream("inbox", "inbox"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linked_gate_unifies_base58_and_base64_of_same_key() {
|
||||
// Real pair observed in the wild: the pairing store holds the base58
|
||||
// Solana address; the inbound `MessageEnvelope.from` holds the base64
|
||||
// Ed25519 public key. Both are the same 32-byte key, so a linked agent
|
||||
// must be recognised regardless of which encoding arrives.
|
||||
let base58 = "7jr5FKYETssD6T1MCzsR4aT4dnjjyJCE2SANYYX1R5vm";
|
||||
let base64 = "ZCAAuA+2GVoRrT08Gt8JUVnxnISTelSxnDuyScze334=";
|
||||
assert_eq!(
|
||||
decode_agent_key(base58),
|
||||
decode_agent_key(base64),
|
||||
"base58 and base64 forms must decode to the same key"
|
||||
);
|
||||
|
||||
// Pairing store keeps the base58 address; the DM arrives base64.
|
||||
let linked: HashSet<String> = [base58.to_string()].into_iter().collect();
|
||||
assert!(
|
||||
agent_id_in_linked_set(base64, &linked),
|
||||
"a base64 `from` must match a base58-stored linked agent"
|
||||
);
|
||||
|
||||
// Exact-string match still works (both base58), and the reverse too.
|
||||
assert!(agent_id_in_linked_set(base58, &linked));
|
||||
let linked_b64: HashSet<String> = [base64.to_string()].into_iter().collect();
|
||||
assert!(agent_id_in_linked_set(base58, &linked_b64));
|
||||
|
||||
// An unrelated key is still rejected.
|
||||
let other = "De6RHrMj6eDqX1WBTXk11sks4WXHMaqEX9A6oQ3ZEmsg";
|
||||
assert!(!agent_id_in_linked_set(other, &linked));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linked_gate_falls_back_to_string_for_non_key_ids() {
|
||||
// A handle (not a 32-byte key) can only match by exact string.
|
||||
let linked: HashSet<String> = ["@codex-handle".to_string()].into_iter().collect();
|
||||
assert!(agent_id_in_linked_set("@codex-handle", &linked));
|
||||
assert!(!agent_id_in_linked_set("@other-handle", &linked));
|
||||
assert!(decode_agent_key("@codex-handle").is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn drain_is_a_noop_when_orchestration_disabled() {
|
||||
// Guard short-circuits before touching the tiny.place client, so this
|
||||
|
||||
Reference in New Issue
Block a user