use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Operation {
Read,
Edit,
Execute,
Network,
Mcp,
Memory,
Skill,
Agent,
Meta,
Plugin,
Other,
}
impl Operation {
pub fn is_side_effecting(self) -> bool {
matches!(
self,
Operation::Edit
| Operation::Execute
| Operation::Network
| Operation::Mcp
| Operation::Agent
| Operation::Plugin
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Resource {
Path {
raw: String,
resolved: PathBuf,
in_cwd: bool,
dev_null: bool,
},
Command { raw: String, head: String },
Mcp {
server: String,
name: String,
raw: String,
},
Url(String),
Bareword(String),
}
impl Resource {
pub fn match_key(&self) -> &str {
match self {
Resource::Path { resolved, raw, .. } => resolved.to_str().unwrap_or(raw),
Resource::Command { raw, .. } => raw,
Resource::Mcp { raw, .. } => raw,
Resource::Url(u) => u,
Resource::Bareword(b) => b,
}
}
pub fn match_candidates(&self) -> Vec<&str> {
match self {
Resource::Path { raw, resolved, .. } => {
let r = resolved.to_str().unwrap_or(raw);
if r == raw { vec![r] } else { vec![r, raw] }
}
_ => vec![self.match_key()],
}
}
}
#[derive(Debug, Clone)]
pub struct Claim {
pub op: Operation,
pub resource: Resource,
}
impl Claim {
pub fn new(op: Operation, resource: Resource) -> Self {
Claim { op, resource }
}
}
#[derive(Debug, Clone)]
pub struct AccessRequest {
pub tool: String,
pub claims: Vec<Claim>,
pub mode: crate::permission::SecurityMode,
pub display_input: String,
}
impl AccessRequest {
pub fn single(
tool: impl Into<String>,
op: Operation,
resource: Resource,
mode: crate::permission::SecurityMode,
display_input: impl Into<String>,
) -> Self {
let display_input = display_input.into();
AccessRequest {
tool: tool.into(),
claims: vec![Claim::new(op, resource)],
mode,
display_input,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Effect {
Allow,
Ask,
Deny,
}
impl Effect {
pub fn meet(self, other: Effect) -> Effect {
self.max(other)
}
}
#[derive(Debug, Clone)]
pub struct Verdict {
pub effect: Effect,
pub why: String,
}
impl Verdict {
pub fn new(effect: Effect, why: impl Into<String>) -> Self {
Verdict {
effect,
why: why.into(),
}
}
}
#[derive(Debug, Clone)]
pub struct Refined {
pub(crate) effect: Effect,
pub(crate) by: Option<&'static str>,
pub(crate) why: String,
}
impl Refined {
pub fn noop(current: Effect) -> Self {
Refined {
effect: current,
by: None,
why: String::new(),
}
}
pub fn tighten(
current: Effect,
proposed: Effect,
by: &'static str,
why: impl Into<String>,
) -> Self {
let effect = current.meet(proposed);
Refined {
effect,
by: if effect != current { Some(by) } else { None },
why: if effect != current {
why.into()
} else {
String::new()
},
}
}
pub fn effect(&self) -> Effect {
self.effect
}
}
#[derive(Debug, Clone)]
pub struct TraceEntry {
pub policy: &'static str,
pub resource: usize,
pub effect: Option<Effect>,
pub why: String,
pub applied: bool,
}
#[derive(Debug, Clone)]
pub struct Decision {
pub effect: Effect,
pub deciding: Option<TraceEntry>,
pub trace: Vec<TraceEntry>,
pub resolved_paths: Vec<PathBuf>,
}
impl Decision {
pub fn reason(&self) -> String {
match &self.deciding {
Some(e) if !e.why.is_empty() => format!("{} ({})", e.why, e.policy),
Some(e) => format!("decided by {}", e.policy),
None => "no resources to authorize".to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn effect_lattice_order() {
assert!(Effect::Allow < Effect::Ask);
assert!(Effect::Ask < Effect::Deny);
assert!(Effect::Allow < Effect::Deny);
}
#[test]
fn meet_is_most_restrictive() {
assert_eq!(Effect::Allow.meet(Effect::Ask), Effect::Ask);
assert_eq!(Effect::Ask.meet(Effect::Allow), Effect::Ask);
assert_eq!(Effect::Allow.meet(Effect::Deny), Effect::Deny);
assert_eq!(Effect::Ask.meet(Effect::Deny), Effect::Deny);
assert_eq!(Effect::Allow.meet(Effect::Allow), Effect::Allow);
assert_eq!(Effect::Deny.meet(Effect::Deny), Effect::Deny);
}
#[test]
fn meet_lattice_laws() {
let all = [Effect::Allow, Effect::Ask, Effect::Deny];
for a in all {
assert_eq!(a.meet(a), a);
for b in all {
assert_eq!(a.meet(b), b.meet(a));
for c in all {
assert_eq!(a.meet(b).meet(c), a.meet(b.meet(c)));
assert!(a.meet(b) >= a);
}
}
}
}
#[test]
fn refined_cannot_loosen() {
let r = Refined::tighten(Effect::Deny, Effect::Allow, "x", "tried to loosen");
assert_eq!(r.effect(), Effect::Deny);
assert!(r.by.is_none(), "no-op tighten records no author");
let r = Refined::tighten(Effect::Allow, Effect::Ask, "loopguard", "retry loop");
assert_eq!(r.effect(), Effect::Ask);
assert_eq!(r.by, Some("loopguard"));
assert_eq!(Refined::noop(Effect::Ask).effect(), Effect::Ask);
}
#[test]
fn side_effecting_classification() {
assert!(Operation::Edit.is_side_effecting());
assert!(Operation::Execute.is_side_effecting());
assert!(Operation::Network.is_side_effecting());
assert!(Operation::Mcp.is_side_effecting());
assert!(!Operation::Read.is_side_effecting());
assert!(!Operation::Meta.is_side_effecting());
assert!(!Operation::Memory.is_side_effecting());
}
}