Merge branch 'fix/display-bug-and-shell-write-bypass'
This commit is contained in:
@@ -216,10 +216,12 @@ class AgentLoop:
|
||||
# Successful response — reset streak
|
||||
reasoning_only_streak = 0
|
||||
|
||||
# Display any assistant text content (even if tool calls follow)
|
||||
if self._display and assistant_msg.content:
|
||||
self._display.write_assistant_message(assistant_msg.content)
|
||||
|
||||
# No tool calls → task complete (plain text response)
|
||||
if not assistant_msg.tool_calls:
|
||||
if self._display and assistant_msg.content:
|
||||
self._display.write_assistant_message(assistant_msg.content)
|
||||
break
|
||||
|
||||
# Execute tool calls
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import shlex
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
@@ -14,6 +15,9 @@ logger = logging.getLogger(__name__)
|
||||
# Type alias for the async prompt callback
|
||||
PromptCallback = Callable[[str, str], Awaitable[bool]]
|
||||
|
||||
# Detect shell redirects that write to files (>, >>, heredocs)
|
||||
_WRITE_REDIRECT_PATTERN = re.compile(r"(?:>\s*\S|>>|<<)")
|
||||
|
||||
|
||||
class PermissionDenied(Exception):
|
||||
"""Raised when a tool is denied execution by permissions policy."""
|
||||
@@ -104,6 +108,11 @@ class PermissionsService:
|
||||
logger.info("Shell command '%s' matches denied prefix '%s'", cmd, denied)
|
||||
return False
|
||||
|
||||
# Detect shell redirects that write to files — require approval
|
||||
if _WRITE_REDIRECT_PATTERN.search(cmd):
|
||||
logger.info("Shell command '%s' contains file-write redirect — requiring approval", cmd)
|
||||
return None # fall through to user prompt
|
||||
|
||||
# Allowed commands: base executable match
|
||||
if shell_config.allowed_commands:
|
||||
if base_cmd in shell_config.allowed_commands:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Shell tool: run_command."""
|
||||
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
from typing import Any
|
||||
@@ -11,6 +12,9 @@ from app.tools.base import BaseTool
|
||||
|
||||
_DEFAULT_TIMEOUT = 30
|
||||
|
||||
# Detect shell redirects that write to files (>, >>, heredocs)
|
||||
_WRITE_REDIRECT_PATTERN = re.compile(r"(?:>\s*\S|>>|<<)")
|
||||
|
||||
|
||||
class RunCommandParams(BaseModel):
|
||||
"""Parameters for the run_command tool."""
|
||||
@@ -43,6 +47,18 @@ class RunCommandTool(BaseTool):
|
||||
error=f"Command denied: matches blocked prefix '{denied}'",
|
||||
)
|
||||
|
||||
# Defense-in-depth: flag file-write redirects in tool result
|
||||
if _WRITE_REDIRECT_PATTERN.search(command):
|
||||
return ToolResult(
|
||||
tool_call_id=tool_call_id,
|
||||
tool_name=self.name,
|
||||
status=ToolResultStatus.ERROR,
|
||||
error=(
|
||||
f"Command contains file-write redirect (>, >>, or <<) "
|
||||
f"which bypasses file-write permissions. Use write_file instead."
|
||||
),
|
||||
)
|
||||
|
||||
# Allow check: first token must be in allowed_commands
|
||||
try:
|
||||
tokens = shlex.split(command)
|
||||
|
||||
Reference in New Issue
Block a user