fix: display assistant content with finish tool call + block shell write redirects

Bug 1: Assistant text content was silently dropped when the LLM response
included both content and tool calls (e.g. finish with a summary). Now
content is displayed before tool call execution regardless.

Bug 2: Shell redirect operators (>, >>, <<) allowed bypassing file-write
permissions when the base command (e.g. cat) was in the allowed list.
Redirects now require explicit user approval in permissions, and the
shell tool itself blocks them as defense-in-depth.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 20:58:07 -05:00
parent 4496fce354
commit 2ad3df521d
3 changed files with 29 additions and 2 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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)