fix(file_tools): strip ANSI escape codes from write_file and patch content (#2532)
Models occasionally copy ANSI escape sequences from terminal output or display formatting into file content, breaking shebangs and injecting binary characters into scripts. Strip ANSI codes (CSI, OSC, simple escapes) from: - write_file content - patch old_string, new_string, and V4A patch content The check is fast (skips entirely if no ESC byte present). Reported by Andi Jaeger.
This commit is contained in:
parent
cd2280d1a3
commit
fa6f069577
1 changed files with 17 additions and 0 deletions
|
|
@ -5,6 +5,7 @@ import errno
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
from typing import Optional
|
||||
from tools.file_operations import ShellFileOperations
|
||||
|
|
@ -12,6 +13,18 @@ from agent.redact import redact_sensitive_text
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Regex to match ANSI escape sequences (CSI codes, OSC codes, simple escapes).
|
||||
# Models occasionally copy these from terminal output into file content.
|
||||
_ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]|\x1b\][^\x07]*\x07|\x1b[()][A-B012]|\x1b[=>]")
|
||||
|
||||
|
||||
def _strip_ansi(text: str) -> str:
|
||||
"""Remove ANSI escape sequences from text destined for file writes."""
|
||||
if not text or "\x1b" not in text:
|
||||
return text
|
||||
return _ANSI_ESCAPE_RE.sub("", text)
|
||||
|
||||
|
||||
_EXPECTED_WRITE_ERRNOS = {errno.EACCES, errno.EPERM, errno.EROFS}
|
||||
|
||||
|
||||
|
|
@ -288,6 +301,7 @@ def notify_other_tool_call(task_id: str = "default"):
|
|||
def write_file_tool(path: str, content: str, task_id: str = "default") -> str:
|
||||
"""Write content to a file."""
|
||||
try:
|
||||
content = _strip_ansi(content)
|
||||
file_ops = _get_file_ops(task_id)
|
||||
result = file_ops.write_file(path, content)
|
||||
return json.dumps(result.to_dict(), ensure_ascii=False)
|
||||
|
|
@ -311,10 +325,13 @@ def patch_tool(mode: str = "replace", path: str = None, old_string: str = None,
|
|||
return json.dumps({"error": "path required"})
|
||||
if old_string is None or new_string is None:
|
||||
return json.dumps({"error": "old_string and new_string required"})
|
||||
old_string = _strip_ansi(old_string)
|
||||
new_string = _strip_ansi(new_string)
|
||||
result = file_ops.patch_replace(path, old_string, new_string, replace_all)
|
||||
elif mode == "patch":
|
||||
if not patch:
|
||||
return json.dumps({"error": "patch content required"})
|
||||
patch = _strip_ansi(patch)
|
||||
result = file_ops.patch_v4a(patch)
|
||||
else:
|
||||
return json.dumps({"error": f"Unknown mode: {mode}"})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue