use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub const PEER_ATTESTATION_ENV: &str = "AI_MEMORY_FED_PEER_ATTESTATION";
pub const TRUST_BODY_AGENT_ID_ENV: &str = "AI_MEMORY_FED_TRUST_BODY_AGENT_ID";
pub const SYNC_TRUST_PEER_ENV: &str = "AI_MEMORY_FED_SYNC_TRUST_PEER";
pub const PEER_ID_HEADER: &str = "x-peer-id";
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct PeerScope {
#[serde(default)]
pub allowed_sender_agent_ids: Vec<String>,
#[serde(default)]
pub allowed_namespaces: Vec<String>,
}
#[derive(Clone, Debug, Default)]
pub struct PeerAttestationConfig {
pub peers: HashMap<String, PeerScope>,
}
#[derive(Debug, Clone)]
pub enum AttestError {
HeaderMissing,
Mismatch {
claimed: String,
peer_header: String,
},
}
impl AttestError {
#[must_use]
pub fn tag(&self) -> &'static str {
match self {
Self::HeaderMissing => "peer_id_header_missing",
Self::Mismatch { .. } => "sender_agent_id_mismatch",
}
}
}
impl PeerAttestationConfig {
#[must_use]
pub fn from_env() -> Self {
match std::env::var(PEER_ATTESTATION_ENV) {
Ok(s) if !s.trim().is_empty() => {
match serde_json::from_str::<HashMap<String, PeerScope>>(&s) {
Ok(peers) => Self { peers },
Err(e) => {
tracing::warn!(
target: "federation::peer_attestation",
env = PEER_ATTESTATION_ENV,
error = %e,
"failed to parse peer-attestation env var as JSON — \
falling back to empty allowlist (default-deny on \
/sync/since, header-must-equal-body on /sync/push)"
);
Self::default()
}
}
}
_ => Self::default(),
}
}
#[must_use]
pub fn scope_for(&self, peer_id: &str) -> Option<&PeerScope> {
self.peers.get(peer_id)
}
#[must_use]
pub fn has_allowlist(&self) -> bool {
!self.peers.is_empty()
}
}
#[must_use]
pub fn trust_body_agent_id_bypass() -> bool {
matches!(std::env::var(TRUST_BODY_AGENT_ID_ENV).as_deref(), Ok("1"))
}
#[must_use]
pub fn sync_trust_peer_bypass() -> bool {
matches!(std::env::var(SYNC_TRUST_PEER_ENV).as_deref(), Ok("1"))
}
pub fn attest_sender(
peer_header: Option<&str>,
body_sender: Option<&str>,
config: &PeerAttestationConfig,
) -> Result<(), AttestError> {
let peer = match peer_header.map(str::trim).filter(|s| !s.is_empty()) {
Some(p) => p,
None => return Err(AttestError::HeaderMissing),
};
let claimed = match body_sender.map(str::trim).filter(|s| !s.is_empty()) {
Some(c) => c,
None => return Ok(()),
};
if claimed == peer {
return Ok(());
}
if let Some(scope) = config.scope_for(peer)
&& scope
.allowed_sender_agent_ids
.iter()
.any(|a| a.as_str() == claimed)
{
return Ok(());
}
Err(AttestError::Mismatch {
claimed: claimed.to_string(),
peer_header: peer.to_string(),
})
}
#[must_use]
pub fn namespace_allowed_test_glob(pattern: &str, target: &str) -> bool {
glob_match(pattern, target)
}
#[must_use]
fn glob_match(pattern: &str, target: &str) -> bool {
if pattern == "**" || pattern == "*" {
return true;
}
if let Some(prefix) = pattern.strip_suffix("/**") {
return target == prefix || target.starts_with(&format!("{prefix}/"));
}
if let Some(prefix) = pattern.strip_suffix("/*") {
if let Some(rest) = target.strip_prefix(&format!("{prefix}/")) {
return !rest.contains('/');
}
return false;
}
if let Some(suffix) = pattern.strip_prefix("*/") {
if let Some(rest) = target.strip_suffix(&format!("/{suffix}")) {
return !rest.contains('/');
}
return false;
}
pattern == target
}
#[must_use]
pub fn namespace_allowed(
peer_header: Option<&str>,
namespace: &str,
config: &PeerAttestationConfig,
) -> bool {
let Some(peer) = peer_header.map(str::trim).filter(|s| !s.is_empty()) else {
return sync_trust_peer_bypass();
};
match config.scope_for(peer) {
Some(scope) => scope
.allowed_namespaces
.iter()
.any(|p| glob_match(p, namespace)),
None => sync_trust_peer_bypass(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg(rows: &[(&str, PeerScope)]) -> PeerAttestationConfig {
let peers = rows
.iter()
.map(|(k, v)| ((*k).to_string(), v.clone()))
.collect();
PeerAttestationConfig { peers }
}
#[test]
fn attest_header_missing_errors() {
let cfg = PeerAttestationConfig::default();
let err = attest_sender(None, Some("alice"), &cfg).unwrap_err();
assert!(matches!(err, AttestError::HeaderMissing));
assert_eq!(err.tag(), "peer_id_header_missing");
}
#[test]
fn attest_header_empty_treated_as_missing() {
let cfg = PeerAttestationConfig::default();
let err = attest_sender(Some(" "), Some("alice"), &cfg).unwrap_err();
assert!(matches!(err, AttestError::HeaderMissing));
}
#[test]
fn attest_body_missing_passes_legacy_unauthored() {
let cfg = PeerAttestationConfig::default();
attest_sender(Some("peer-1"), None, &cfg).unwrap();
attest_sender(Some("peer-1"), Some(""), &cfg).unwrap();
}
#[test]
fn attest_self_authoring_passes() {
let cfg = PeerAttestationConfig::default();
attest_sender(Some("peer-1"), Some("peer-1"), &cfg).unwrap();
}
#[test]
fn attest_mismatch_no_allowlist_errors() {
let cfg = PeerAttestationConfig::default();
let err = attest_sender(Some("peer-1"), Some("alice"), &cfg).unwrap_err();
match err {
AttestError::Mismatch {
claimed,
peer_header,
} => {
assert_eq!(claimed, "alice");
assert_eq!(peer_header, "peer-1");
}
other => panic!("expected Mismatch, got: {other:?}"),
}
}
#[test]
fn attest_mismatch_with_matching_allowlist_passes() {
let cfg = cfg(&[(
"peer-1",
PeerScope {
allowed_sender_agent_ids: vec!["alice".to_string(), "bob".to_string()],
..PeerScope::default()
},
)]);
attest_sender(Some("peer-1"), Some("alice"), &cfg).unwrap();
attest_sender(Some("peer-1"), Some("bob"), &cfg).unwrap();
}
#[test]
fn attest_mismatch_outside_allowlist_errors() {
let cfg = cfg(&[(
"peer-1",
PeerScope {
allowed_sender_agent_ids: vec!["alice".to_string()],
..PeerScope::default()
},
)]);
let err = attest_sender(Some("peer-1"), Some("eve"), &cfg).unwrap_err();
assert!(matches!(err, AttestError::Mismatch { .. }));
}
#[test]
fn glob_wildcard_all() {
assert!(glob_match("*", "anything"));
assert!(glob_match("**", "anything/even/nested"));
}
#[test]
fn glob_prefix_double_star() {
assert!(glob_match("public/**", "public"));
assert!(glob_match("public/**", "public/a"));
assert!(glob_match("public/**", "public/a/b/c"));
assert!(!glob_match("public/**", "private"));
assert!(!glob_match("public/**", "publicx"));
}
#[test]
fn glob_prefix_single_star() {
assert!(glob_match("public/*", "public/foo"));
assert!(!glob_match("public/*", "public/foo/bar"));
assert!(!glob_match("public/*", "public"));
}
#[test]
fn glob_suffix_single_star() {
assert!(glob_match("*/notes", "alice/notes"));
assert!(!glob_match("*/notes", "alice/team/notes"));
assert!(!glob_match("*/notes", "notes"));
}
#[test]
fn glob_exact_literal() {
assert!(glob_match("ai-memory-mcp", "ai-memory-mcp"));
assert!(!glob_match("ai-memory-mcp", "ai-memory"));
}
#[test]
fn namespace_no_header_no_bypass_denies() {
unsafe { std::env::remove_var(SYNC_TRUST_PEER_ENV) };
let cfg = PeerAttestationConfig::default();
assert!(!namespace_allowed(None, "any", &cfg));
assert!(!namespace_allowed(Some(""), "any", &cfg));
}
#[test]
fn namespace_match_via_glob() {
let cfg = cfg(&[(
"peer-1",
PeerScope {
allowed_namespaces: vec!["public/*".to_string(), "shared/team-x/**".to_string()],
..PeerScope::default()
},
)]);
assert!(namespace_allowed(Some("peer-1"), "public/foo", &cfg));
assert!(namespace_allowed(Some("peer-1"), "shared/team-x/a/b", &cfg));
assert!(!namespace_allowed(Some("peer-1"), "private/foo", &cfg));
assert!(!namespace_allowed(Some("peer-1"), "public/foo/bar", &cfg));
}
#[test]
fn namespace_no_scope_row_denies_without_bypass() {
unsafe { std::env::remove_var(SYNC_TRUST_PEER_ENV) };
let cfg = PeerAttestationConfig::default();
assert!(!namespace_allowed(Some("peer-1"), "any", &cfg));
}
static ENV_GUARD: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn lock_env() -> std::sync::MutexGuard<'static, ()> {
ENV_GUARD
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
#[test]
fn from_env_absent_is_empty() {
let _g = lock_env();
unsafe { std::env::remove_var(PEER_ATTESTATION_ENV) };
let cfg = PeerAttestationConfig::from_env();
assert!(cfg.peers.is_empty());
}
#[test]
fn has_allowlist_false_when_zero_config_1056() {
let _g = lock_env();
unsafe { std::env::remove_var(PEER_ATTESTATION_ENV) };
let cfg = PeerAttestationConfig::from_env();
assert!(
!cfg.has_allowlist(),
"#1056: zero-config PeerAttestationConfig MUST report has_allowlist()=false"
);
}
#[test]
fn has_allowlist_true_when_peers_enrolled_1056() {
let _g = lock_env();
let body = r#"{"enrolled-peer": {"allowed_namespaces": ["ns/*"]}}"#;
unsafe { std::env::set_var(PEER_ATTESTATION_ENV, body) };
let cfg = PeerAttestationConfig::from_env();
unsafe { std::env::remove_var(PEER_ATTESTATION_ENV) };
assert!(
cfg.has_allowlist(),
"#1056: configured PeerAttestationConfig MUST report has_allowlist()=true"
);
assert!(cfg.scope_for("enrolled-peer").is_some());
assert!(
cfg.scope_for("not-in-map").is_none(),
"#1056: unknown peer MUST return None (handlers refuse)"
);
}
#[test]
fn from_env_parses_valid_json() {
let _g = lock_env();
let body = r#"{
"peer-1": {
"allowed_sender_agent_ids": ["alice", "bob"],
"allowed_namespaces": ["public/*"]
}
}"#;
unsafe { std::env::set_var(PEER_ATTESTATION_ENV, body) };
let cfg = PeerAttestationConfig::from_env();
unsafe { std::env::remove_var(PEER_ATTESTATION_ENV) };
let scope = cfg.scope_for("peer-1").expect("peer-1 row present");
assert_eq!(scope.allowed_sender_agent_ids, vec!["alice", "bob"]);
assert_eq!(scope.allowed_namespaces, vec!["public/*"]);
}
#[test]
fn from_env_parse_error_is_empty() {
let _g = lock_env();
unsafe { std::env::set_var(PEER_ATTESTATION_ENV, "not json{{") };
let cfg = PeerAttestationConfig::from_env();
unsafe { std::env::remove_var(PEER_ATTESTATION_ENV) };
assert!(cfg.peers.is_empty());
}
}