mirror of
https://github.com/openai/codex.git
synced 2026-05-04 05:11:37 +03:00
execpolicy: add host_executable() path mappings (#12964)
## Why `execpolicy` currently keys `prefix_rule()` matching off the literal first token. That works for rules like `["/usr/bin/git"]`, but it means shared basename rules such as `["git"]` do not help when a caller passes an absolute executable path like `/usr/bin/git`. This PR lays the groundwork for basename-aware matching without changing existing callers yet. It adds typed host-executable metadata and an opt-in resolution path in `codex-execpolicy`, so a follow-up PR can adopt the new behavior in `unix_escalation.rs` and other call sites without having to redesign the policy layer first. ## What Changed - added `host_executable(name = ..., paths = [...])` to the execpolicy parser and validated it with `AbsolutePathBuf` - stored host executable mappings separately from prefix rules inside `Policy` - added `MatchOptions` and opt-in `*_with_options()` APIs that preserve existing behavior by default - implemented exact-first matching with optional basename fallback, gated by `host_executable()` allowlists when present - normalized executable names for cross-platform matching so Windows paths like `git.exe` can satisfy `host_executable(name = "git", ...)` - updated `match` / `not_match` example validation to exercise the host-executable resolution path instead of only raw prefix-rule matching - preserved source locations for deferred example-validation errors so policy load failures still point at the right file and line - surfaced `resolvedProgram` on `RuleMatch` so callers can tell when a basename rule matched an absolute executable path - preserved host executable metadata when requirements policies overlay file-based policies in `core/src/exec_policy.rs` - documented the new rule shape and CLI behavior in `execpolicy/README.md` ## Verification - `cargo test -p codex-execpolicy` - added coverage in `execpolicy/tests/basic.rs` for parsing, precedence, empty allowlists, basename fallback, exact-match precedence, and host-executable-backed `match` / `not_match` examples - added a regression test in `core/src/exec_policy.rs` to verify requirements overlays preserve `host_executable()` metadata - verified `cargo test -p codex-core --lib`, including source-rendering coverage for deferred validation errors
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
use crate::decision::Decision;
|
||||
use crate::error::Error;
|
||||
use crate::error::Result;
|
||||
use crate::policy::MatchOptions;
|
||||
use crate::policy::Policy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use shlex::try_join;
|
||||
@@ -63,6 +66,8 @@ pub enum RuleMatch {
|
||||
#[serde(rename = "matchedPrefix")]
|
||||
matched_prefix: Vec<String>,
|
||||
decision: Decision,
|
||||
#[serde(rename = "resolvedProgram", skip_serializing_if = "Option::is_none")]
|
||||
resolved_program: Option<AbsolutePathBuf>,
|
||||
/// Optional rationale for why this rule exists.
|
||||
///
|
||||
/// This can be supplied for any decision and may be surfaced in different contexts
|
||||
@@ -83,6 +88,23 @@ impl RuleMatch {
|
||||
Self::HeuristicsRuleMatch { decision, .. } => *decision,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_resolved_program(self, resolved_program: &AbsolutePathBuf) -> Self {
|
||||
match self {
|
||||
Self::PrefixRuleMatch {
|
||||
matched_prefix,
|
||||
decision,
|
||||
justification,
|
||||
..
|
||||
} => Self::PrefixRuleMatch {
|
||||
matched_prefix,
|
||||
decision,
|
||||
resolved_program: Some(resolved_program.clone()),
|
||||
justification,
|
||||
},
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -210,6 +232,7 @@ impl Rule for PrefixRule {
|
||||
.map(|matched_prefix| RuleMatch::PrefixRuleMatch {
|
||||
matched_prefix,
|
||||
decision: self.decision,
|
||||
resolved_program: None,
|
||||
justification: self.justification.clone(),
|
||||
})
|
||||
}
|
||||
@@ -220,11 +243,21 @@ impl Rule for PrefixRule {
|
||||
}
|
||||
|
||||
/// Count how many rules match each provided example and error if any example is unmatched.
|
||||
pub(crate) fn validate_match_examples(rules: &[RuleRef], matches: &[Vec<String>]) -> Result<()> {
|
||||
pub(crate) fn validate_match_examples(
|
||||
policy: &Policy,
|
||||
rules: &[RuleRef],
|
||||
matches: &[Vec<String>],
|
||||
) -> Result<()> {
|
||||
let mut unmatched_examples = Vec::new();
|
||||
let options = MatchOptions {
|
||||
resolve_host_executables: true,
|
||||
};
|
||||
|
||||
for example in matches {
|
||||
if rules.iter().any(|rule| rule.matches(example).is_some()) {
|
||||
if !policy
|
||||
.matches_for_command_with_options(example, None, &options)
|
||||
.is_empty()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -240,21 +273,31 @@ pub(crate) fn validate_match_examples(rules: &[RuleRef], matches: &[Vec<String>]
|
||||
Err(Error::ExampleDidNotMatch {
|
||||
rules: rules.iter().map(|rule| format!("{rule:?}")).collect(),
|
||||
examples: unmatched_examples,
|
||||
location: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure that no rule matches any provided negative example.
|
||||
pub(crate) fn validate_not_match_examples(
|
||||
rules: &[RuleRef],
|
||||
policy: &Policy,
|
||||
_rules: &[RuleRef],
|
||||
not_matches: &[Vec<String>],
|
||||
) -> Result<()> {
|
||||
let options = MatchOptions {
|
||||
resolve_host_executables: true,
|
||||
};
|
||||
|
||||
for example in not_matches {
|
||||
if let Some(rule) = rules.iter().find(|rule| rule.matches(example).is_some()) {
|
||||
if let Some(rule) = policy
|
||||
.matches_for_command_with_options(example, None, &options)
|
||||
.first()
|
||||
{
|
||||
return Err(Error::ExampleDidMatch {
|
||||
rule: format!("{rule:?}"),
|
||||
example: try_join(example.iter().map(String::as_str))
|
||||
.unwrap_or_else(|_| "unable to render example".to_string()),
|
||||
location: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user