use crate::cli::CliOutput;
use crate::models::field_names;
use crate::{db, models};
use anyhow::Result;
use models::{GovernanceDecision, GovernedAction};
use rusqlite::Connection;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GovernanceOutcome {
Allow,
Pending,
Deny,
}
#[allow(clippy::too_many_arguments)]
pub fn enforce(
conn: &Connection,
action: GovernedAction,
namespace: &str,
caller_agent_id: &str,
memory_id: Option<&str>,
memory_owner: Option<&str>,
payload: &serde_json::Value,
json_out: bool,
out: &mut CliOutput<'_>,
) -> Result<GovernanceOutcome> {
match db::enforce_governance(
conn,
action,
namespace,
caller_agent_id,
memory_id,
memory_owner,
payload,
)? {
GovernanceDecision::Allow => Ok(GovernanceOutcome::Allow),
GovernanceDecision::Deny(refusal) => {
writeln!(
out.stderr,
"{} denied by governance: {reason}",
action.as_str(),
reason = refusal.reason,
)?;
Ok(GovernanceOutcome::Deny)
}
GovernanceDecision::Pending(pending_id) => {
if json_out {
let mut payload_obj = serde_json::json!({
"status": "pending",
(field_names::PENDING_ID): pending_id,
"reason": crate::errors::msg::GOVERNANCE_REQUIRES_APPROVAL,
"action": action.as_str(),
"namespace": namespace,
});
if let Some(mid) = memory_id
&& let Some(obj) = payload_obj.as_object_mut()
{
obj.insert(
"memory_id".to_string(),
serde_json::Value::String(mid.to_string()),
);
}
writeln!(out.stdout, "{payload_obj}")?;
} else if let Some(mid) = memory_id {
writeln!(
out.stdout,
"{} queued for approval: pending_id={pending_id} id={mid}",
action.as_str()
)?;
} else {
writeln!(
out.stdout,
"{} queued for approval: pending_id={pending_id} ns={namespace}",
action.as_str()
)?;
}
Ok(GovernanceOutcome::Pending)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::test_utils::{TestEnv, seed_memory};
use crate::models::{ApproverType, CorePolicy, GovernanceLevel, GovernancePolicy};
fn pin_governance_enforce_for_test() -> std::sync::MutexGuard<'static, ()> {
let guard = crate::config::lock_permissions_mode_for_test();
crate::config::override_active_permissions_mode_for_test(
crate::config::PermissionsMode::Enforce,
);
guard
}
fn seed_governance_policy(
db_path: &std::path::Path,
namespace: &str,
policy: GovernancePolicy,
owner_agent_id: &str,
) {
let conn = db::open(db_path).unwrap();
let now = chrono::Utc::now().to_rfc3339();
let mut metadata = models::default_metadata();
if let Some(obj) = metadata.as_object_mut() {
obj.insert(
"agent_id".to_string(),
serde_json::Value::String(owner_agent_id.to_string()),
);
obj.insert(
"governance".to_string(),
serde_json::to_value(&policy).unwrap(),
);
}
let standard = models::Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: models::Tier::Long,
namespace: format!("_standards-{namespace}"),
title: format!("standard for {namespace}"),
content: "policy".to_string(),
tags: vec![],
priority: 9,
confidence: 1.0,
source: "test".to_string(),
access_count: 0,
created_at: now.clone(),
updated_at: now,
last_accessed_at: None,
expires_at: None,
metadata,
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
let standard_id = db::insert(&conn, &standard).unwrap();
db::set_namespace_standard(&conn, namespace, &standard_id, None).unwrap();
}
#[test]
fn test_governance_allow_returns_allow_no_output() {
let mut env = TestEnv::fresh();
let db_path = env.db_path.clone();
let _ = seed_memory(&db_path, "ns", "x", "y");
let conn = db::open(&db_path).unwrap();
let payload = serde_json::json!({});
let outcome = {
let mut out = env.output();
enforce(
&conn,
GovernedAction::Store,
"ns-without-policy",
"alice",
None,
None,
&payload,
false,
&mut out,
)
.unwrap()
};
assert_eq!(outcome, GovernanceOutcome::Allow);
assert!(env.stdout_str().is_empty());
assert!(env.stderr_str().is_empty());
}
#[test]
fn test_governance_pending_writes_pending_status_text() {
let _gate = pin_governance_enforce_for_test();
let mut env = TestEnv::fresh();
let db_path = env.db_path.clone();
let policy = GovernancePolicy {
core: CorePolicy {
write: GovernanceLevel::Approve,
promote: GovernanceLevel::Any,
delete: GovernanceLevel::Owner,
approver: ApproverType::Human,
inherit: true,
max_reflection_depth: None,
},
..Default::default()
};
seed_governance_policy(&db_path, "gov-ns", policy, "alice");
let conn = db::open(&db_path).unwrap();
let payload = serde_json::json!({"title": "t"});
let outcome = {
let mut out = env.output();
enforce(
&conn,
GovernedAction::Store,
"gov-ns",
"bob",
None,
None,
&payload,
false,
&mut out,
)
.unwrap()
};
assert_eq!(outcome, GovernanceOutcome::Pending);
let stdout = env.stdout_str();
assert!(stdout.contains("queued for approval"), "got: {stdout}");
assert!(stdout.contains("pending_id="), "got: {stdout}");
assert!(stdout.contains("ns=gov-ns"), "got: {stdout}");
}
#[test]
fn test_governance_pending_writes_pending_status_json() {
let _gate = pin_governance_enforce_for_test();
let mut env = TestEnv::fresh();
let db_path = env.db_path.clone();
let policy = GovernancePolicy {
core: CorePolicy {
write: GovernanceLevel::Any,
promote: GovernanceLevel::Any,
delete: GovernanceLevel::Approve,
approver: ApproverType::Human,
inherit: true,
max_reflection_depth: None,
},
..Default::default()
};
seed_governance_policy(&db_path, "gov-ns", policy, "alice");
let conn = db::open(&db_path).unwrap();
let payload = serde_json::json!({});
let outcome = {
let mut out = env.output();
enforce(
&conn,
GovernedAction::Delete,
"gov-ns",
"bob",
Some("00000000-0000-0000-0000-000000000abc"),
Some("alice"),
&payload,
true,
&mut out,
)
.unwrap()
};
assert_eq!(outcome, GovernanceOutcome::Pending);
let v: serde_json::Value = serde_json::from_str(env.stdout_str().trim()).unwrap();
assert_eq!(v["status"].as_str().unwrap(), "pending");
assert_eq!(v["action"].as_str().unwrap(), "delete");
assert_eq!(v["namespace"].as_str().unwrap(), "gov-ns");
assert!(v["pending_id"].is_string());
assert_eq!(
v["memory_id"].as_str().unwrap(),
"00000000-0000-0000-0000-000000000abc"
);
}
#[test]
fn test_governance_deny_writes_reason_to_stderr() {
let _gate = pin_governance_enforce_for_test();
let mut env = TestEnv::fresh();
let db_path = env.db_path.clone();
let policy = GovernancePolicy {
core: CorePolicy {
write: GovernanceLevel::Any,
promote: GovernanceLevel::Any,
delete: GovernanceLevel::Owner,
approver: ApproverType::Human,
inherit: true,
max_reflection_depth: None,
},
..Default::default()
};
seed_governance_policy(&db_path, "gov-ns", policy, "alice");
let conn = db::open(&db_path).unwrap();
let payload = serde_json::json!({});
let outcome = {
let mut out = env.output();
enforce(
&conn,
GovernedAction::Delete,
"gov-ns",
"bob",
Some("00000000-0000-0000-0000-000000000def"),
Some("alice"),
&payload,
false,
&mut out,
)
.unwrap()
};
assert_eq!(outcome, GovernanceOutcome::Deny);
let stderr = env.stderr_str();
assert!(
stderr.contains("delete denied by governance"),
"got: {stderr}"
);
assert!(stderr.contains("not the owner"), "got: {stderr}");
assert!(env.stdout_str().is_empty());
}
#[test]
fn test_governance_deny_returns_deny_outcome() {
let _gate = pin_governance_enforce_for_test();
let mut env = TestEnv::fresh();
let db_path = env.db_path.clone();
let policy = GovernancePolicy {
core: CorePolicy {
write: GovernanceLevel::Registered,
promote: GovernanceLevel::Any,
delete: GovernanceLevel::Owner,
approver: ApproverType::Human,
inherit: true,
max_reflection_depth: None,
},
..Default::default()
};
seed_governance_policy(&db_path, "gov-ns", policy, "alice");
let conn = db::open(&db_path).unwrap();
let payload = serde_json::json!({});
let outcome = {
let mut out = env.output();
enforce(
&conn,
GovernedAction::Store,
"gov-ns",
"unregistered-caller",
None,
None,
&payload,
false,
&mut out,
)
.unwrap()
};
assert_eq!(outcome, GovernanceOutcome::Deny);
assert!(env.stderr_str().contains("not a registered agent"));
}
#[test]
fn test_governance_payload_serializes_correctly() {
let _gate = pin_governance_enforce_for_test();
let mut env = TestEnv::fresh();
let db_path = env.db_path.clone();
let policy = GovernancePolicy {
core: CorePolicy {
write: GovernanceLevel::Approve,
promote: GovernanceLevel::Any,
delete: GovernanceLevel::Owner,
approver: ApproverType::Human,
inherit: true,
max_reflection_depth: None,
},
..Default::default()
};
seed_governance_policy(&db_path, "gov-ns", policy, "alice");
let conn = db::open(&db_path).unwrap();
let payload = serde_json::json!({"title": "hello", "priority": 7});
let _ = {
let mut out = env.output();
enforce(
&conn,
GovernedAction::Store,
"gov-ns",
"carol",
None,
None,
&payload,
true,
&mut out,
)
.unwrap()
};
let stored_payload: String = conn
.query_row(
"SELECT payload FROM pending_actions WHERE namespace = 'gov-ns' AND requested_by = 'carol' ORDER BY requested_at DESC LIMIT 1",
[],
|r| r.get(0),
)
.unwrap();
let v: serde_json::Value = serde_json::from_str(&stored_payload).unwrap();
assert_eq!(v["title"].as_str().unwrap(), "hello");
assert_eq!(v["priority"].as_u64().unwrap(), 7);
}
}