introduce variant typing to policy result

This commit is contained in:
kevin zhao
2025-11-10 16:02:36 -08:00
parent 0bac9939af
commit 687a8c38ff
4 changed files with 71 additions and 33 deletions

View File

@@ -4,6 +4,7 @@ use std::path::Path;
use anyhow::Context;
use anyhow::Result;
use anyhow::bail;
use codex_execpolicy2::Evaluation;
use codex_execpolicy2::PolicyParser;
use codex_execpolicy2::load_default_policy;
@@ -49,14 +50,14 @@ fn cmd_check(policy_path: Option<String>, args: Vec<String>) -> Result<()> {
let policy = load_policy(policy_path)?;
match policy.evaluate(&args) {
Some(eval) => {
eval @ Evaluation::Match { .. } => {
let json = serde_json::to_string_pretty(&eval)?;
println!("{json}");
}
None => {
Evaluation::NoMatch => {
println!("no match");
}
}
};
Ok(())
}

View File

@@ -1,5 +1,6 @@
use crate::decision::Decision;
use crate::rule::Rule;
use crate::rule::RuleMatch;
use multimap::MultiMap;
use serde::Deserialize;
use serde::Serialize;
@@ -10,9 +11,12 @@ pub struct Policy {
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Evaluation {
pub decision: Decision,
pub matched_rules: Vec<crate::rule::RuleMatch>,
pub enum Evaluation {
NoMatch,
Match {
decision: Decision,
matched_rules: Vec<RuleMatch>,
},
}
impl Policy {
@@ -24,12 +28,15 @@ impl Policy {
&self.rules_by_program
}
pub fn evaluate(&self, cmd: &[String]) -> Option<Evaluation> {
let first = cmd.first()?;
let Some(rules) = self.rules_by_program.get_vec(first) else {
return None;
pub fn evaluate(&self, cmd: &[String]) -> Evaluation {
let rules = match cmd.first() {
Some(first) => match self.rules_by_program.get_vec(first) {
Some(rules) => rules,
None => return Evaluation::NoMatch,
},
None => return Evaluation::NoMatch,
};
let mut matched_rules: Vec<crate::rule::RuleMatch> = Vec::new();
let mut matched_rules: Vec<RuleMatch> = Vec::new();
let mut best_decision: Option<Decision> = None;
for rule in rules {
if let Some(matched) = rule.matches(cmd) {
@@ -47,9 +54,12 @@ impl Policy {
matched_rules.push(matched);
}
}
best_decision.map(|decision| Evaluation {
decision,
matched_rules,
})
match best_decision {
Some(decision) => Evaluation::Match {
decision,
matched_rules,
},
None => Evaluation::NoMatch,
}
}
}

View File

@@ -37,6 +37,10 @@ impl PrefixPattern {
self.rest.len() + 1
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn matches_prefix(&self, cmd: &[String]) -> Option<Vec<String>> {
if cmd.len() < self.len() || cmd[0] != self.first {
return None;

View File

@@ -1,4 +1,5 @@
use codex_execpolicy2::Decision;
use codex_execpolicy2::Evaluation;
use codex_execpolicy2::PolicyParser;
use codex_execpolicy2::RuleMatch;
use codex_execpolicy2::rule::PatternToken;
@@ -19,10 +20,16 @@ prefix_rule(
.parse()
.expect("parse policy");
let cmd = tokens(&["git", "status"]);
let eval = policy.evaluate(&cmd).expect("match");
assert_eq!(eval.decision, Decision::Allow);
let Evaluation::Match {
decision,
matched_rules,
} = policy.evaluate(&cmd)
else {
panic!("expected match");
};
assert_eq!(decision, Decision::Allow);
assert_eq!(
eval.matched_rules,
matched_rules,
vec![RuleMatch {
rule_id: "git_status".to_string(),
matched_prefix: tokens(&["git", "status"]),
@@ -54,8 +61,10 @@ prefix_rule(
),
(tokens(&["sh", "-l", "echo", "hi"]), tokens(&["sh", "-l"])),
] {
let eval = policy.evaluate(&cmd).expect("match");
assert_eq!(eval.matched_rules[0].matched_prefix, prefix);
let Evaluation::Match { matched_rules, .. } = policy.evaluate(&cmd) else {
panic!("expected match");
};
assert_eq!(matched_rules[0].matched_prefix, prefix);
}
}
@@ -85,7 +94,7 @@ prefix_rule(
tokens(&["npm", "i", "--legacy-peer-deps"]),
tokens(&["npm", "install", "--no-save", "leftpad"]),
] {
assert!(policy.evaluate(&cmd).is_some());
assert!(matches!(policy.evaluate(&cmd), Evaluation::Match { .. }));
}
}
@@ -101,12 +110,14 @@ prefix_rule(
"#;
let parser = PolicyParser::new("test.policy", policy_src);
let policy = parser.parse().expect("parse policy");
assert!(policy.evaluate(&tokens(&["git", "status"])).is_some());
assert!(
policy
.evaluate(&tokens(&["git", "reset", "--hard"]))
.is_none()
);
assert!(matches!(
policy.evaluate(&tokens(&["git", "status"])),
Evaluation::Match { .. }
));
assert!(matches!(
policy.evaluate(&tokens(&["git", "reset", "--hard"])),
Evaluation::NoMatch
));
}
#[test]
@@ -132,10 +143,16 @@ prefix_rule(
let policy = parser.parse().expect("parse policy");
let status = tokens(&["git", "status"]);
let status_eval = policy.evaluate(&status).expect("match");
assert_eq!(status_eval.decision, Decision::Prompt);
let Evaluation::Match {
decision: status_decision,
matched_rules: status_matches,
} = policy.evaluate(&status)
else {
panic!("expected status to match");
};
assert_eq!(status_decision, Decision::Prompt);
assert_eq!(
status_eval.matched_rules,
status_matches,
vec![
RuleMatch {
rule_id: "allow_git_status".to_string(),
@@ -151,10 +168,16 @@ prefix_rule(
);
let commit = tokens(&["git", "commit", "-m", "hi"]);
let commit_eval = policy.evaluate(&commit).expect("match");
assert_eq!(commit_eval.decision, Decision::Forbidden);
let Evaluation::Match {
decision: commit_decision,
matched_rules: commit_matches,
} = policy.evaluate(&commit)
else {
panic!("expected commit to match");
};
assert_eq!(commit_decision, Decision::Forbidden);
assert_eq!(
commit_eval.matched_rules,
commit_matches,
vec![
RuleMatch {
rule_id: "prompt_git".to_string(),