feat: add shell tool#1107
Conversation
Tool PR ChecklistUse this checklist when adding or modifying tools in Protocol Compliance
Integration
|
planetf1
left a comment
There was a problem hiding this comment.
Overall the design is solid — subprocess.run(shell=False) as the foundation, shlex.split() for tokenisation, and working-dir restriction are the right primitives. Two confirmed bugs block merge; a third issue means the _bash_patterns.py test suite gives false assurance about what's actually blocked at runtime.
| if cmd not in SAFE_WRAPPER_COMMANDS or arg_cmd not in ( | ||
| "bash", | ||
| "sh", | ||
| "zsh", |
There was a problem hiding this comment.
env bash -c <payload> bypasses the denylist
The code-execution check (lines 176–182) only fires when argv[0] is the interpreter. When argv[0] is env, that path is never reached — and lines 225–231 explicitly allow shells as nested arguments inside safe wrappers, so -c is never re-checked.
_is_dangerous_command(["env", "bash", "-c", "id"]) # (False, '')
_is_dangerous_command(["timeout", "60", "bash", "-c", "id"]) # (False, '')Fix: re-check for -c/-e on the nested interpreter, or restrict the shell allowlist here to script-file invocations only (no -c/-e in the remaining argv).
There was a problem hiding this comment.
additional check is added. The tests are added.
There was a problem hiding this comment.
Verified fixed — env/timeout/nice bash -c bypass is now blocked correctly. LGTM.
| with self._storage_lock: | ||
| results = self._violations[:] | ||
|
|
||
| for violation in results: |
There was a problem hiding this comment.
Classic iterate-and-remove bug — filter results will be incomplete
results.remove() inside for violation in results causes Python's iterator to skip the element immediately after each removal. Filters will silently return the wrong set.
if session_id:
results = [v for v in results if v.session_id == session_id]
if pattern:
results = [v for v in results if v.pattern == pattern]
if category:
results = [v for v in results if v.category == category]
if severity:
results = [v for v in results if v.severity == severity]| Returns: | ||
| A tuple of (has_dangerous_paths, reason_message). | ||
| """ | ||
| write_commands = {"rm", "touch", "cp", "mv", "mkdir", "mkfifo", "mknod", "tee"} |
There was a problem hiding this comment.
write_commands is missing several filesystem-write commands
chmod, chown, ln, and dd aren't in this set, so the dangerous-path checks never fire for them. A few things that currently slip through:
chmod 777 /etc/passwdln -sf /etc/passwd /allowed/path/link(symlink escape out of the allowed fence)dd if=/dev/urandom of=/boot/vmlinuz
Same set is duplicated on line 415 — both need updating.
There was a problem hiding this comment.
Additional write commands are added. Add "ln" command handling.
There was a problem hiding this comment.
Verified fixed — WRITE_COMMANDS now covers chmod, chown, chgrp, ln, dd, install, truncate. All four cases blocked. LGTM.
| ] | ||
|
|
||
|
|
||
| def check_all_patterns( |
There was a problem hiding this comment.
This framework isn't connected to the execution path
shell.py only imports record_bash_violation from _bash_audit — check_all_patterns() is never called from bash_executor(). The tests in test_bash_guardrails.py all pass, but none of it affects whether a command is actually blocked at runtime.
If this is intended as the extensible pattern layer, wire check_all_patterns(argv) into _is_dangerous_command(). If it's documentation of the threat model, the disconnect from the execution path should be made explicit.
There was a problem hiding this comment.
wire check_all_patterns is wired into the execution path by updating _is_dangerous_command to use the pattern framework.
There was a problem hiding this comment.
Verified fixed — check_all_patterns is now imported and called at the top of _is_dangerous_command. LGTM.
|
|
||
|
|
||
| def example_3_with_working_dir() -> None: | ||
| """Example 3: Restrict write validation and execution cwd to a directory.""" |
There was a problem hiding this comment.
Two functions are both labelled "Example 3" — example_3_llm_with_forced_tool_use (line 85) already has that number. This one should be Example 4, with the subsequent examples renumbered.
|
After investigate, it may not be good to share the @DataClass Benefits:
|
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
planetf1
left a comment
There was a problem hiding this comment.
Second pass. Clarification replies added to the A, C, and D threads — none of those claimed fixes are reflected in the current code. Inline comments below cover the two unaddressed findings without author replies (B and E) and one new issue surfaced by this review.
| results = [ | ||
| v | ||
| for v in results | ||
| if (session_id is None or v.session_id == session_id) | ||
| and (pattern is None or v.pattern == pattern) | ||
| and (category is None or v.category == category) | ||
| and (severity is None or v.severity == severity) | ||
| ] | ||
|
|
There was a problem hiding this comment.
Verified fixed — filter is now a list comprehension, returns correct counts. LGTM.
| print(f" Error: {exec_result.stderr}") | ||
| print() | ||
|
|
||
|
|
There was a problem hiding this comment.
Verified fixed — renamed to example_4_with_working_dir with correct numbering; __main__ calls the LLM example. LGTM.
There was a problem hiding this comment.
Would you check this comment? There may be some confusion.
| is_dangerous, reason = pattern.check(argv) | ||
| if is_dangerous: | ||
| pattern_name = type(pattern).__name__ | ||
| category = getattr(pattern, "category", "unknown") |
There was a problem hiding this comment.
New finding: even if check_all_patterns were wired into the execution path, every violation would still be logged with category='unknown' and severity='MEDIUM'. The function reads these via getattr(pattern, 'category', 'unknown') and getattr(pattern, 'severity', 'MEDIUM'), but no concrete pattern class defines category or severity attributes — those live in _bash_guardrails.COMMAND_RULES, not on the pattern objects themselves.
To fix this, either add category and severity class attributes to each concrete pattern (populated from COMMAND_RULES), or return them as part of a richer result object from check() instead of a plain tuple[bool, str].
There was a problem hiding this comment.
Verified fixed in the latest commit — each pattern class now declares category and severity as class attributes, so the getattr defaults never fire. Violations are logged with correct metadata. LGTM.
There was a problem hiding this comment.
Correction to my previous reply — on closer inspection this is only partially fixed. The class-level attributes prevent the getattr defaults, but they apply a blanket value per pattern class rather than per command. For example DangerousCommandPattern hardcodes severity='HIGH', so a sudo attempt (which COMMAND_RULES defines as CRITICAL/PRIVILEGE_ESCALATION) gets logged as HIGH/dangerous_command. The granular data is already in COMMAND_RULES — the fix is to look up the matched command there rather than using a class-level constant.
There was a problem hiding this comment.
Thanks. I made changes.
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Pull Request
Issue
Fix #1024
Description
This PR doesn't include issues mentioned in #1087
Testing
Attribution
Adding a new component, requirement, sampling strategy, or tool?
If your PR adds or modifies one of the types below, check the matching box. A checklist of type-specific review items will be posted as a comment.
NOTE: Please ensure you have an issue that has been acknowledged by a core contributor and routed you to open a pull request against this repository. Otherwise, please open an issue before continuing with this pull request.