fix(tools): a sandbox that cannot run commands is 'environment unavailable', not 'File not found' (#44750)
_probe_regular_file returned 'missing' for ANY non-zero exit, so a docker container that was still starting, paused, or otherwise unable to exec made read_file / read_file_raw / read_file_bytes report a false 'File not found' — which the model then trusted for the rest of the session. The probe now echoes a sentinel for a genuinely missing path and a non-zero exit without either sentinel is reported as an environment failure with a retry hint; search_files does the same when its existence probe returns neither marker. Salvaged from #44753 by @dredozubov.
This commit is contained in:
@@ -441,20 +441,29 @@ class ShellFileOperations(LintMixin, SearchMixin, FileOperations):
|
||||
name-based blocklist can't cover a FIFO (a file TYPE at any path); ``[ -f ]``
|
||||
is a stat (symlinks followed) so it answers without touching content."""
|
||||
arg = self._escape_shell_arg(path)
|
||||
# A missing path ECHOES its sentinel: a non-zero exit with no sentinel means the shell itself did
|
||||
# not run (container still starting, removed out-of-band, transport down) — not a missing file.
|
||||
# Reporting that as "File not found" made the model trust a false negative for the whole session.
|
||||
stat_result = self._exec(
|
||||
f"if [ -f {arg} ]; then wc -c < {arg} 2>/dev/null; "
|
||||
f"elif [ -e {arg} ]; then echo {NOT_REGULAR_SENTINEL}; "
|
||||
f"else exit 1; fi")
|
||||
if stat_result.exit_code != 0:
|
||||
return 0, "missing"
|
||||
f"else echo {MISSING_SENTINEL}; fi")
|
||||
stat_output = _strip_terminal_fence_leaks(stat_result.stdout).strip()
|
||||
if stat_output == MISSING_SENTINEL:
|
||||
return 0, "missing"
|
||||
if stat_output == NOT_REGULAR_SENTINEL:
|
||||
return 0, "not_regular"
|
||||
if stat_result.exit_code != 0:
|
||||
return 0, "env_unavailable"
|
||||
try:
|
||||
return int(stat_output), "ok"
|
||||
except ValueError:
|
||||
return 0, "bad_size"
|
||||
|
||||
def _env_unavailable_error(self, path: str) -> ReadResult:
|
||||
return ReadResult(error=(f"Terminal environment unavailable: could not stat {path} "
|
||||
"(the sandbox may still be starting or was removed). Retry shortly."))
|
||||
|
||||
def _detect_binary(self, path: str) -> tuple[bool, Optional[bytes]]:
|
||||
"""``(is_binary, sample_bytes)`` — byte-layer detection when the transport
|
||||
allows (base64 sample), else the legacy text heuristic (sample is None)."""
|
||||
@@ -812,6 +821,8 @@ class ShellFileOperations(LintMixin, SearchMixin, FileOperations):
|
||||
return self._read_file_missing(path, offset, limit)
|
||||
if status == "not_regular":
|
||||
return self._not_regular_error(path)
|
||||
if status == "env_unavailable":
|
||||
return self._env_unavailable_error(path)
|
||||
if self._is_image(path): # never inlined — redirect to the vision tool
|
||||
return self._image_redirect_result(file_size)
|
||||
is_binary, sample_bytes = self._detect_binary(path)
|
||||
@@ -964,6 +975,8 @@ class ShellFileOperations(LintMixin, SearchMixin, FileOperations):
|
||||
return self._suggest_similar_files(path)
|
||||
if status == "not_regular":
|
||||
return self._not_regular_error(path)
|
||||
if status == "env_unavailable":
|
||||
return self._env_unavailable_error(path)
|
||||
if self._is_image(path):
|
||||
return ReadResult(is_image=True, is_binary=True, file_size=file_size)
|
||||
is_binary, sample_bytes = self._detect_binary(path)
|
||||
@@ -985,6 +998,8 @@ class ShellFileOperations(LintMixin, SearchMixin, FileOperations):
|
||||
return ReadResult(error=f"File not found: {path}")
|
||||
if status == "not_regular":
|
||||
return self._not_regular_error(path)
|
||||
if status == "env_unavailable":
|
||||
return self._env_unavailable_error(path)
|
||||
if status == "bad_size":
|
||||
return ReadResult(error=f"Could not determine file size: {path}")
|
||||
if max_bytes is not None and file_size > max_bytes:
|
||||
@@ -1357,7 +1372,11 @@ class ShellFileOperations(LintMixin, SearchMixin, FileOperations):
|
||||
error=(f"Invalid file search order {order!r}; expected "
|
||||
"'discovery' or 'modified'."))
|
||||
path = self._expand_path(path)
|
||||
if "not_found" in self._path_exists_probe(path):
|
||||
exists_probe = self._path_exists_probe(path)
|
||||
if "exists" not in exists_probe and "not_found" not in exists_probe:
|
||||
return SearchResult(error=(f"Terminal environment unavailable: could not stat {path} "
|
||||
"(the sandbox may still be starting or was removed). Retry shortly."))
|
||||
if "not_found" in exists_probe:
|
||||
# Models often pass several paths in one string: search the parts that exist.
|
||||
multi = self._try_multi_path_search(
|
||||
pattern, path, target, file_glob, limit, offset, output_mode, context, order)
|
||||
|
||||
Reference in New Issue
Block a user