core-agent-ide/codex-rs/execpolicy/src/error.rs
Michael Bolin b148d98e0e
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
2026-02-27 12:59:24 -08:00

101 lines
2.8 KiB
Rust

use starlark::Error as StarlarkError;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TextPosition {
pub line: usize,
pub column: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TextRange {
pub start: TextPosition,
pub end: TextPosition,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorLocation {
pub path: String,
pub range: TextRange,
}
#[derive(Debug, Error)]
pub enum Error {
#[error("invalid decision: {0}")]
InvalidDecision(String),
#[error("invalid pattern element: {0}")]
InvalidPattern(String),
#[error("invalid example: {0}")]
InvalidExample(String),
#[error("invalid rule: {0}")]
InvalidRule(String),
#[error(
"expected every example to match at least one rule. rules: {rules:?}; unmatched examples: \
{examples:?}"
)]
ExampleDidNotMatch {
rules: Vec<String>,
examples: Vec<String>,
location: Option<ErrorLocation>,
},
#[error("expected example to not match rule `{rule}`: {example}")]
ExampleDidMatch {
rule: String,
example: String,
location: Option<ErrorLocation>,
},
#[error("starlark error: {0}")]
Starlark(StarlarkError),
}
impl Error {
pub fn with_location(self, location: ErrorLocation) -> Self {
match self {
Error::ExampleDidNotMatch {
rules,
examples,
location: None,
} => Error::ExampleDidNotMatch {
rules,
examples,
location: Some(location),
},
Error::ExampleDidMatch {
rule,
example,
location: None,
} => Error::ExampleDidMatch {
rule,
example,
location: Some(location),
},
other => other,
}
}
pub fn location(&self) -> Option<ErrorLocation> {
match self {
Error::ExampleDidNotMatch { location, .. }
| Error::ExampleDidMatch { location, .. } => location.clone(),
Error::Starlark(err) => err.span().map(|span| {
let resolved = span.resolve_span();
ErrorLocation {
path: span.filename().to_string(),
range: TextRange {
start: TextPosition {
line: resolved.begin.line + 1,
column: resolved.begin.column + 1,
},
end: TextPosition {
line: resolved.end.line + 1,
column: resolved.end.column + 1,
},
},
}
}),
_ => None,
}
}
}