use crate::permission::PermissionTier;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ApprovalMode {
AlwaysAllow,
RequireApproval,
Deny,
}
impl ApprovalMode {
pub fn as_str(self) -> &'static str {
match self {
ApprovalMode::AlwaysAllow => "always_allow",
ApprovalMode::RequireApproval => "require_approval",
ApprovalMode::Deny => "deny",
}
}
pub fn from_str_opt(s: &str) -> Option<ApprovalMode> {
match s {
"always_allow" | "allow" => Some(ApprovalMode::AlwaysAllow),
"require_approval" | "approval" | "ask" => Some(ApprovalMode::RequireApproval),
"deny" | "block" => Some(ApprovalMode::Deny),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TierPosture {
#[serde(skip_serializing_if = "Option::is_none", default)]
pub read_only: Option<ApprovalMode>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub sandbox_edit: Option<ApprovalMode>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub full_access: Option<ApprovalMode>,
}
impl TierPosture {
pub fn get(&self, tier: PermissionTier) -> Option<ApprovalMode> {
match tier {
PermissionTier::ReadOnly => self.read_only,
PermissionTier::SandboxEdit => self.sandbox_edit,
PermissionTier::FullAccess => self.full_access,
}
}
pub fn set(&mut self, tier: PermissionTier, mode: ApprovalMode) {
match tier {
PermissionTier::ReadOnly => self.read_only = Some(mode),
PermissionTier::SandboxEdit => self.sandbox_edit = Some(mode),
PermissionTier::FullAccess => self.full_access = Some(mode),
}
}
pub fn uniform(mode: ApprovalMode) -> Self {
TierPosture {
read_only: Some(mode),
sandbox_edit: Some(mode),
full_access: Some(mode),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ApprovalPreset {
Cautious,
Balanced,
Trusting,
}
impl ApprovalPreset {
pub fn posture(self) -> TierPosture {
use ApprovalMode::*;
match self {
ApprovalPreset::Cautious => TierPosture {
read_only: Some(RequireApproval),
sandbox_edit: Some(RequireApproval),
full_access: Some(RequireApproval),
},
ApprovalPreset::Balanced => TierPosture {
read_only: Some(AlwaysAllow),
sandbox_edit: Some(RequireApproval),
full_access: Some(RequireApproval),
},
ApprovalPreset::Trusting => TierPosture {
read_only: Some(AlwaysAllow),
sandbox_edit: Some(AlwaysAllow),
full_access: Some(RequireApproval),
},
}
}
pub fn from_str_opt(s: &str) -> Option<ApprovalPreset> {
match s {
"cautious" => Some(ApprovalPreset::Cautious),
"balanced" => Some(ApprovalPreset::Balanced),
"trusting" => Some(ApprovalPreset::Trusting),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentPermissionPolicy {
pub default: TierPosture,
#[serde(default)]
pub agents: BTreeMap<String, TierPosture>,
}
impl Default for AgentPermissionPolicy {
fn default() -> Self {
AgentPermissionPolicy {
default: ApprovalPreset::Balanced.posture(),
agents: BTreeMap::new(),
}
}
}
impl AgentPermissionPolicy {
pub fn resolve(&self, agent_id: &str, tier: PermissionTier) -> ApprovalMode {
if let Some(posture) = self.agents.get(agent_id) {
if let Some(mode) = posture.get(tier) {
return mode;
}
}
self.default
.get(tier)
.unwrap_or(ApprovalMode::RequireApproval)
}
pub fn effective(&self, agent_id: &str) -> TierPosture {
TierPosture {
read_only: Some(self.resolve(agent_id, PermissionTier::ReadOnly)),
sandbox_edit: Some(self.resolve(agent_id, PermissionTier::SandboxEdit)),
full_access: Some(self.resolve(agent_id, PermissionTier::FullAccess)),
}
}
pub fn has_override(&self, agent_id: &str) -> bool {
self.agents.contains_key(agent_id)
}
pub fn set_agent(&mut self, agent_id: &str, tier: PermissionTier, mode: ApprovalMode) {
self.agents
.entry(agent_id.to_string())
.or_default()
.set(tier, mode);
}
pub fn set_agent_uniform(&mut self, agent_id: &str, mode: ApprovalMode) {
self.agents
.insert(agent_id.to_string(), TierPosture::uniform(mode));
}
pub fn set_default(&mut self, tier: PermissionTier, mode: ApprovalMode) {
self.default.set(tier, mode);
}
pub fn set_default_preset(&mut self, preset: ApprovalPreset) {
self.default = preset.posture();
}
pub fn reset_agent(&mut self, agent_id: &str) {
self.agents.remove(agent_id);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_balanced() {
let p = AgentPermissionPolicy::default();
assert_eq!(
p.resolve("any", PermissionTier::ReadOnly),
ApprovalMode::AlwaysAllow
);
assert_eq!(
p.resolve("any", PermissionTier::SandboxEdit),
ApprovalMode::RequireApproval
);
assert_eq!(
p.resolve("any", PermissionTier::FullAccess),
ApprovalMode::RequireApproval
);
}
#[test]
fn agent_override_wins_then_falls_through() {
let mut p = AgentPermissionPolicy::default();
p.set_agent("risky", PermissionTier::FullAccess, ApprovalMode::Deny);
assert_eq!(
p.resolve("risky", PermissionTier::FullAccess),
ApprovalMode::Deny
);
assert_eq!(
p.resolve("risky", PermissionTier::ReadOnly),
ApprovalMode::AlwaysAllow
);
assert_eq!(
p.resolve("other", PermissionTier::FullAccess),
ApprovalMode::RequireApproval
);
}
#[test]
fn uniform_and_reset() {
let mut p = AgentPermissionPolicy::default();
p.set_agent_uniform("blocked", ApprovalMode::Deny);
assert_eq!(
p.resolve("blocked", PermissionTier::ReadOnly),
ApprovalMode::Deny
);
assert!(p.has_override("blocked"));
p.reset_agent("blocked");
assert!(!p.has_override("blocked"));
assert_eq!(
p.resolve("blocked", PermissionTier::ReadOnly),
ApprovalMode::AlwaysAllow
);
}
#[test]
fn presets_map_as_expected() {
assert_eq!(
ApprovalPreset::Cautious.posture().read_only,
Some(ApprovalMode::RequireApproval)
);
assert_eq!(
ApprovalPreset::Trusting.posture().sandbox_edit,
Some(ApprovalMode::AlwaysAllow)
);
assert_eq!(
ApprovalPreset::Trusting.posture().full_access,
Some(ApprovalMode::RequireApproval)
);
}
#[test]
fn roundtrips_through_json() {
let mut p = AgentPermissionPolicy::default();
p.set_agent("a", PermissionTier::SandboxEdit, ApprovalMode::AlwaysAllow);
let json = serde_json::to_string(&p).unwrap();
let back: AgentPermissionPolicy = serde_json::from_str(&json).unwrap();
assert_eq!(p, back);
}
}