Skip to content

Commit bf1f7d7

Browse files
authored
feat(evaluators): add starts_with/ends_with mode to list evaluator (#154)
## Summary - add `starts_with` and `ends_with` match modes to the built-in `list` evaluator so policies can be expressed as plain prefix and suffix lists instead of synthesized regexes - expose the new modes in the UI evaluator form and update the shared UI types - expand evaluator coverage for prefix and suffix matching, allowlist-style `no_match` usage, path-like inputs, ordinary strings, escaping, and list-input semantics - document the new list evaluator modes in the evaluator overview README ## Scope - User-facing/API changes: `list` evaluator configs now accept `match_mode: "starts_with"` and `match_mode: "ends_with"`; the UI exposes both as selectable options - Internal changes: compile the new match modes into anchored prefix/suffix regexes in the evaluator implementation and add targeted evaluator tests - Out of scope: changing prefix or suffix semantics to be path-boundary-aware; this PR preserves generic string-prefix and string-suffix behavior ## Risk and Rollout - Risk level: low - Rollback plan: revert commits `5d29b8a`, `4ddef38`, `567f25f`, and `ef694b2`, which removes the new match modes, coverage, and docs updates ## Testing - [x] Added or updated automated tests - [x] Ran `make check` (or explained why not) - [ ] Manually verified behavior ## Checklist - [ ] Linked issue/spec (if applicable) - [x] Updated docs/examples for user-facing changes - [ ] Included any required follow-up tasks
1 parent 90265ba commit bf1f7d7

6 files changed

Lines changed: 457 additions & 8 deletions

File tree

‎evaluators/README.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,14 @@ Pattern matching for text (PII, keywords, SQL injection)
1919
```
2020

2121
### List
22-
Match against value lists (blocked users, restricted cities)
22+
Match against value lists (blocked users, restricted cities, allowed prefixes).
23+
Supports `match_mode: "exact"` for full-string membership, `match_mode: "contains"`
24+
for keyword-style matching, `match_mode: "starts_with"` for prefix matching, and
25+
`match_mode: "ends_with"` for suffix matching.
2326
```python
2427
{"name": "list", "config": {"values": ["admin", "root"], "case_sensitive": False}}
28+
{"name": "list", "config": {"values": ["/home/lev/agent-control"], "match_mode": "starts_with"}}
29+
{"name": "list", "config": {"values": [".md"], "match_mode": "ends_with"}}
2530
```
2631

2732
### SQL

‎evaluators/builtin/src/agent_control_evaluators/list/config.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ class ListEvaluatorConfig(EvaluatorConfig):
1919
match_on: Literal["match", "no_match"] = Field(
2020
"match", description="Trigger rule on match or no match"
2121
)
22-
match_mode: Literal["exact", "contains"] = Field(
22+
match_mode: Literal["exact", "contains", "starts_with", "ends_with"] = Field(
2323
"exact",
24-
description="'exact' for full string match, 'contains' for keyword/substring match",
24+
description=(
25+
"'exact' for full string match, 'contains' for keyword matching, "
26+
"'starts_with' for prefix matching, and 'ends_with' for suffix matching"
27+
),
2528
)
2629
case_sensitive: bool = Field(False, description="Whether matching is case sensitive")
2730

‎evaluators/builtin/src/agent_control_evaluators/list/evaluator.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class ListEvaluator(Evaluator[ListEvaluatorConfig]):
1818
Checks if data matches values in a list. Supports:
1919
- any/all logic (match any value vs match all values)
2020
- match/no_match trigger (trigger on match or no match)
21-
- exact/contains mode (full match vs substring/keyword)
21+
- exact/contains/starts_with/ends_with mode (full match vs keyword vs prefix/suffix)
2222
- case sensitivity toggle
2323
2424
Example configs:
@@ -50,6 +50,12 @@ def _build_regex(self) -> Any:
5050
if self.config.match_mode == "contains":
5151
# Word boundary matching for substring/keyword detection
5252
pattern = f"\\b({'|'.join(escaped)})\\b"
53+
elif self.config.match_mode == "starts_with":
54+
# Prefix matching using anchors
55+
pattern = f"^({'|'.join(escaped)})"
56+
elif self.config.match_mode == "ends_with":
57+
# Suffix matching using anchors
58+
pattern = f"({'|'.join(escaped)})$"
5359
else:
5460
# Exact match using anchors
5561
pattern = f"^({'|'.join(escaped)})$"

0 commit comments

Comments
 (0)