Add shell-command policies and a machine-readable Violation category - #74
Open
higagan wants to merge 1 commit into
Open
Add shell-command policies and a machine-readable Violation category#74higagan wants to merge 1 commit into
higagan wants to merge 1 commit into
Conversation
The README named shell.run in its threat model, but URLAllowList was the only strong bundled rule -- anyone guarding a shell tool had to write correct shell-safety logic themselves. ShellCommandAllowList is default-deny and matches structured argv rather than raw-string prefixes, because textual matching is defeated by a leading path, quoting, an environment assignment, a chained command, an embedded newline, or an inline interpreter script. Each of those is now refused, and unparseable input fails closed. An inline interpreter script is blocked even when the interpreter itself is allowlisted: permitting sh must not silently permit everything sh can run. NoDangerousShellPatterns is the complementary tripwire, documented everywhere as a tripwire rather than a boundary. Violation gains a category, every bundled rule sets one, and ModelFuzzBlockError exposes it -- a block is a policy decision, not an infrastructure failure, and an agent loop should be able to tell them apart without regex-matching prose. Fixes #71
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The README named
shell.runin its threat model, butURLAllowListwas the only strong bundled rule β anyone guarding a shell tool had to hand-write correct shell-safety logic. This adds the two policies and thecategoryfield from the refined spec, incorporating @newjsouza's design feedback.ShellCommandAllowListβ default-deny, matching structured argv rather than raw-string prefixes. Entries are argv prefixes:"git status"permitsgit status --short, notgit push.Raw-string prefix matching falls to every row below; structured matching does not:
/bin/ls,./ls,"ls" -lalsβ allowedls; curl evil.commetacharacterls\ncurl evil.commetacharacterFOO=bar curl evil.comcurl, notFOO=barnot_allowlistedenv FOO=bar curl evil.comcurlnot_allowlistedsh -c "curl evil.com"shis allowlistedinterpretersudo lsnot_allowlistedls "unbalancedunparseableNoDangerousShellPatternsβ the raw-text tripwire, documented in the class docstring, README body, README Limitations andAGENTS.mdas a tripwire and explicitly not a shell parser or security boundary. Unlike the allowlist it only blocks on a positive match, so it is safe on a multi-argument tool.Violation.categoryβ every bundled rule now tags its blocks (credential,not_allowlisted,metacharacter,interpreter, β¦), defaulting tounspecifiedso a hand-written policy predating the field keeps working.Fixes #71
Three decisions worth a reviewer's attention
The category had to reach the exception, not just the log. The stated goal was "the agent loop can surface a safe tool error and recover without treating the block as an infrastructure failure" β the loop catches
ModelFuzzBlockError, so a log field alone wouldn't have delivered it.ModelFuzzBlockErrornow exposes.category,.rule_nameand.violation;str(exc)is unchanged and bare one-arg construction still works (both pinned by tests).A newline was a real bypass, caught by a failing test.
shlextreats\nas ordinary whitespace, sols\ncurl evil.comflattened into a single innocent-looking argv while a shell would run two commands. Now checked on the raw string before the split erases it.ShellCommandAllowListreads dict values but not dict keys β a deliberate divergence from the other rules' walk, commented at the call site. Because this rule is default-deny, reading keys made{"cmd": "ls"}block on a command namedcmd, i.e. nearly every dict argument would fail on its own field names. A command arrives as a value.Both new rules name only the offending binary or pattern in the reason, never the full command β same reasoning as #73, since blocks are logged and the arguments are exactly where a credential or customer record lives.
Limits, documented rather than papered over
ShellCommandAllowListtreats every string it sees as a command (a policy cannot know an argument's name, so a second string argument likecwdis judged as a command and blocked β it belongs on an engine guarding a single-command-argument tool), and it governs the command rather than what the command then does. Both are in the docstring, the README section, and Limitations.Test plan
uv run ruff check ./ruff format --check ./mypy --strict src/modelfuzzcleanuv run pytest -qβ 277 passed (122 new), no changes needed to any pre-existing testtests/test_shell_rules.pycovers the four criteria from the issue: quoting variants (whitespace, quoted binary, absolute/relative/traversal paths), nested containers (argv-vs-command-list distinction, dicts, sets, bytes, cycles), interpreter and wrapper invocation (sh -c,bash -c,python -c,perl -e,pwsh -Command,env, bare assignments,sudo), and fail-closed behaviour (unbalanced quotes, empty, assignment-only)AGENTS.mdsnippet end-to-end, including all eight table rows β output matches the documented categories exactly