use crate::error::MurkError;
use crate::types::{Murk, Policy, Vault};
pub fn check_agent_keys(vault: &Vault, keys: &[String]) -> Result<(), MurkError> {
let Some(policy) = &vault.policy else {
return Ok(());
};
let forbidden: Vec<&String> = keys
.iter()
.filter(|key| !key_allowed(vault, policy, key))
.collect();
if forbidden.is_empty() {
return Ok(());
}
let names: Vec<&str> = forbidden.iter().map(|s| s.as_str()).collect();
let allowed = if policy.agent_allow_tags.is_empty() {
"none — this vault's policy locks agents out entirely".to_string()
} else {
policy.agent_allow_tags.join(", ")
};
Err(MurkError::Policy(format!(
"policy forbids {} in agent mode (allowed tags: {allowed}) — tag the key with `murk describe` or update the policy with `murk policy`",
names.join(", "),
)))
}
pub fn is_agent_identity(murk: &Murk, pubkey: &str) -> bool {
murk.grants.values().any(|g| g.pubkey == pubkey)
}
pub fn enforce_agent_policy(
vault: &Vault,
murk: &Murk,
pubkey: &str,
keys: &[String],
) -> Result<(), MurkError> {
if is_agent_identity(murk, pubkey) {
check_agent_keys(vault, keys)?;
}
Ok(())
}
fn key_allowed(vault: &Vault, policy: &Policy, key: &str) -> bool {
vault.schema.get(key).is_some_and(|entry| {
entry
.tags
.iter()
.any(|t| policy.agent_allow_tags.contains(t))
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{GrantEntry, Murk, Policy, SchemaEntry, Vault};
use std::collections::BTreeMap;
fn agent_murk(pubkey: &str) -> Murk {
let mut grants = BTreeMap::new();
grants.insert(
"codex".to_string(),
GrantEntry {
pubkey: pubkey.to_string(),
..Default::default()
},
);
Murk {
grants,
..Default::default()
}
}
fn vault_with(tags: &[(&str, &[&str])], policy: Option<Policy>) -> Vault {
let mut schema = BTreeMap::new();
for (key, key_tags) in tags {
schema.insert(
(*key).to_string(),
SchemaEntry {
tags: key_tags.iter().map(|t| (*t).to_string()).collect(),
..Default::default()
},
);
}
Vault {
version: "2.0".into(),
created: "2026-06-16T00:00:00Z".into(),
vault_name: ".murk".into(),
repo: String::new(),
recipients: vec![],
schema,
policy,
secrets: BTreeMap::new(),
meta: String::new(),
}
}
fn policy(tags: &[&str]) -> Policy {
Policy {
agent_allow_tags: tags.iter().map(|t| (*t).to_string()).collect(),
}
}
#[test]
fn no_policy_allows_everything() {
let v = vault_with(&[("PROD_DB", &["production"])], None);
assert!(check_agent_keys(&v, &["PROD_DB".into()]).is_ok());
}
#[test]
fn allow_tag_permits_matching_key() {
let v = vault_with(&[("TEST_KEY", &["agents"])], Some(policy(&["agents"])));
assert!(check_agent_keys(&v, &["TEST_KEY".into()]).is_ok());
}
#[test]
fn missing_tag_is_refused() {
let v = vault_with(
&[("PROD_DB", &["production"]), ("TEST_KEY", &["agents"])],
Some(policy(&["agents"])),
);
let err = check_agent_keys(&v, &["PROD_DB".into()]).unwrap_err();
assert!(err.to_string().contains("PROD_DB"));
assert!(err.to_string().contains("agents"));
let err = check_agent_keys(&v, &["TEST_KEY".into(), "PROD_DB".into()]).unwrap_err();
assert!(err.to_string().contains("PROD_DB"));
assert!(!err.to_string().contains("TEST_KEY,"));
}
#[test]
fn unknown_key_is_refused_under_policy() {
let v = vault_with(&[], Some(policy(&["agents"])));
assert!(check_agent_keys(&v, &["NOPE".into()]).is_err());
}
#[test]
fn empty_allow_list_locks_agents_out() {
let v = vault_with(&[("TEST_KEY", &["agents"])], Some(policy(&[])));
let err = check_agent_keys(&v, &["TEST_KEY".into()]).unwrap_err();
assert!(err.to_string().contains("locks agents out"));
}
#[test]
fn is_agent_identity_matches_granted_pubkey() {
let murk = agent_murk("age1agent");
assert!(is_agent_identity(&murk, "age1agent"));
assert!(!is_agent_identity(&murk, "age1operator"));
assert!(!is_agent_identity(&Murk::default(), "age1agent"));
}
#[test]
fn enforce_agent_policy_is_noop_for_operator() {
let v = vault_with(&[("PROD_DB", &["production"])], Some(policy(&["agents"])));
let operator = Murk::default();
assert!(enforce_agent_policy(&v, &operator, "age1operator", &["PROD_DB".into()]).is_ok());
}
#[test]
fn enforce_agent_policy_applies_to_agents() {
let v = vault_with(
&[("PROD_DB", &["production"]), ("TEST_KEY", &["agents"])],
Some(policy(&["agents"])),
);
let agent = agent_murk("age1agent");
assert!(enforce_agent_policy(&v, &agent, "age1agent", &["TEST_KEY".into()]).is_ok());
let err = enforce_agent_policy(&v, &agent, "age1agent", &["PROD_DB".into()]).unwrap_err();
assert!(err.to_string().contains("PROD_DB"));
}
#[test]
fn enforce_agent_policy_noop_without_policy() {
let v = vault_with(&[("PROD_DB", &["production"])], None);
let agent = agent_murk("age1agent");
assert!(enforce_agent_policy(&v, &agent, "age1agent", &["PROD_DB".into()]).is_ok());
}
}