From 1187cac306ea64a9712f91e8bac82ebb90c5e136 Mon Sep 17 00:00:00 2001 From: kevin zhao Date: Tue, 2 Dec 2025 23:03:22 +0000 Subject: [PATCH] update cli with new behavior --- codex-rs/cli/tests/execpolicy.rs | 18 ++++++------ codex-rs/execpolicy/README.md | 32 ++++++++-------------- codex-rs/execpolicy/src/execpolicycheck.rs | 27 ++++++++++++++---- codex-rs/execpolicy/src/main.rs | 10 ++----- codex-rs/execpolicy/src/policy.rs | 17 +++++++----- 5 files changed, 54 insertions(+), 50 deletions(-) diff --git a/codex-rs/cli/tests/execpolicy.rs b/codex-rs/cli/tests/execpolicy.rs index c6bca85bc6..4610b95874 100644 --- a/codex-rs/cli/tests/execpolicy.rs +++ b/codex-rs/cli/tests/execpolicy.rs @@ -40,17 +40,15 @@ prefix_rule( assert_eq!( result, json!({ - "match": { - "decision": "forbidden", - "matchedRules": [ - { - "prefixRuleMatch": { - "matchedPrefix": ["git", "push"], - "decision": "forbidden" - } + "decision": "forbidden", + "matchedRules": [ + { + "prefixRuleMatch": { + "matchedPrefix": ["git", "push"], + "decision": "forbidden" } - ] - } + } + ] }) ); diff --git a/codex-rs/execpolicy/README.md b/codex-rs/execpolicy/README.md index 9fd9c63306..1ddb67252f 100644 --- a/codex-rs/execpolicy/README.md +++ b/codex-rs/execpolicy/README.md @@ -30,32 +30,24 @@ codex execpolicy check --policy path/to/policy.codexpolicy git status cargo run -p codex-execpolicy -- check --policy path/to/policy.codexpolicy git status ``` - Example outcomes: - - Match: `{"match": { ... "decision": "allow" ... }}` - - No match: `{"noMatch": {}}` + - Match: `{"matchedRules":[{...}],"decision":"allow"}` + - No match: `{"matchedRules":[]}` -## Response shapes -- Match: +## Response shape ```json { - "match": { - "decision": "allow|prompt|forbidden", - "matchedRules": [ - { - "prefixRuleMatch": { - "matchedPrefix": ["", "..."], - "decision": "allow|prompt|forbidden" - } + "matchedRules": [ + { + "prefixRuleMatch": { + "matchedPrefix": ["", "..."], + "decision": "allow|prompt|forbidden" } - ] - } + } + ], + "decision": "allow|prompt|forbidden" } ``` - -- No match: -```json -{"noMatch": {}} -``` - +- When no rules match, `matchedRules` is an empty array and `decision` is omitted. - `matchedRules` lists every rule whose prefix matched the command; `matchedPrefix` is the exact prefix that matched. - The effective `decision` is the strictest severity across all matches (`forbidden` > `prompt` > `allow`). diff --git a/codex-rs/execpolicy/src/execpolicycheck.rs b/codex-rs/execpolicy/src/execpolicycheck.rs index d26a0de126..939ed8590b 100644 --- a/codex-rs/execpolicy/src/execpolicycheck.rs +++ b/codex-rs/execpolicy/src/execpolicycheck.rs @@ -4,11 +4,12 @@ use std::path::PathBuf; use anyhow::Context; use anyhow::Result; use clap::Parser; +use serde::Serialize; use crate::Decision; -use crate::Evaluation; use crate::Policy; use crate::PolicyParser; +use crate::RuleMatch; /// Arguments for evaluating a command against one or more execpolicy files. #[derive(Debug, Parser, Clone)] @@ -35,20 +36,25 @@ 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 evaluation = policy.check(&self.command, &|_| Decision::Allow); + let matched_rules = policy.matches_for_command(&self.command, None); - let json = format_evaluation_json(&evaluation, self.pretty)?; + let json = format_matches_json(&matched_rules, self.pretty)?; println!("{json}"); Ok(()) } } -pub fn format_evaluation_json(evaluation: &Evaluation, pretty: bool) -> Result { +pub fn format_matches_json(matched_rules: &[RuleMatch], pretty: bool) -> Result { + let output = ExecPolicyCheckOutput { + matched_rules, + decision: matched_rules.iter().map(RuleMatch::decision).max(), + }; + if pretty { - serde_json::to_string_pretty(evaluation).map_err(Into::into) + serde_json::to_string_pretty(&output).map_err(Into::into) } else { - serde_json::to_string(evaluation).map_err(Into::into) + serde_json::to_string(&output).map_err(Into::into) } } @@ -66,3 +72,12 @@ pub fn load_policies(policy_paths: &[PathBuf]) -> Result { 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, +} diff --git a/codex-rs/execpolicy/src/main.rs b/codex-rs/execpolicy/src/main.rs index 5b5296890a..be20d4a602 100644 --- a/codex-rs/execpolicy/src/main.rs +++ b/codex-rs/execpolicy/src/main.rs @@ -4,8 +4,8 @@ use std::path::PathBuf; use anyhow::Context; use anyhow::Result; use clap::Parser; -use codex_execpolicy::Decision; use codex_execpolicy::PolicyParser; +use codex_execpolicy::execpolicycheck::format_matches_json; /// CLI for evaluating exec policies #[derive(Parser)] @@ -45,12 +45,8 @@ fn main() -> Result<()> { fn cmd_check(policy_paths: Vec, args: Vec, pretty: bool) -> Result<()> { let policy = load_policies(&policy_paths)?; - let eval = policy.check(&args, &|_| Decision::Allow); - let json = if pretty { - serde_json::to_string_pretty(&eval)? - } else { - serde_json::to_string(&eval)? - }; + let matched_rules = policy.matches_for_command(&args, None); + let json = format_matches_json(&matched_rules, pretty)?; println!("{json}"); Ok(()) } diff --git a/codex-rs/execpolicy/src/policy.rs b/codex-rs/execpolicy/src/policy.rs index 6da5857c37..9b6915951b 100644 --- a/codex-rs/execpolicy/src/policy.rs +++ b/codex-rs/execpolicy/src/policy.rs @@ -54,7 +54,7 @@ impl Policy { where F: Fn(&[String]) -> Decision, { - let matched_rules = self.matches_for_command(cmd, heuristics_fallback); + let matched_rules = self.matches_for_command(cmd, Some(heuristics_fallback)); Evaluation::from_matches(matched_rules) } @@ -70,16 +70,19 @@ impl Policy { { let matched_rules: Vec = commands .into_iter() - .flat_map(|command| self.matches_for_command(command.as_ref(), heuristics_fallback)) + .flat_map(|command| { + self.matches_for_command(command.as_ref(), Some(heuristics_fallback)) + }) .collect(); Evaluation::from_matches(matched_rules) } - fn matches_for_command(&self, cmd: &[String], heuristics_fallback: &F) -> Vec - where - F: Fn(&[String]) -> Decision, - { + pub fn matches_for_command( + &self, + cmd: &[String], + heuristics_fallback: Option<&dyn Fn(&[String]) -> Decision>, + ) -> Vec { let mut matched_rules: Vec = match cmd.first() { Some(first) => self .rules_by_program @@ -89,7 +92,7 @@ impl Policy { None => Vec::new(), }; - if matched_rules.is_empty() { + if let (true, Some(heuristics_fallback)) = (matched_rules.is_empty(), heuristics_fallback) { matched_rules.push(RuleMatch::HeuristicsRuleMatch { command: cmd.to_vec(), decision: heuristics_fallback(cmd),