feat: tokenizing whole strings in match and not_match

This commit is contained in:
kevin zhao
2025-11-13 22:07:42 -05:00
parent 814c6506d2
commit d3aafb93f3
3 changed files with 56 additions and 31 deletions
+3 -3
View File
@@ -4,7 +4,7 @@
- Policy engine and CLI built around `prefix_rule(pattern=[...], decision?, match?, not_match?)`.
- This release covers only the prefix-rule subset of the planned execpolicy v2 language; a richer language will follow.
- Tokens are matched in order; any `pattern` element may be a list to denote alternatives. `decision` defaults to `allow`; valid values: `allow`, `prompt`, `forbidden`.
- `match` / `not_match` supply example invocations that are validated at load time (think of them as unit tests).
- `match` / `not_match` supply example invocations that are validated at load time (think of them as unit tests); examples can be token arrays or strings (strings are tokenized with `shlex`).
- The CLI always prints the JSON serialization of the evaluation result (whether a match or not).
## Policy shapes
@@ -13,8 +13,8 @@
prefix_rule(
pattern = ["cmd", ["alt1", "alt2"]], # ordered tokens; list entries denote alternatives
decision = "prompt", # allow | prompt | forbidden; defaults to allow
match = [["cmd", "alt1"]], # examples that must match this rule
not_match = [["cmd", "oops"]], # examples that must not match this rule
match = [["cmd", "alt1"], "cmd alt2"], # examples that must match this rule
not_match = [["cmd", "oops"], "cmd alt3"], # examples that must not match this rule
)
```
+48 -26
View File
@@ -1,5 +1,6 @@
use multimap::MultiMap;
use parking_lot::Mutex;
use shlex;
use starlark::any::ProvidesStaticType;
use starlark::environment::GlobalsBuilder;
use starlark::environment::Module;
@@ -123,34 +124,55 @@ fn parse_pattern_token<'v>(value: Value<'v>) -> Result<PatternToken> {
}
fn parse_examples<'v>(examples: UnpackList<Value<'v>>) -> Result<Vec<Vec<String>>> {
examples
.items
.into_iter()
.map(|example| {
let list = ListRef::from_value(example).ok_or_else(|| {
Error::InvalidExample("example must be a list of strings".to_string())
})?;
let tokens: Vec<String> = list
.content()
.iter()
.map(|value| {
value
.unpack_str()
.ok_or_else(|| {
Error::InvalidExample("example tokens must be strings".to_string())
})
.map(str::to_string)
})
.collect::<Result<_>>()?;
examples.items.into_iter().map(parse_example).collect()
}
match tokens.as_slice() {
[] => Err(Error::InvalidExample(
"example cannot be an empty list".to_string(),
)),
_ => Ok(tokens),
}
fn parse_example<'v>(value: Value<'v>) -> Result<Vec<String>> {
if let Some(raw) = value.unpack_str() {
return parse_string_example(raw);
}
if let Some(list) = ListRef::from_value(value) {
return parse_list_example(&list);
}
Err(Error::InvalidExample(format!(
"example must be a string or list of strings (got {})",
value.get_type()
)))
}
fn parse_string_example(raw: &str) -> Result<Vec<String>> {
let tokens = shlex::split(raw).ok_or_else(|| {
Error::InvalidExample("example string has invalid shell syntax".to_string())
})?;
match tokens.as_slice() {
[] => Err(Error::InvalidExample(
"example cannot be an empty string".to_string(),
)),
_ => Ok(tokens),
}
}
fn parse_list_example(list: &ListRef) -> Result<Vec<String>> {
let tokens: Vec<String> = list
.content()
.iter()
.map(|value| {
value
.unpack_str()
.ok_or_else(|| Error::InvalidExample("example tokens must be strings".to_string()))
.map(str::to_string)
})
.collect()
.collect::<Result<_>>()?;
match tokens.as_slice() {
[] => Err(Error::InvalidExample(
"example cannot be an empty list".to_string(),
)),
_ => Ok(tokens),
}
}
fn policy_builder<'v, 'a>(eval: &Evaluator<'v, 'a, '_>) -> &'a PolicyBuilder {
+5 -2
View File
@@ -112,8 +112,11 @@ fn match_and_not_match_examples_are_enforced() {
let policy_src = r#"
prefix_rule(
pattern = ["git", "status"],
match = [["git", "status"]],
not_match = [["git", "--config", "color.status=always", "status"]],
match = [["git", "status"], "git status"],
not_match = [
["git", "--config", "color.status=always", "status"],
"git --config color.status=always status",
],
)
"#;
let parser = PolicyParser::new("test.codexpolicy", policy_src);