Repository navigation
Redesigned bash toolset that have a prefix based approvals - #1360
Conversation
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
…tive_lists() Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Introduce operation-type-aware prefix rules that guide the LLM
to suggest appropriate prefixes based on whether commands are
read-only, mixed (can branch to read or write), or destructive.
Key changes:
- Read-only: use shortest committed prefix (e.g., 'kubectl get')
- Mixed operations: use longer prefix specifying read-only branch
(e.g., 'kubectl config view' not 'kubectl config')
- Destructive: include full target identification excluding values
(e.g., 'kubectl delete pod my-pod -n default')
Also adds command structure guidance to place parameter values at
the end so prefixes can match effectively.
Includes three new eval tests (192-194) to verify prefix behavior
for each operation type.
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
… holmes pod rbac in cluster Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Avi-Robusta
left a comment
There was a problem hiding this comment.
some renaming and refactoring comments
there is a comment about using an existing file for the commands instead of creating a new one, see if there is a quick workaround, if not than ignore it
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@holmes/core/tool_calling_llm.py`:
- Around line 801-814: The _is_tool_call_already_approved function builds a
ToolInvokeContext but omits session_approved_prefixes, so approval re-checks
ignore previously approved prefixes; update _is_tool_call_already_approved to
accept a session_approved_prefixes argument and pass it into the
ToolInvokeContext constructor (ToolInvokeContext(...,
session_approved_prefixes=session_approved_prefixes)), then update every call
site that invokes _is_tool_call_already_approved (the place that re-checks
approvals after tool execution) to forward the current session_approved_prefixes
so tool.requires_approval sees session-level approvals.
♻️ Duplicate comments (5)
holmes/plugins/toolsets/bash/bash_toolset.py (4)
133-150: Add return type annotation for_validate_command.The method is missing a return type annotation. Based on the implementation, it returns
ValidationResultfromvalidate_command().♻️ Proposed fix
First, update the import:
from holmes.plugins.toolsets.bash.validation import ( DenyReason, + ValidationResult, ValidationStatus, get_effective_lists, validate_command, )Then update the signature:
def _validate_command( - self, command_str: str, suggested_prefixes: list, context: ToolInvokeContext - ): + self, + command_str: str, + suggested_prefixes: list[str], + context: ToolInvokeContext, + ) -> ValidationResult:
265-283: Add type annotation forvalidation_resultparameter.The
validation_resultparameter lacks a type annotation. AddValidationResulttype (requires importing it from the validation module).♻️ Proposed fix
- def _build_deny_error_message(self, validation_result) -> str: + def _build_deny_error_message(self, validation_result: ValidationResult) -> str:
35-81: Include stderr in error results for LLM self-correction.The
bash_result_to_structuredfunction omitsstderrfrom error responses. Per coding guidelines, toolsets must return detailed error messages including full API/command error output. Including stderr helps the LLM understand what went wrong and self-correct.🔧 Proposed fix
def bash_result_to_structured( result: BashResult, cmd: str, timeout: int, params: dict ) -> StructuredToolResult: + stderr = result.stderr if hasattr(result, 'stderr') else None + if result.timed_out: + data_parts = [cmd, result.stdout, stderr] + data = "\n".join(p for p in data_parts if p) return StructuredToolResult( status=StructuredToolResultStatus.ERROR, - error=f"Error: Command '{cmd}' timed out after {timeout} seconds.", - data=f"{cmd}\n{result.stdout}" if result.stdout else None, + error=f"Error: Command '{cmd}' timed out after {timeout} seconds." + + (f" stderr: {stderr}" if stderr else ""), + data=data or None, params=params, invocation=cmd, ) - result_data = f"{cmd}\n{result.stdout}" + data_parts = [cmd, result.stdout, stderr] + result_data = "\n".join(p for p in data_parts if p) # ... in the else branch for non-zero return code: - error = ( - f'Error: Command "{cmd}" returned non-zero exit status {result.return_code}' - ) + error = ( + f'Error: Command "{cmd}" returned non-zero exit status {result.return_code}' + + (f". stderr: {stderr}" if stderr else "") + )As per coding guidelines, toolsets must return detailed error messages from underlying APIs.
162-170: Guard against invalidsuggested_prefixestype inrequires_approval.
requires_approval()calls_validate_command()before_invoke()performs type validation. Ifsuggested_prefixesis not a list or contains non-strings, validation may throw or mis-evaluate. Add a defensive check to let_invoke()handle type errors.✅ Proposed defensive check
command_str = params.get("command", "") suggested_prefixes = params.get("suggested_prefixes", []) if not command_str or not suggested_prefixes: return None # Let _invoke() handle validation errors + + # Type check - let _invoke() handle type errors with proper error messages + if not isinstance(suggested_prefixes, list) or not all( + isinstance(p, str) for p in suggested_prefixes + ): + return Noneholmes/core/tool_calling_llm.py (1)
124-135: Regex pattern fails to capture JSON with nested structures.The pattern
[^}]+stops at the first}, which breaks when the JSON contains arrays or nested objects. For example,{"bash_session_approved_prefixes": ["kubectl", "helm"]}would be truncated at the first], causing JSON parse failure.🐛 Proposed fix using balanced brace matching
- # Extract tool_call_metadata from the content string - # Format: tool_call_metadata={"tool_name": "...", ...} - match = re.search(r"tool_call_metadata=(\{[^}]+\})", content) - if not match: - continue - - try: - metadata = json.loads(match.group(1)) + # Extract tool_call_metadata from the content string + # Format: tool_call_metadata={"tool_name": "...", ...} + marker = "tool_call_metadata=" + start_idx = content.find(marker) + if start_idx == -1: + continue + + json_start = start_idx + len(marker) + if json_start >= len(content) or content[json_start] != '{': + continue + + # Find matching closing brace using balanced counting + brace_count = 0 + json_end = json_start + for i, char in enumerate(content[json_start:], json_start): + if char == '{': + brace_count += 1 + elif char == '}': + brace_count -= 1 + if brace_count == 0: + json_end = i + 1 + break + else: + continue # No matching brace found + + try: + metadata = json.loads(content[json_start:json_end])
🧹 Nitpick comments (5)
holmes/plugins/toolsets/bash/common/cli_prefixes.py (3)
52-59: Narrow exception handling for YAML loading.The
except Exceptioncatches all exceptions including unexpected ones likeKeyboardInterrupt. Consider narrowing to the specific exceptions that can occur here:OSErrorfor file operations andyaml.YAMLErrorfor parsing errors.♻️ Proposed fix
- except Exception as e: + except (OSError, yaml.YAMLError) as e: logging.warning(f"Failed to load approved prefixes: {e}")
74-81: Silent exception swallowing hides failures.The
try-except-passblock silently ignores read errors when loading existing prefixes during save. This could mask issues like permission problems or corrupted YAML. Consider logging a warning so operators can diagnose persistence issues.♻️ Proposed fix
- except Exception: - pass + except (OSError, yaml.YAMLError) as e: + logging.debug(f"Could not load existing prefixes for merge: {e}")
85-89: Uselogging.exceptionto preserve traceback.When logging errors,
logging.exceptionautomatically includes the traceback, which is helpful for debugging write failures. Also consider narrowing the exception type.♻️ Proposed fix
- except Exception as e: - logging.error(f"Failed to save approved prefixes: {e}") + except (OSError, yaml.YAMLError) as e: + logging.exception(f"Failed to save approved prefixes: {e}")holmes/interactive.py (2)
626-636: Remove unusedconsoleparameter.The
consoleparameter is declared but never used in_run_inline_menu. Either remove it or use it (e.g., for fallback output if the Application fails).♻️ Proposed fix
-def _run_inline_menu(options: list[str], console: Console) -> Optional[int]: +def _run_inline_menu(options: list[str]) -> Optional[int]:And update the call site at line 746:
- result = _run_inline_menu(options, console) + result = _run_inline_menu(options)
43-48: Consider consolidating imports from the same module.The two separate import statements from
cli_prefixescan be combined for clarity.♻️ Proposed fix
-from holmes.plugins.toolsets.bash.common.cli_prefixes import ( - enable_cli_mode, -) -from holmes.plugins.toolsets.bash.common.cli_prefixes import ( - save_cli_bash_tools_approved_prefixes as _save_approved_prefixes, -) +from holmes.plugins.toolsets.bash.common.cli_prefixes import ( + enable_cli_mode, + save_cli_bash_tools_approved_prefixes as _save_approved_prefixes, +)
Signed-off-by: Tomer Keshet <tomer@robusta.dev>
Summary by CodeRabbit
Release Notes
New Features
--bash-always-denyand--bash-always-allowCLI flags for controlling bash command approval behaviorImprovements
✏️ Tip: You can customize this high-level summary in your review settings.