execpolicy2 extension (#6627)

- enabling execpolicy2 parser to parse multiple policy files to build a
combined `Policy` (useful if codex detects many `.codexpolicy` files)
- adding functionality to `Policy` to allow evaluation of multiple cmds
at once (useful when we have chained commands)
This commit is contained in:
zhao-oai
2025-11-17 16:44:41 -08:00
committed by GitHub
parent cecbd5b021
commit 7ab45487dd
5 changed files with 223 additions and 53 deletions

View File

@@ -1,5 +1,4 @@
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use anyhow::Context;
@@ -13,8 +12,12 @@ use codex_execpolicy2::PolicyParser;
enum Cli {
/// Evaluate a command against a policy.
Check {
#[arg(short, long, value_name = "PATH")]
policy: PathBuf,
#[arg(short, long = "policy", value_name = "PATH", required = true)]
policies: Vec<PathBuf>,
/// Pretty-print the JSON output.
#[arg(long)]
pretty: bool,
/// Command tokens to check.
#[arg(
@@ -30,25 +33,34 @@ enum Cli {
fn main() -> Result<()> {
let cli = Cli::parse();
match cli {
Cli::Check { policy, command } => cmd_check(policy, command),
Cli::Check {
policies,
command,
pretty,
} => cmd_check(policies, command, pretty),
}
}
fn cmd_check(policy_path: PathBuf, args: Vec<String>) -> Result<()> {
let policy = load_policy(&policy_path)?;
fn cmd_check(policy_paths: Vec<PathBuf>, args: Vec<String>, pretty: bool) -> Result<()> {
let policy = load_policies(&policy_paths)?;
let eval = policy.check(&args);
let json = serde_json::to_string_pretty(&eval)?;
let json = if pretty {
serde_json::to_string_pretty(&eval)?
} else {
serde_json::to_string(&eval)?
};
println!("{json}");
Ok(())
}
fn load_policy(policy_path: &Path) -> Result<codex_execpolicy2::Policy> {
let policy_file_contents = fs::read_to_string(policy_path)
.with_context(|| format!("failed to read policy at {}", policy_path.display()))?;
let policy_identifier = policy_path.to_string_lossy();
Ok(PolicyParser::parse(
policy_identifier.as_ref(),
&policy_file_contents,
)?)
fn load_policies(policy_paths: &[PathBuf]) -> Result<codex_execpolicy2::Policy> {
let mut parser = PolicyParser::new();
for policy_path in policy_paths {
let policy_file_contents = fs::read_to_string(policy_path)
.with_context(|| format!("failed to read policy at {}", policy_path.display()))?;
let policy_identifier = policy_path.to_string_lossy().to_string();
parser.parse(&policy_identifier, &policy_file_contents)?;
}
Ok(parser.build())
}

View File

@@ -25,16 +25,26 @@ use crate::rule::RuleRef;
use crate::rule::validate_match_examples;
use crate::rule::validate_not_match_examples;
// todo: support parsing multiple policies
pub struct PolicyParser;
pub struct PolicyParser {
builder: RefCell<PolicyBuilder>,
}
impl Default for PolicyParser {
fn default() -> Self {
Self::new()
}
}
impl PolicyParser {
pub fn new() -> Self {
Self {
builder: RefCell::new(PolicyBuilder::new()),
}
}
/// Parses a policy, tagging parser errors with `policy_identifier` so failures include the
/// identifier alongside line numbers.
pub fn parse(
policy_identifier: &str,
policy_file_contents: &str,
) -> Result<crate::policy::Policy> {
pub fn parse(&mut self, policy_identifier: &str, policy_file_contents: &str) -> Result<()> {
let mut dialect = Dialect::Extended.clone();
dialect.enable_f_strings = true;
let ast = AstModule::parse(
@@ -45,14 +55,16 @@ impl PolicyParser {
.map_err(Error::Starlark)?;
let globals = GlobalsBuilder::standard().with(policy_builtins).build();
let module = Module::new();
let builder = RefCell::new(PolicyBuilder::new());
{
let mut eval = Evaluator::new(&module);
eval.extra = Some(&builder);
eval.extra = Some(&self.builder);
eval.eval_module(ast, &globals).map_err(Error::Starlark)?;
}
Ok(builder.into_inner().build())
Ok(())
}
pub fn build(self) -> crate::policy::Policy {
self.builder.into_inner().build()
}
}

View File

@@ -38,6 +38,28 @@ impl Policy {
None => Evaluation::NoMatch,
}
}
pub fn check_multiple<Commands>(&self, commands: Commands) -> Evaluation
where
Commands: IntoIterator,
Commands::Item: AsRef<[String]>,
{
let matched_rules: Vec<RuleMatch> = commands
.into_iter()
.flat_map(|command| match self.check(command.as_ref()) {
Evaluation::Match { matched_rules, .. } => matched_rules,
Evaluation::NoMatch => Vec::new(),
})
.collect();
match matched_rules.iter().map(RuleMatch::decision).max() {
Some(decision) => Evaluation::Match {
decision,
matched_rules,
},
None => Evaluation::NoMatch,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]