fix: allow_patterns take priority over deny_patterns in ExecTool#3594
Merged
Re-bin merged 6 commits intoHKUDS:mainfrom May 2, 2026
Merged
fix: allow_patterns take priority over deny_patterns in ExecTool#3594Re-bin merged 6 commits intoHKUDS:mainfrom
Re-bin merged 6 commits intoHKUDS:mainfrom
Conversation
Previously deny_patterns were checked first with no bypass, meaning allow_patterns could never exempt commands from the built-in deny list. This made it impossible to whitelist destructive commands for specific directories (e.g. build/cleanup tasks). Changes: - shell.py: check allow_patterns first; if matched, skip deny check - shell.py: deny_patterns now appends to built-in list (not replaces) - schema.py: add allow_patterns/deny_patterns to ExecToolConfig - loop.py/subagent.py: pass allow_patterns/deny_patterns to ExecTool - Add test_exec_allow_patterns.py covering priority semantics
The deny pattern error message "Command blocked by safety guard" was
included in _WORKSPACE_BLOCK_MARKERS, causing deny_pattern blocks to be
misclassified as fatal workspace violations. This meant LLMs had no
chance to retry with a different command — the turn was aborted
immediately.
Changes:
- shell.py: deny/allowlist error messages now use distinct phrasing
("blocked by deny pattern filter" / "blocked by allowlist filter")
- runner.py: remove "blocked by safety guard" from
_WORKSPACE_BLOCK_MARKERS so deny_pattern errors are treated as normal
tool errors (LLM can retry) instead of fatal violations
- workspace path errors still use "blocked by safety guard" and remain
fatal as intended
Address review feedback on the deny/allow_patterns rework: - runner.py: re-add "internal/private url detected" to _WORKSPACE_BLOCK_MARKERS. The earlier marker removal also stripped fatal classification from SSRF / internal-URL rejections (whose message still says "blocked by safety guard"), turning a hard security boundary into something the LLM could retry. - loop.py / subagent.py: drop `or None` between ExecToolConfig and ExecTool. The schema default is an empty list and ExecTool already normalizes None back to [], so the indirection was a no-op. - shell.py: extract `explicitly_allowed` flag in _guard_command so allow_patterns are scanned once instead of twice and the control flow no longer relies on a no-op `pass + else` branch. - tests/agent/test_runner.py: add a regression test asserting that the SSRF block message is treated as fatal, while deny/allowlist filter messages are deliberately non-fatal.
Keep the new ExecTool allow-pattern coverage clean under ruff. Co-authored-by: Cursor <cursoragent@cursor.com>
Re-bin
approved these changes
May 2, 2026
Collaborator
Re-bin
left a comment
There was a problem hiding this comment.
This is the right kind of fix.
The PR tightens the ExecTool policy boundary in the intended way: configured deny patterns are appended to the built-in deny list instead of replacing it, allow patterns can explicitly exempt narrow commands from the deny-pattern filter, and deny/allowlist blocks are no longer misclassified as fatal workspace violations. I also checked that this does not bypass the harder safety boundaries: SSRF/internal URL and workspace path guards still run after the allow-pattern decision.
I pushed one tiny cleanup commit (d137cc44) to remove an unused import from the new test file.
Local validation:
uv run pytest tests/tools/test_exec_allow_patterns.py tests/tools/test_exec_security.py tests/agent/test_runner.py -q-> 110 passeduv run ruff check nanobot/agent/tools/shell.py nanobot/config/schema.py nanobot/agent/runner.py nanobot/agent/loop.py nanobot/agent/subagent.py tests/tools/test_exec_allow_patterns.py tests/tools/test_exec_security.py && git diff --check-> passeduv run pytest -q-> 2616 passed, 2 skipped
From my side, this is clean and ready to merge once CI is green.
bibistellar
pushed a commit
to bibistellar/nanobot
that referenced
this pull request
May 3, 2026
上游新提交: - refactor(setup): enhance SKILL.md for upgrade process clarity - fix: improve media failure diagnostics and token fallback coverage - fix: allow_patterns take priority over deny_patterns in ExecTool (HKUDS#3594) 冲突解决: - shell.py: 保留我们的 rm -rf 移除,采用上游新增的 deny patterns Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.
Problem
allow_patterns cannot override deny_patterns:
ExecTool._guard_command()checks deny_patterns before allow_patterns. When a command matches a deny pattern, it is immediately blocked with no way to exempt it via allow_patterns. The built-in deny list can never be whitelisted.user-supplied deny_patterns replace built-in list: config
deny_patternsusesor [list]which accidentally disables default safety patterns.deny pattern blocks silently abort turns: deny pattern error message
"blocked by safety guard"is included in_WORKSPACE_BLOCK_MARKERS, causing deny_pattern blocks to be misclassified as fatal workspace violations. LLMs have no chance to retry — the turn is aborted immediately, and ifMessageTool._sent_in_turnis True, the error message is silently swallowed (returnsNone) with no notification sent to the user.Changes
nanobot/agent/tools/shell.pynanobot/agent/config/schema.pyallow_patternsanddeny_patternsfields toExecToolConfignanobot/agent/runner.py"blocked by safety guard"from_WORKSPACE_BLOCK_MARKERSso deny_pattern errors are treated as normal tool errors (LLM can retry) instead of fatal violationsnanobot/agent/loop.pynanobot/agent/subagent.pytests/tools/test_exec_allow_patterns.pyConfig Example
{ "exec": { "allowPatterns": ["rm\\s+-rf\\s+/tmp/"], "denyPatterns": ["\\bping\\b"] } }This allows destructive commands under
/tmp/while still blocking other usage.