Conversation
…ommand validation Fix two bugs: 1. LLM may pass timeout parameter as a string instead of integer, causing TypeError in subprocess.communicate() (bash toolset) and in timeout comparison (prometheus toolset). Cast to int with fallback. 2. When bashlex cannot parse a command (e.g. unquoted parentheses in PromQL queries), the fallback always returns APPROVAL_REQUIRED even if all command segments are in the allow list. In server mode there is no user to approve, so these commands silently fail. Add a shlex-based fallback that tokenizes the command respecting shell quoting, splits by operators (|, &&, ;, &), and validates each segment against the allow/deny lists. Fixes HolmesGPT#1727, fixes HolmesGPT#1728, related to HolmesGPT#1677 Signed-off-by: Andrei Aleksandrov <aladex@gmail.com>
|
|
WalkthroughAdds defensive timeout parameter handling in bash and Prometheus toolsets to gracefully convert string timeouts from LLMs to integers, and introduces a fallback command validation path using shlex parsing for cases where bashlex fails on unquoted parentheses in command arguments. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
✅ Deploy Preview for holmes-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/test_bash_toolset_validation.py (1)
1070-1080: Consider adding deny_reason assertion for consistency.Other similar denial tests in this file (e.g.,
test_subshell_with_deny_listed_command_still_deniedat lines 1001-1002) assert bothresult.statusandresult.deny_reason. Adding the assertion here would improve consistency and provide better failure diagnostics.🔧 Proposed addition
assert result.status == ValidationStatus.DENIED + assert result.deny_reason == DenyReason.DENY_LIST🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/test_bash_toolset_validation.py` around lines 1070 - 1080, The test test_curl_with_parentheses_denied_command only asserts result.status; update it to also assert the denial reason for consistency by adding an assertion on result.deny_reason (e.g., assert result.deny_reason and "rm" in result.deny_reason) so the test verifies a non-empty deny_reason and that it references the denied command; reference symbols: test_curl_with_parentheses_denied_command, result, result.deny_reason, and ValidationStatus.DENIED.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@tests/test_bash_toolset_validation.py`:
- Around line 1070-1080: The test test_curl_with_parentheses_denied_command only
asserts result.status; update it to also assert the denial reason for
consistency by adding an assertion on result.deny_reason (e.g., assert
result.deny_reason and "rm" in result.deny_reason) so the test verifies a
non-empty deny_reason and that it references the denied command; reference
symbols: test_curl_with_parentheses_denied_command, result, result.deny_reason,
and ValidationStatus.DENIED.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 8bfdb79e-52a7-4231-8c95-e5b1a5bc667a
📒 Files selected for processing (4)
holmes/plugins/toolsets/bash/bash_toolset.pyholmes/plugins/toolsets/bash/validation.pyholmes/plugins/toolsets/prometheus/prometheus.pytests/test_bash_toolset_validation.py
|
@aantn thanks for the pointer! Timeout coercion — I'll rebase on top of #1790 and drop the per-tool timeout casts from this PR. The universal coercion approach is cleaner. bashlex fallback — the bash command itself is valid. The typical case is something like: The parentheses are inside single quotes, so bash handles them fine — but The shlex fallback only kicks in when bashlex fails, tokenizes by shell operators, and validates each segment against the same allow/deny lists. If any segment isn't explicitly allowed, it still returns |
|
@Aladex are you sure about that? By me bashlex handles it just fine: In the original issue you opened, the example command was unquoted, so I think the LLM was just generating an invalid command. In any event, can we fix by giving you the option to auto-disapprove all commands on the server flow and continue the investigation? |
|
@aantn you're right — I double-checked and bashlex does handle the quoted version fine, so the fallback wouldn't actually trigger in that case. And the unquoted version is indeed invalid bash. I need to dig into our production logs a bit more to catch the exact commands the LLM is generating and understand where it's actually breaking. I'll follow up once I have concrete examples. The auto-disapprove + continue idea for server mode sounds good regardless — happy to help with that if needed. |
|
@aantn you're right on both counts. Confirmed on production — the LLM does quote the arguments properly, so bashlex handles it fine. The shlex fallback isn't needed. The timeout issue is also confirmed to be a real problem on 0.20.0, but #1790 addresses it with the universal coercion approach, which is the right fix. Closing this PR in favor of #1790. |
|
Awesome, thank you. Adding docs on how to disable approval in server mode - turns out we support it but its not documented! Let me know if that helps. |
Related Issues
Changes
1. Timeout type coercion (bash + prometheus toolsets)
LLMs sometimes pass the
timeouttool parameter as a string ("30") instead of an integer. This causes:TypeError: unsupported operand type(s) for +: 'float' and 'str'insubprocess.communicate(timeout=timeout)TypeError: '>' not supported between instances of 'str' and 'int'in timeout comparisonFix: cast
timeouttointwith a try/except fallback to the default value.2. shlex-based fallback for unparseable commands (bash toolset)
When
bashlex.parse()fails (e.g. unquoted parentheses in PromQL queries likequery=topk(10, metric)), the current code always returnsAPPROVAL_REQUIRED. In server mode (e.g. Keep/Robusta integrations), there is no user to approve, so these commands silently fail.Added a fallback that uses
shlex.split()(stdlib, respects shell quoting) to tokenize the command, then groups tokens into segments by shell operators (|,&&,;,&) and validates each segment against the allow/deny lists. If all segments are allowed, the command is permitted.Security is preserved:
validate_segment()deny → allow → approval pipelineAPPROVAL_REQUIRED)shlex.split()also fails, falls through toAPPROVAL_REQUIREDTests
Added 3 test cases to
test_bash_toolset_validation.py:curlwith parentheses in args piped tojq(both allowed) →ALLOWEDcurlwith parentheses piped to non-allowed command →APPROVAL_REQUIREDcurlwith parentheses piped to denied command →DENIEDSummary by CodeRabbit
Bug Fixes
New Features
Tests