#[cfg(test)]
mod tests {
use std::sync::Arc;
use hirn_core::HirnConfig;
use hirn_core::episodic::EpisodicRecord;
use hirn_core::id::MemoryId;
use hirn_core::semantic::SemanticRecord;
use hirn_core::types::{AgentId, EventType, Namespace};
use hirn_engine::event::MemoryEvent;
use hirn_engine::event_log::{EventFilter, EventLog};
use hirn_engine::policy::{Action, AuthzRequest, PolicyEngine};
use hirn_engine::{
HirnDB, SemanticMerge, SemanticRetraction, SemanticSupersession, SemanticUpdate,
};
use hirn_storage::{HirnDb, HirnDbConfig, PhysicalStore};
const DIM: usize = 32;
fn ns(s: &str) -> Namespace {
Namespace::new(s).unwrap()
}
fn rand_vec(dim: usize, seed: u128) -> Vec<f32> {
(0..dim)
.map(|i| (seed as f64).mul_add(0.618_033, i as f64 * 0.414_213).sin() as f32)
.collect()
}
const TENANT_POLICIES: &str = r#"
// Finance team can access finance realm
permit(
principal in Hirn::Team::"finance-team",
action in [Hirn::Action::"remember", Hirn::Action::"recall",
Hirn::Action::"think", Hirn::Action::"connect",
Hirn::Action::"watch"],
resource in Hirn::Realm::"finance"
);
// Healthcare team can access healthcare realm
permit(
principal in Hirn::Team::"healthcare-team",
action in [Hirn::Action::"remember", Hirn::Action::"recall",
Hirn::Action::"think", Hirn::Action::"connect",
Hirn::Action::"watch"],
resource in Hirn::Realm::"healthcare"
);
// Engineering team can access engineering realm
permit(
principal in Hirn::Team::"engineering-team",
action in [Hirn::Action::"remember", Hirn::Action::"recall",
Hirn::Action::"think", Hirn::Action::"connect",
Hirn::Action::"watch"],
resource in Hirn::Realm::"engineering"
);
// Only admins can consolidate, forget, or admin
permit(
principal in Hirn::Team::"admins",
action,
resource
);
// ABAC: block agents with low reputation from writing
forbid(
principal,
action in [Hirn::Action::"remember", Hirn::Action::"connect"],
resource
) when { principal.reputation < 50 };
// Restricted namespaces require admin
forbid(
principal,
action,
resource
) when { resource.classification == "restricted" }
unless { principal in Hirn::Team::"admins" };
"#;
const SEMANTIC_EDIT_POLICIES: &str = r#"
permit(
principal == Hirn::Agent::"writer",
action == Hirn::Action::"remember",
resource in Hirn::Realm::"semantic-security"
);
permit(
principal == Hirn::Agent::"source_writer",
action == Hirn::Action::"remember",
resource in Hirn::Realm::"semantic-security"
);
permit(
principal == Hirn::Agent::"editor",
action in [
Hirn::Action::"correct",
Hirn::Action::"supersede",
Hirn::Action::"merge",
Hirn::Action::"retract"
],
resource in Hirn::Realm::"semantic-security"
);
permit(
principal == Hirn::Agent::"purger",
action == Hirn::Action::"purge",
resource in Hirn::Realm::"semantic-security"
);
"#;
fn create_policy_engine() -> PolicyEngine {
let engine = PolicyEngine::new(
hirn_engine::policy::DEFAULT_SCHEMA,
&[("security-test.cedar", TENANT_POLICIES)],
)
.expect("valid policy");
engine
.register_team("finance-team", "Finance team", None)
.unwrap();
engine
.register_team("healthcare-team", "Healthcare team", None)
.unwrap();
engine
.register_team("engineering-team", "Engineering team", None)
.unwrap();
engine
.register_team("admins", "Administrators", None)
.unwrap();
engine
.register_agent(
"agent-finance",
80,
"2024-01-01T00:00:00Z",
&["finance-team"],
)
.unwrap();
engine
.register_agent(
"agent-health",
75,
"2024-01-01T00:00:00Z",
&["healthcare-team"],
)
.unwrap();
engine
.register_agent(
"agent-eng",
90,
"2024-01-01T00:00:00Z",
&["engineering-team"],
)
.unwrap();
engine
.register_agent("agent-admin", 100, "2024-01-01T00:00:00Z", &["admins"])
.unwrap();
engine.register_realm("finance", "Finance realm").unwrap();
engine
.register_realm("healthcare", "Healthcare realm")
.unwrap();
engine
.register_realm("engineering", "Engineering realm")
.unwrap();
engine
.register_namespace("fin-default", "public", "finance")
.unwrap();
engine
.register_namespace("health-default", "public", "healthcare")
.unwrap();
engine
.register_namespace("eng-default", "public", "engineering")
.unwrap();
engine
.register_namespace("fin-secrets", "restricted", "finance")
.unwrap();
engine
}
fn create_semantic_edit_policy_engine() -> PolicyEngine {
let engine = PolicyEngine::new(
hirn_engine::policy::DEFAULT_SCHEMA,
&[("semantic-edits.cedar", SEMANTIC_EDIT_POLICIES)],
)
.expect("valid semantic edit policy");
for agent in ["writer", "source_writer", "editor", "purger"] {
engine
.register_agent(agent, 100, "2024-01-01T00:00:00Z", &[])
.unwrap();
}
engine
.register_realm("semantic-security", "Semantic edit realm")
.unwrap();
engine
.register_namespace("team_edit", "public", "semantic-security")
.unwrap();
engine
}
async fn create_realm_db(realm: &str, dir: &std::path::Path, engine: PolicyEngine) -> HirnDB {
let db_path = dir.join(format!("{realm}"));
let config = HirnConfig::builder()
.db_path(&db_path)
.embedding_dimensions(DIM as u32)
.default_realm(realm)
.build()
.unwrap();
let storage: Arc<dyn hirn_storage::PhysicalStore> =
Arc::new(hirn_storage::memory_store::MemoryStore::new());
let mut db = HirnDB::open_with_config(config, storage).await.unwrap();
db.set_policy_engine(engine);
db
}
async fn create_realm_db_with_event_log(
realm: &str,
dir: &std::path::Path,
engine: PolicyEngine,
) -> (HirnDB, Arc<EventLog>) {
let db_path = dir.join(format!("{realm}"));
let lance_path = dir.join(format!("{realm}_lance"));
let lance_config = HirnDbConfig::local(lance_path.to_str().unwrap());
let backend = HirnDb::open(lance_config.clone()).await.unwrap();
let storage: Arc<dyn PhysicalStore> = backend.store_arc();
let config = HirnConfig::builder()
.db_path(&db_path)
.embedding_dimensions(DIM as u32)
.default_realm(realm)
.build()
.unwrap();
let mut db = HirnDB::open_with_config(config, Arc::clone(&storage))
.await
.unwrap();
db.set_policy_engine(engine);
let log = Arc::new(EventLog::open(storage).await.unwrap());
db.set_event_log(Arc::clone(&log));
(db, log)
}
fn make_episode_ns(agent: &str, content: &str, seed: u128, ns_str: &str) -> EpisodicRecord {
EpisodicRecord::builder()
.content(content)
.agent_id(AgentId::new(agent).unwrap())
.event_type(EventType::Observation)
.embedding(rand_vec(DIM, seed))
.namespace(ns(ns_str))
.build()
.unwrap()
}
fn make_semantic_ns(
agent: &str,
concept: &str,
description: &str,
ns_str: &str,
) -> SemanticRecord {
SemanticRecord::builder()
.concept(concept)
.description(description)
.agent_id(AgentId::new(agent).unwrap())
.namespace(ns(ns_str))
.build()
.unwrap()
}
#[tokio::test(flavor = "multi_thread")]
async fn multi_tenant_realm_isolation() {
let dir = tempfile::tempdir().unwrap();
let engine = create_policy_engine();
let db_finance = create_realm_db("finance", dir.path(), engine).await;
let record = make_episode_ns("agent-finance", "Q3 revenue is $10M", 1, "fin-default");
let _id = db_finance.episodic().remember(record).await.unwrap();
let query = rand_vec(DIM, 1);
let result = db_finance
.recall_view()
.query(query.clone())
.agent_id("agent-health")
.namespace(ns("fin-default"))
.execute()
.await;
assert!(
result.is_err(),
"healthcare agent must not access finance realm"
);
let err = result.unwrap_err();
assert!(
matches!(err, hirn_core::HirnError::AccessDenied(_)),
"expected AccessDenied, got: {err:?}"
);
let record = make_episode_ns("agent-eng", "should not be stored", 2, "fin-default");
let result = db_finance.episodic().remember(record).await;
assert!(
result.is_err(),
"engineering agent must not write to finance realm"
);
assert!(matches!(
result.unwrap_err(),
hirn_core::HirnError::AccessDenied(_)
));
let result = db_finance
.recall_view()
.query(query)
.agent_id("agent-finance")
.namespace(ns("fin-default"))
.execute()
.await;
assert!(
result.is_ok(),
"finance agent should be allowed to recall from own realm: {result:?}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn semantic_edit_actions_are_distinct_from_remember_and_purge() {
let dir = tempfile::tempdir().unwrap();
let engine = create_semantic_edit_policy_engine();
let db = create_realm_db("semantic-security", dir.path(), engine).await;
let writer = AgentId::new("writer").unwrap();
let source_writer = AgentId::new("source_writer").unwrap();
let editor = AgentId::new("editor").unwrap();
let purger = AgentId::new("purger").unwrap();
for (agent_id, label) in [
(&writer, "Writer"),
(&source_writer, "Source writer"),
(&editor, "Editor"),
(&purger, "Purger"),
] {
db.register_agent(agent_id, label).await.unwrap();
}
db.create_team_namespace(
"team_edit",
vec![
writer.clone(),
source_writer.clone(),
editor.clone(),
purger.clone(),
],
)
.await
.unwrap();
let target_id = db
.semantic()
.store(make_semantic_ns(
"writer",
"lease_policy",
"lease policy v1",
"team_edit",
))
.await
.unwrap();
let source_id = db
.semantic()
.store(make_semantic_ns(
"source_writer",
"lease_policy",
"lease policy corroboration",
"team_edit",
))
.await
.unwrap();
let writer_ctx = db.as_agent(&writer).await.unwrap();
let editor_ctx = db.as_agent(&editor).await.unwrap();
let purger_ctx = db.as_agent(&purger).await.unwrap();
let mut current_target_id = target_id;
let mut unauthorized_update = SemanticUpdate::with_metadata(writer, MemoryId::new());
unauthorized_update.description = Some("writer edit".into());
unauthorized_update.reason = Some("unauthorized".into());
let err = writer_ctx
.correct_semantic(target_id, unauthorized_update)
.await
.unwrap_err();
assert!(
matches!(err, hirn_core::HirnError::AccessDenied(_)),
"remember permission must not imply correct permission: {err:?}"
);
let mut correction = SemanticUpdate::with_metadata(editor, MemoryId::new());
correction.description = Some("lease policy v2".into());
correction.reason = Some("reviewed".into());
let corrected = editor_ctx
.correct_semantic(current_target_id, correction)
.await
.unwrap();
current_target_id = corrected.id;
let mut supersession = SemanticSupersession::with_metadata(editor, MemoryId::new());
supersession.description = Some("lease policy v3".into());
supersession.reason = Some("authoritative cutover".into());
let superseded = editor_ctx
.supersede_semantic(current_target_id, supersession)
.await
.unwrap();
current_target_id = superseded.id;
let mut merge = SemanticMerge::with_metadata(editor, MemoryId::new());
merge.source_ids = vec![source_id];
merge.description = Some("lease policy canonical".into());
merge.reason = Some("dedupe".into());
let merged = editor_ctx
.merge_semantic(current_target_id, merge)
.await
.unwrap();
current_target_id = merged.target.id;
let mut retraction = SemanticRetraction::with_metadata(editor, MemoryId::new());
retraction.reason = Some("withdrawn".into());
let retracted = editor_ctx
.retract_semantic(current_target_id, retraction)
.await
.unwrap();
current_target_id = retracted.id;
let history = db.semantic().history(current_target_id).await.unwrap();
let tombstone = history.last().unwrap();
assert!(tombstone.is_retracted());
assert_eq!(tombstone.provenance.created_by, editor);
assert_eq!(tombstone.revision_reason.as_deref(), Some("withdrawn"));
let purge_err = editor_ctx
.purge_semantic(current_target_id)
.await
.unwrap_err();
assert!(
matches!(purge_err, hirn_core::HirnError::AccessDenied(_)),
"retract permission must not imply purge permission: {purge_err:?}"
);
purger_ctx.purge_semantic(current_target_id).await.unwrap();
assert!(db.semantic().history(current_target_id).await.is_err());
}
#[tokio::test(flavor = "multi_thread")]
async fn cross_realm_deny_with_diagnostics() {
let dir = tempfile::tempdir().unwrap();
let engine = create_policy_engine();
let db_health = create_realm_db("healthcare", dir.path(), engine.clone()).await;
let record = make_episode_ns(
"agent-health",
"Patient X has condition Y",
10,
"health-default",
);
db_health.episodic().remember(record).await.unwrap();
let query = rand_vec(DIM, 10);
let err = db_health
.recall_view()
.query(query)
.agent_id("agent-finance")
.namespace(ns("health-default"))
.execute()
.await
.unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("agent-finance"),
"diagnostics should name the agent: {msg}"
);
assert!(
msg.contains("healthcare") || msg.contains("recall"),
"diagnostics should mention realm or action: {msg}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn audit_trail_traces_all_operations() {
let dir = tempfile::tempdir().unwrap();
let engine = create_policy_engine();
let (db, log) = create_realm_db_with_event_log("finance", dir.path(), engine).await;
let record = make_episode_ns("agent-finance", "authorized write", 20, "fin-default");
db.episodic().remember(record).await.unwrap();
let record = make_episode_ns("agent-health", "unauthorized attempt", 21, "fin-default");
let _ = db.episodic().remember(record).await;
let query = rand_vec(DIM, 20);
let _ = db
.recall_view()
.query(query)
.agent_id("agent-eng")
.namespace(ns("fin-default"))
.execute()
.await;
let all = log.read_all().await.unwrap();
assert!(!all.is_empty(), "audit log should contain events");
let granted = log
.read_with_filter(&EventFilter {
event_type: Some("access_granted".into()),
..Default::default()
})
.await
.unwrap();
assert!(
!granted.is_empty(),
"authorized operations should produce AccessGranted events"
);
if let MemoryEvent::AccessGranted {
action,
realm,
policy_ids,
..
} = &granted[0].event
{
assert_eq!(action, "remember");
assert_eq!(realm, "finance");
let _ = policy_ids;
} else {
panic!("expected AccessGranted, got: {:?}", granted[0].event);
}
let denied = log
.read_with_filter(&EventFilter {
event_type: Some("access_denied".into()),
..Default::default()
})
.await
.unwrap();
assert!(
denied.len() >= 2,
"should have at least 2 deny events (health write + eng recall), got {}",
denied.len()
);
for envelope in &denied {
if let MemoryEvent::AccessDenied {
action,
realm,
reasons,
..
} = &envelope.event
{
assert_eq!(realm, "finance");
assert!(
action == "remember" || action == "recall",
"unexpected denied action: {action}"
);
let _ = reasons; } else {
panic!("expected AccessDenied, got: {:?}", envelope.event);
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn hmac_tamper_detection() {
let dir = tempfile::tempdir().unwrap();
let lance_path = dir.path().join("hmac_lance");
let lance_config = HirnDbConfig::local(lance_path.to_str().unwrap());
let backend = HirnDb::open(lance_config.clone()).await.unwrap();
let storage: Arc<dyn PhysicalStore> = backend.store_arc();
let log = EventLog::open(storage).await.unwrap();
let secret = b"realm-secret-key-for-audit-integrity";
for i in 0u128..10 {
let agent = match i % 3 {
0 => "agent-finance",
1 => "agent-health",
_ => "agent-eng",
};
let event = MemoryEvent::EpisodeCreated {
id: hirn_core::id::MemoryId::new(),
content_preview: format!("event {i} from {agent}"),
};
log.append_signed(event, "production", "default", agent, secret)
.await
.unwrap();
}
let failures = log.verify_integrity(secret).await.unwrap();
assert!(
failures.is_empty(),
"no tampered events expected, got: {failures:?}"
);
let wrong_failures = log.verify_integrity(b"wrong-secret").await.unwrap();
assert_eq!(
wrong_failures.len(),
10,
"wrong secret should detect all events as tampered"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn abac_reputation_change() {
let dir = tempfile::tempdir().unwrap();
let engine = create_policy_engine();
engine
.register_agent(
"agent-low-rep",
25,
"2025-01-01T00:00:00Z",
&["finance-team"],
)
.unwrap();
let db = create_realm_db("finance", dir.path(), engine.clone()).await;
let record = make_episode_ns("agent-low-rep", "attempt with low rep", 30, "fin-default");
let result = db.episodic().remember(record).await;
assert!(result.is_err(), "low-rep agent should be denied writing");
assert!(matches!(
result.unwrap_err(),
hirn_core::HirnError::AccessDenied(_)
));
let query = rand_vec(DIM, 30);
let result = db
.recall_view()
.query(query)
.agent_id("agent-low-rep")
.namespace(ns("fin-default"))
.execute()
.await;
assert!(
result.is_ok(),
"low-rep agent should be able to recall: {result:?}"
);
engine
.register_agent(
"agent-low-rep",
80,
"2025-01-01T00:00:00Z",
&["finance-team"],
)
.unwrap();
let record = make_episode_ns("agent-low-rep", "attempt with high rep", 31, "fin-default");
let result = db.episodic().remember(record).await;
assert!(
result.is_ok(),
"high-rep agent should be allowed to write: {result:?}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn thousand_operations_mixed_agents_all_enforced() {
let dir = tempfile::tempdir().unwrap();
let engine = create_policy_engine();
let db_finance = create_realm_db("finance", dir.path(), engine.clone()).await;
let agents = ["agent-finance", "agent-health", "agent-eng", "agent-admin"];
let mut allowed_count = 0u32;
let mut denied_count = 0u32;
for i in 0u128..1000 {
let agent = agents[(i % 4) as usize];
let record = make_episode_ns(agent, &format!("event {i}"), i, "fin-default");
match db_finance.episodic().remember(record).await {
Ok(_) => allowed_count += 1,
Err(hirn_core::HirnError::AccessDenied(_)) => denied_count += 1,
Err(e) => panic!("unexpected error on iteration {i}: {e:?}"),
}
}
assert_eq!(
allowed_count, 500,
"finance + admin agents (500 each of 1000) should be allowed, got {allowed_count}"
);
assert_eq!(
denied_count, 500,
"health + eng agents (500 each of 1000) should be denied, got {denied_count}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn restricted_namespace_requires_admin() {
let engine = create_policy_engine();
let decision = engine.authorize(&AuthzRequest {
agent_id: "agent-finance".to_string(),
action: Action::Recall,
realm: "finance".to_string(),
namespace: "fin-secrets".to_string(),
});
assert!(
!decision.allowed,
"non-admin should be denied access to restricted namespace"
);
let decision = engine.authorize(&AuthzRequest {
agent_id: "agent-admin".to_string(),
action: Action::Recall,
realm: "finance".to_string(),
namespace: "fin-secrets".to_string(),
});
assert!(
decision.allowed,
"admin should be allowed access to restricted namespace"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn admin_only_operations() {
let dir = tempfile::tempdir().unwrap();
let engine = create_policy_engine();
let db = create_realm_db("finance", dir.path(), engine).await;
let result = db
.admin()
.consolidate()
.agent_id("agent-finance")
.execute()
.await;
assert!(result.is_err(), "non-admin should be denied consolidation");
assert!(matches!(
result.unwrap_err(),
hirn_core::HirnError::AccessDenied(_)
));
let result = db
.admin()
.consolidate()
.agent_id("agent-admin")
.execute()
.await;
assert!(
result.is_ok(),
"admin should be allowed to consolidate: {result:?}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn deny_audit_includes_diagnostics() {
let dir = tempfile::tempdir().unwrap();
let engine = create_policy_engine();
let (db, log) = create_realm_db_with_event_log("finance", dir.path(), engine).await;
let query = rand_vec(DIM, 40);
let _ = db
.recall_view()
.query(query)
.agent_id("agent-health")
.namespace(ns("fin-default"))
.execute()
.await;
let denied = log
.read_with_filter(&EventFilter {
event_type: Some("access_denied".into()),
..Default::default()
})
.await
.unwrap();
assert!(
!denied.is_empty(),
"should have at least one deny event in audit log"
);
let envelope = &denied[0];
assert_eq!(envelope.realm, "finance");
if let MemoryEvent::AccessDenied {
action,
realm,
namespace,
reasons,
policy_ids,
} = &envelope.event
{
assert_eq!(action, "recall");
assert_eq!(realm, "finance");
let _ = namespace;
let has_diagnostics = !reasons.is_empty() || !policy_ids.is_empty();
let _ = has_diagnostics;
} else {
panic!("expected AccessDenied, got: {:?}", envelope.event);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn three_realm_full_isolation_scenario() {
let dir = tempfile::tempdir().unwrap();
let engine = create_policy_engine();
let db_fin = create_realm_db("finance", dir.path(), engine.clone()).await;
let db_health = create_realm_db("healthcare", dir.path(), engine.clone()).await;
let db_eng = create_realm_db("engineering", dir.path(), engine.clone()).await;
for i in 0u128..10 {
db_fin
.episodic()
.remember(make_episode_ns(
"agent-finance",
&format!("fin-{i}"),
100 + i,
"fin-default",
))
.await
.unwrap();
db_health
.episodic()
.remember(make_episode_ns(
"agent-health",
&format!("health-{i}"),
200 + i,
"health-default",
))
.await
.unwrap();
db_eng
.episodic()
.remember(make_episode_ns(
"agent-eng",
&format!("eng-{i}"),
300 + i,
"eng-default",
))
.await
.unwrap();
}
let fin_result = db_fin
.recall_view()
.query(rand_vec(DIM, 100))
.agent_id("agent-finance")
.namespace(ns("fin-default"))
.limit(20)
.execute()
.await;
assert!(
fin_result.is_ok(),
"finance agent should be allowed to recall: {fin_result:?}"
);
let health_result = db_health
.recall_view()
.query(rand_vec(DIM, 200))
.agent_id("agent-health")
.namespace(ns("health-default"))
.limit(20)
.execute()
.await;
assert!(
health_result.is_ok(),
"healthcare agent should be allowed to recall: {health_result:?}"
);
let eng_result = db_eng
.recall_view()
.query(rand_vec(DIM, 300))
.agent_id("agent-eng")
.namespace(ns("eng-default"))
.limit(20)
.execute()
.await;
assert!(
eng_result.is_ok(),
"engineering agent should be allowed to recall: {eng_result:?}"
);
let cross_checks: [(&HirnDB, &str, &str, &str); 6] = [
(&db_fin, "agent-health", "finance", "fin-default"),
(&db_fin, "agent-eng", "finance", "fin-default"),
(&db_health, "agent-finance", "healthcare", "health-default"),
(&db_health, "agent-eng", "healthcare", "health-default"),
(&db_eng, "agent-finance", "engineering", "eng-default"),
(&db_eng, "agent-health", "engineering", "eng-default"),
];
for (db, agent, realm, ns_id) in cross_checks {
let result = db
.recall_view()
.query(rand_vec(DIM, 999))
.agent_id(agent)
.namespace(ns(ns_id))
.execute()
.await;
assert!(
result.is_err(),
"{agent} should be denied access to {realm} realm"
);
assert!(
matches!(result.unwrap_err(), hirn_core::HirnError::AccessDenied(_)),
"{agent} in {realm}: expected AccessDenied"
);
}
}
}