use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::error::Error;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PolicyRule {
Allow(String),
Deny(String),
AllowAll,
DenyAll,
AskUser { tool: String, handler_id: String },
WorkspaceOnly(Vec<PathBuf>),
}
impl PolicyRule {
#[must_use]
pub fn allow(tool: impl Into<String>) -> Self {
Self::Allow(tool.into())
}
#[must_use]
pub fn deny(tool: impl Into<String>) -> Self {
Self::Deny(tool.into())
}
#[must_use]
pub const fn allow_all() -> Self {
Self::AllowAll
}
#[must_use]
pub const fn deny_all() -> Self {
Self::DenyAll
}
#[must_use]
pub fn workspace_only(paths: impl IntoIterator<Item = impl Into<PathBuf>>) -> Self {
Self::WorkspaceOnly(paths.into_iter().map(Into::into).collect())
}
#[must_use]
pub fn description(&self) -> String {
match self {
Self::Allow(tool) => format!("allow({tool})"),
Self::Deny(tool) => format!("deny({tool})"),
Self::AllowAll => "allow(*)".to_owned(),
Self::DenyAll => "deny(*)".to_owned(),
Self::AskUser { tool, handler_id } => format!("ask_user({tool}, handler={handler_id})"),
Self::WorkspaceOnly(paths) => {
let joined: Vec<String> = paths.iter().map(|p| p.display().to_string()).collect();
format!("workspace_only([{}])", joined.join(", "))
}
}
}
pub fn validate(&self) -> Result<(), Error> {
match self {
Self::Allow(tool) | Self::Deny(tool) => {
if tool.trim().is_empty() {
return Err(Error::InvalidConfig {
message: "PolicyRule tool name must not be empty".to_owned(),
});
}
}
Self::AskUser { tool, handler_id } => {
if tool.trim().is_empty() {
return Err(Error::InvalidConfig {
message: "PolicyRule::AskUser tool name must not be empty".to_owned(),
});
}
if handler_id.trim().is_empty() {
return Err(Error::InvalidConfig {
message: format!("PolicyRule::AskUser '{tool}' has an empty handler_id"),
});
}
}
Self::WorkspaceOnly(paths) => {
if paths.is_empty() {
return Err(Error::InvalidConfig {
message: "PolicyRule::WorkspaceOnly must contain at least one path"
.to_owned(),
});
}
}
Self::AllowAll | Self::DenyAll => {}
}
Ok(())
}
}
#[must_use]
pub fn ask_user(tool: impl Into<String>, handler_id: impl Into<String>) -> PolicyRule {
PolicyRule::AskUser {
tool: tool.into(),
handler_id: handler_id.into(),
}
}
#[must_use]
pub fn confirm_run_command() -> PolicyRule {
ask_user("run_command", "confirm_run_command")
}
#[must_use]
pub fn safe_defaults() -> PolicySet {
const READ_TOOLS: &[&str] = &["view_file", "read_file", "list_dir", "search"];
const WRITE_TOOLS: &[(&str, &str)] = &[
("run_command", "confirm_run_command"),
("write_file", "confirm_write_file"),
("edit_file", "confirm_edit_file"),
];
let mut set = PolicySet::new();
for tool in READ_TOOLS {
set.push(PolicyRule::Allow((*tool).to_owned()))
.expect("safe_defaults: valid Allow rule");
}
for (tool, handler_id) in WRITE_TOOLS {
set.push(PolicyRule::AskUser {
tool: (*tool).to_owned(),
handler_id: (*handler_id).to_owned(),
})
.expect("safe_defaults: valid AskUser rule");
}
set.push(PolicyRule::DenyAll)
.expect("safe_defaults: valid DenyAll rule");
set
}
pub trait AskUserHandler: Send + Sync {
fn confirm(&self, tool_name: &str, tool_args: &serde_json::Value) -> bool;
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PolicyDecision<'a> {
Allow,
Deny,
NeedsConfirmation {
handler_id: &'a str,
},
}
impl PolicyDecision<'_> {
#[must_use]
pub const fn is_allowed(&self) -> bool {
matches!(self, Self::Allow)
}
#[must_use]
pub const fn is_denied(&self) -> bool {
matches!(self, Self::Deny)
}
#[must_use]
pub const fn needs_confirmation(&self) -> bool {
matches!(self, Self::NeedsConfirmation { .. })
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PolicySet {
rules: Vec<PolicyRule>,
}
impl PolicySet {
#[must_use]
pub const fn new() -> Self {
Self { rules: Vec::new() }
}
pub fn push(&mut self, rule: PolicyRule) -> Result<(), Error> {
rule.validate()?;
self.rules.push(rule);
Ok(())
}
pub fn with_rule(mut self, rule: PolicyRule) -> Result<Self, Error> {
self.push(rule)?;
Ok(self)
}
pub fn iter(&self) -> impl Iterator<Item = &PolicyRule> {
self.rules.iter()
}
#[must_use]
pub const fn len(&self) -> usize {
self.rules.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.rules.is_empty()
}
#[must_use]
pub fn evaluate(&self, tool_name: &str) -> PolicyDecision<'_> {
for rule in &self.rules {
match rule {
PolicyRule::Allow(name) if name == tool_name => return PolicyDecision::Allow,
PolicyRule::Deny(name) if name == tool_name => return PolicyDecision::Deny,
PolicyRule::AllowAll => return PolicyDecision::Allow,
PolicyRule::DenyAll => return PolicyDecision::Deny,
PolicyRule::AskUser { tool, handler_id } if tool == tool_name => {
return PolicyDecision::NeedsConfirmation { handler_id };
}
PolicyRule::AskUser { .. }
| PolicyRule::Allow(_)
| PolicyRule::Deny(_)
| PolicyRule::WorkspaceOnly(_) => {}
}
}
PolicyDecision::Deny
}
}
impl From<Vec<PolicyRule>> for PolicySet {
fn from(rules: Vec<PolicyRule>) -> Self {
Self::validated_from(rules).expect("PolicySet::from(Vec<PolicyRule>): invalid rules")
}
}
impl FromIterator<PolicyRule> for PolicySet {
fn from_iter<T: IntoIterator<Item = PolicyRule>>(iter: T) -> Self {
let rules = iter.into_iter().collect::<Vec<_>>();
Self::from(rules)
}
}
impl<const N: usize> From<[PolicyRule; N]> for PolicySet {
fn from(rules: [PolicyRule; N]) -> Self {
Self::from(Vec::from(rules))
}
}
impl PolicySet {
pub fn validated_from(rules: Vec<PolicyRule>) -> Result<Self, Error> {
for rule in &rules {
rule.validate()?;
}
Ok(Self { rules })
}
}
impl<'a> IntoIterator for &'a PolicySet {
type Item = &'a PolicyRule;
type IntoIter = std::slice::Iter<'a, PolicyRule>;
fn into_iter(self) -> Self::IntoIter {
self.rules.iter()
}
}
impl IntoIterator for PolicySet {
type Item = PolicyRule;
type IntoIter = std::vec::IntoIter<PolicyRule>;
fn into_iter(self) -> Self::IntoIter {
self.rules.into_iter()
}
}
impl From<PolicySet> for Vec<PolicyRule> {
fn from(set: PolicySet) -> Self {
set.rules
}
}
impl From<&PolicySet> for Vec<PolicyRule> {
fn from(set: &PolicySet) -> Self {
set.rules.clone()
}
}
#[cfg(test)]
#[path = "rules_tests.rs"]
mod tests;