use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::io::Write;
use aa_core::identity::{AgentId, SessionId};
use aa_core::{AgentContext, Capability, CapabilitySet, GovernanceAction, GovernanceLevel, PolicyResult};
use aa_gateway::engine::PolicyEngine;
use aa_gateway::policy::document::PolicyDocument;
use aa_gateway::policy::scope::PolicyScope;
use aa_gateway::policy::PolicyValidator;
fn make_engine() -> PolicyEngine {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
writeln!(tmp, "version: \"1\"").unwrap();
tmp.flush().unwrap();
let (alert_tx, _) = tokio::sync::broadcast::channel::<aa_gateway::budget::BudgetAlert>(64);
PolicyEngine::load_from_file(tmp.path(), alert_tx).unwrap()
}
fn make_ctx() -> AgentContext {
AgentContext {
agent_id: AgentId::from_bytes([1u8; 16]),
session_id: SessionId::from_bytes([0u8; 16]),
pid: 0,
started_at: aa_core::time::Timestamp::from_nanos(0),
metadata: BTreeMap::new(),
governance_level: GovernanceLevel::default(),
parent_agent_id: None,
team_id: None,
depth: 0,
delegation_reason: None,
spawned_by_tool: None,
root_agent_id: None,
}
}
fn cap_doc(scope: PolicyScope, allow: &[Capability], deny: &[Capability]) -> PolicyDocument {
PolicyDocument {
name: None,
policy_version: None,
version: None,
scope,
network: None,
schedule: None,
budget: None,
data: None,
approval_timeout_secs: 300,
approval_policy: None,
tools: HashMap::new(),
capabilities: Some(CapabilitySet {
allow: allow.iter().cloned().collect::<BTreeSet<_>>(),
deny: deny.iter().cloned().collect::<BTreeSet<_>>(),
}),
}
}
fn no_cap_doc(scope: PolicyScope) -> PolicyDocument {
PolicyDocument {
name: None,
policy_version: None,
version: None,
scope,
network: None,
schedule: None,
budget: None,
data: None,
approval_timeout_secs: 300,
approval_policy: None,
tools: HashMap::new(),
capabilities: None,
}
}
#[test]
fn capability_policy_yaml_round_trip_via_validator() {
let yaml = r#"
apiVersion: agent-assembly/v1
kind: Policy
metadata:
name: capability-example
version: "1.0.0"
spec:
scope: global
capabilities:
allow:
- file_read
- network_outbound
- mcp_tool:git
- mcp_tool:bash
deny:
- terminal_exec
- file_write
"#;
let output = PolicyValidator::from_yaml(yaml);
assert!(output.is_ok(), "expected Ok, got: {:?}", output.err());
let doc = output.unwrap().document;
let caps = doc.capabilities.as_ref().expect("capabilities must be Some");
assert!(caps.allow.contains(&Capability::FileRead));
assert!(caps.allow.contains(&Capability::NetworkOutbound));
assert!(caps.allow.contains(&Capability::McpTool("git".to_string())));
assert!(caps.allow.contains(&Capability::McpTool("bash".to_string())));
assert!(caps.deny.contains(&Capability::TerminalExec));
assert!(caps.deny.contains(&Capability::FileWrite));
}
#[test]
fn full_cascade_capability_policy_denies_disallowed_file_write() {
let mut engine = make_engine();
engine.load_policy(cap_doc(PolicyScope::Global, &[Capability::FileRead], &[]));
engine.load_policy(cap_doc(
PolicyScope::Team("alpha".to_string()),
&[Capability::FileRead],
&[Capability::FileWrite],
));
let ctx = make_ctx();
let action = GovernanceAction::FileAccess {
path: "/tmp/secret.txt".into(),
mode: aa_core::FileMode::Write,
};
let result = engine.evaluate(&ctx, &action).decision;
assert!(
matches!(result, PolicyResult::Deny { .. }),
"expected Deny for FileWrite denied in two-policy cascade, got {:?}",
result
);
}
#[test]
fn full_cascade_capability_policy_allows_permitted_file_read() {
let mut engine = make_engine();
engine.load_policy(cap_doc(PolicyScope::Global, &[Capability::FileRead], &[]));
engine.load_policy(cap_doc(
PolicyScope::Team("alpha".to_string()),
&[Capability::FileRead],
&[Capability::FileWrite],
));
let ctx = make_ctx();
let action = GovernanceAction::FileAccess {
path: "/tmp/readme.txt".into(),
mode: aa_core::FileMode::Read,
};
let result = engine.evaluate(&ctx, &action).decision;
assert_eq!(
result,
PolicyResult::Allow,
"expected Allow for FileRead in two-policy cascade allow set, got {:?}",
result
);
}
#[test]
fn full_cascade_capability_denies_file_write_when_not_in_allow_set() {
let mut engine = make_engine();
engine.load_policy(cap_doc(PolicyScope::Global, &[Capability::FileRead], &[]));
let ctx = make_ctx();
let action = GovernanceAction::FileAccess {
path: "/tmp/data.txt".into(),
mode: aa_core::FileMode::Write,
};
let result = engine.evaluate(&ctx, &action).decision;
assert!(
matches!(result, PolicyResult::Deny { .. }),
"expected Deny for FileWrite not in allow set, got {:?}",
result
);
}
#[test]
fn cascade_empty_capabilities_does_not_block_any_action() {
let mut engine = make_engine();
engine.load_policy(no_cap_doc(PolicyScope::Global));
let agent_id = AgentId::from_bytes([1u8; 16]);
engine.load_policy(no_cap_doc(PolicyScope::Agent(agent_id)));
let ctx = make_ctx();
let action = GovernanceAction::FileAccess {
path: "/tmp/file.txt".into(),
mode: aa_core::FileMode::Write,
};
let result = engine.evaluate(&ctx, &action).decision;
assert_eq!(
result,
PolicyResult::Allow,
"expected Allow when no capabilities are configured, got {:?}",
result
);
}
#[test]
fn parent_deny_overrides_child_allow_in_full_cascade() {
let agent_id = AgentId::from_bytes([1u8; 16]);
let mut engine = make_engine();
engine.load_policy(cap_doc(PolicyScope::Global, &[], &[Capability::TerminalExec]));
engine.load_policy(cap_doc(
PolicyScope::Agent(agent_id),
&[Capability::TerminalExec, Capability::FileRead],
&[],
));
let ctx = make_ctx();
let action = GovernanceAction::ProcessExec { command: "ls".into() };
let result = engine.evaluate(&ctx, &action).decision;
assert!(
matches!(result, PolicyResult::Deny { .. }),
"expected Deny: global deny of TerminalExec must override agent allow, got {:?}",
result
);
}
#[test]
fn mcp_tool_capability_denied_blocks_tool_call() {
let mut engine = make_engine();
engine.load_policy(cap_doc(
PolicyScope::Global,
&[],
&[Capability::McpTool("bash".to_string())],
));
let ctx = make_ctx();
let action = GovernanceAction::ToolCall {
name: "bash".into(),
args: "{}".into(),
};
let result = engine.evaluate(&ctx, &action).decision;
assert!(
matches!(result, PolicyResult::Deny { .. }),
"expected Deny: McpTool(bash) denied by capability policy, got {:?}",
result
);
}
#[test]
fn mcp_tool_capability_allowlist_permits_only_listed_tools() {
let mut engine = make_engine();
engine.load_policy(cap_doc(
PolicyScope::Global,
&[Capability::McpTool("git".to_string())],
&[],
));
let ctx = make_ctx();
let bash_result = engine
.evaluate(
&ctx,
&GovernanceAction::ToolCall {
name: "bash".into(),
args: "{}".into(),
},
)
.decision;
assert!(
matches!(bash_result, PolicyResult::Deny { .. }),
"expected Deny for bash (not in MCP tool allowlist), got {:?}",
bash_result
);
let git_result = engine
.evaluate(
&ctx,
&GovernanceAction::ToolCall {
name: "git".into(),
args: "{}".into(),
},
)
.decision;
assert_eq!(
git_result,
PolicyResult::Allow,
"expected Allow for git (in MCP tool allowlist), got {:?}",
git_result
);
}