33from __future__ import annotations
44
55import logging
6+ import re
67
78from frontier_agent .core .tool import tool
9+ from plugins .tools import _sandbox
810from plugins .tools ._deliverable_policy import output_write_error
911from plugins .tools ._path_auth import _authorized_local_path
1012from plugins .tools ._sandbox import (
1113 aget_sandbox ,
1214 arun_sandbox_cmd ,
1315 asandbox_write_file ,
16+ shell_quote ,
1417)
1518
1619logger = logging .getLogger (__name__ )
@@ -39,6 +42,16 @@ async def file_editor_view(path: str, view_range: str = "") -> str:
3942 if not path or not path .strip ():
4043 return "Error: path is required."
4144
45+ line_range : tuple [int , int ] | None = None
46+ if view_range :
47+ match = re .fullmatch (r"\s*([0-9]+)\s*-\s*([0-9]+)\s*" , view_range )
48+ if match is None :
49+ return "Error: view_range must be a positive line range like 1-50."
50+ start , end = int (match [1 ]), int (match [2 ])
51+ if start < 1 or end < start :
52+ return "Error: view_range must be a positive line range like 1-50."
53+ line_range = start , end
54+
4255 local_path , _reason = _authorized_local_path (path )
4356 if local_path is not None :
4457 from plugins .tools .ignore_rules import discover_repo_root , should_ignore_path
@@ -64,12 +77,7 @@ async def file_editor_view(path: str, view_range: str = "") -> str:
6477 lines = local_path .read_text (encoding = "utf-8" ).splitlines ()
6578 except Exception as e :
6679 return f"Error viewing { path } : { e } "
67- if view_range and "-" in view_range :
68- start_s , end_s = view_range .split ("-" , 1 )
69- start = max (int (start_s .strip () or "1" ), 1 )
70- end = max (int (end_s .strip () or str (start )), start )
71- else :
72- start , end = 1 , len (lines )
80+ start , end = line_range if line_range is not None else (1 , len (lines ))
7381 rendered = "\n " .join (f"{ idx } : { line } " for idx , line in enumerate (lines [start - 1 :end ], start = start ))
7482 return _truncate (rendered or "(empty file)" )
7583
@@ -78,33 +86,33 @@ async def file_editor_view(path: str, view_range: str = "") -> str:
7886 except RuntimeError as e :
7987 return f"Error: { e } "
8088
89+ # Every path below is interpolated into a shell command, so it is quoted
90+ # once here: an unquoted path containing ``;`` or ``$(…)`` would run
91+ # arbitrary commands in the sandbox without passing the bash policy.
92+ q = shell_quote (path )
8193 try :
8294 check = await arun_sandbox_cmd (
8395 sandbox ,
84- f"test -d { path } && echo DIR || echo FILE" , timeout = 10 ,
96+ f"test -d { q } && echo DIR || echo FILE" , timeout = 10 ,
8597 )
8698 is_dir = "DIR" in check .stdout
8799
88100 if is_dir :
89101 result = await arun_sandbox_cmd (
90102 sandbox ,
91- f"find { path } -maxdepth 2 -not -path '*/.git/*' "
103+ f"find { q } -maxdepth 2 -not -path '*/.git/*' "
92104 f"-not -path '*/node_modules/*' -not -path '*/__pycache__/*' "
93105 f"| head -100 | sort" ,
94106 timeout = 15 ,
95107 )
96108 return _truncate (result .stdout or f"(empty directory: { path } )" )
97109
98110 # File view
99- if view_range :
100- parts = view_range .split ("-" )
101- if len (parts ) == 2 :
102- start , end = parts [0 ].strip (), parts [1 ].strip ()
103- cmd = f"sed -n '{ start } ,{ end } p' { path } | cat -n"
104- else :
105- cmd = f"cat -n { path } "
111+ if line_range is not None :
112+ start , end = line_range
113+ cmd = f"sed -n { shell_quote (f'{ start } ,{ end } p' )} -- { q } | cat -n"
106114 else :
107- cmd = f"cat -n { path } "
115+ cmd = f"cat -n -- { q } "
108116
109117 result = await arun_sandbox_cmd (sandbox , cmd , timeout = 15 )
110118
@@ -154,9 +162,9 @@ async def file_editor_create(path: str, content: str) -> str:
154162 try :
155163 import os
156164 parent = os .path .dirname (path )
157- if parent :
165+ if parent and not isinstance ( sandbox , _sandbox . CurrentSandbox ) :
158166 await arun_sandbox_cmd (
159- sandbox , f"mkdir -p { parent } " , timeout = 10 ,
167+ sandbox , f"mkdir -p { shell_quote ( parent ) } " , timeout = 10 ,
160168 )
161169
162170 # Write via python in sandbox (avoids files.write permission issues)
@@ -165,7 +173,7 @@ async def file_editor_create(path: str, content: str) -> str:
165173 return f"Error creating { path } : { err } "
166174
167175 result = await arun_sandbox_cmd (
168- sandbox , f"wc -l < { path } " , timeout = 5 ,
176+ sandbox , f"wc -l < { shell_quote ( path ) } " , timeout = 5 ,
169177 )
170178 lines = result .stdout .strip () if result .stdout else "?"
171179
@@ -232,7 +240,9 @@ async def file_editor_str_replace(path: str, old_str: str, new_str: str) -> str:
232240
233241 try :
234242 # Read current content via cat (more reliable than files.read for all paths)
235- result = await arun_sandbox_cmd (sandbox , f"cat { path } " , timeout = 10 )
243+ result = await arun_sandbox_cmd (
244+ sandbox , f"cat -- { shell_quote (path )} " , timeout = 10 ,
245+ )
236246 if result .exit_code != 0 :
237247 return f"Error: Cannot read { path } : { result .stderr or 'file not found' } "
238248 content = result .stdout
0 commit comments