core-agent-ide/codex-rs/execpolicy/src/execpolicycheck.rs
zhao-oai 3d35cb4619
Refactor execpolicy fallback evaluation (#7544)
## Refactor of the `execpolicy` crate

To illustrate why we need this refactor, consider an agent attempting to
run `apple | rm -rf ./`. Suppose `apple` is allowed by `execpolicy`.
Before this PR, `execpolicy` would consider `apple` and `pear` and only
render one rule match: `Allow`. We would skip any heuristics checks on
`rm -rf ./` and immediately approve `apple | rm -rf ./` to run.

To fix this, we now thread a `fallback` evaluation function into
`execpolicy` that runs when no `execpolicy` rules match a given command.
In our example, we would run `fallback` on `rm -rf ./` and prevent
`apple | rm -rf ./` from being run without approval.
2025-12-03 23:39:48 -08:00

83 lines
2.4 KiB
Rust

use std::fs;
use std::path::PathBuf;
use anyhow::Context;
use anyhow::Result;
use clap::Parser;
use serde::Serialize;
use crate::Decision;
use crate::Policy;
use crate::PolicyParser;
use crate::RuleMatch;
/// Arguments for evaluating a command against one or more execpolicy files.
#[derive(Debug, Parser, Clone)]
pub struct ExecPolicyCheckCommand {
/// Paths to execpolicy files to evaluate (repeatable).
#[arg(short = 'p', long = "policy", value_name = "PATH", required = true)]
pub policies: Vec<PathBuf>,
/// Pretty-print the JSON output.
#[arg(long)]
pub pretty: bool,
/// Command tokens to check against the policy.
#[arg(
value_name = "COMMAND",
required = true,
trailing_var_arg = true,
allow_hyphen_values = true
)]
pub command: Vec<String>,
}
impl ExecPolicyCheckCommand {
/// Load the policies for this command, evaluate the command, and render JSON output.
pub fn run(&self) -> Result<()> {
let policy = load_policies(&self.policies)?;
let matched_rules = policy.matches_for_command(&self.command, None);
let json = format_matches_json(&matched_rules, self.pretty)?;
println!("{json}");
Ok(())
}
}
pub fn format_matches_json(matched_rules: &[RuleMatch], pretty: bool) -> Result<String> {
let output = ExecPolicyCheckOutput {
matched_rules,
decision: matched_rules.iter().map(RuleMatch::decision).max(),
};
if pretty {
serde_json::to_string_pretty(&output).map_err(Into::into)
} else {
serde_json::to_string(&output).map_err(Into::into)
}
}
pub fn load_policies(policy_paths: &[PathBuf]) -> Result<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)
.with_context(|| format!("failed to parse policy at {}", policy_path.display()))?;
}
Ok(parser.build())
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ExecPolicyCheckOutput<'a> {
#[serde(rename = "matchedRules")]
matched_rules: &'a [RuleMatch],
#[serde(skip_serializing_if = "Option::is_none")]
decision: Option<Decision>,
}