From b2f54010ba66373e2ef7a97a8365e744678ad4d6 Mon Sep 17 00:00:00 2001 From: rockymadden Date: Tue, 4 Aug 2026 07:14:10 -0600 Subject: [PATCH 01/10] fix(command-executed): match patterns against shell-normalized commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `command_pattern` regexes were run only against the raw `bash -lc "..."` wrapper string, so whichever way the agent happened to quote an argument (bare, "double", 'single', \"escaped\") leaked into the pattern. Authors hand-model that escaping and get it subtly wrong: ~173/1557 criteria encode quote-tolerance, in four inconsistent idioms. skill-flow-paginated-reference-lookup used `\\?"?` (double-quote only); the agent single-quoted the resource arg, so a correct multi-call pagination loop scored 0.0 on a gating criterion — a false negative — while the sibling `nextPage=` criterion matched the same calls. Normalize before matching: unwrap the `bash`/`sh -c` wrapper and resolve shell quoting via shlex, then match against BOTH the raw text and the normalized form (either hit counts). This is additive — nothing that matched before can stop matching — and lands in the shared `_matching_commands` helper so the score and the live early-stop trigger stay consistent. Shell operators (`&&`, `|`, `>`) survive as tokens; unparseable input (unbalanced quotes, heredocs) falls back to the raw text. Fixes the whole class with no task-YAML changes. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/coder_eval/criteria/command_executed.py | 82 +++++++++++- tests/test_command_executed.py | 141 ++++++++++++++++++++ 2 files changed, 217 insertions(+), 6 deletions(-) diff --git a/src/coder_eval/criteria/command_executed.py b/src/coder_eval/criteria/command_executed.py index e5343ef2..a87da535 100644 --- a/src/coder_eval/criteria/command_executed.py +++ b/src/coder_eval/criteria/command_executed.py @@ -3,6 +3,7 @@ import json import logging import re +import shlex from typing import TYPE_CHECKING from coder_eval.criteria.base import BaseCriterion, CheckContext, LiveVerdict, register_criterion @@ -19,6 +20,73 @@ # Limit regex search input length to mitigate ReDoS on large command strings _MAX_PATTERN_SEARCH_LEN = 2000 +# Skip shell-normalization above this size — shlex is linear so this is only a +# worst-case guard; real telemetry commands are far smaller. +_MAX_NORMALIZE_LEN = 10 * _MAX_PATTERN_SEARCH_LEN + +# Shells whose `-c`/`-lc` payload is the real command we want to match against. +_SHELL_WRAPPERS = {"bash", "sh"} +_SHELL_CMD_FLAGS = {"-c", "-lc", "-lic"} + + +def _normalize_shell(cmd_text: str) -> str | None: + """Quote-resolved, wrapper-stripped form of a shell command, or None. + + ``command_pattern`` regexes are written against the *logical* command + (``uip is resources run list ``), but telemetry records the + raw ``bash -lc "..."`` wrapper — so whichever way the agent happened to quote + an argument (bare, ``"double"``, ``'single'``, ``\\"escaped\\"``) leaks into + the pattern. Authors then hand-model that escaping and get it subtly wrong + (e.g. allowing ``"`` but not ``'``), silently under-counting correct calls. + + This unwraps a ``bash``/``sh -c`` wrapper and resolves shell quoting with + ``shlex`` so a pattern can match argv semantics regardless of quoting. + Shell operators (``&&``, ``|``, ``>``) survive as their own tokens, so + patterns that reference them keep working. Returns ``None`` when the text + can't be parsed (unbalanced quotes, heredocs); the caller keeps the raw + text as a haystack, so nothing that matched before can stop matching. + """ + try: + tokens = shlex.split(cmd_text, posix=True) + except ValueError: + return None + if not tokens: + return None + # Unwrap `bash -lc "