Skip to content

Commit bf1c084

Browse files
author
zhansheng.lzs
committed
acli: sync the write_file temp-dir fix from agenticCLI 749999a
write_file refused any path outside the cwd, which made it stricter than every layer around it without protecting anything: run_command can already write to temp because the shell is not path-guarded, delete_file reaches anywhere behind its own prompt, and the opt-in OS sandbox confines writes to "the workspace and temp areas". The refusal also cost more than a failed call — models reach for /tmp for scratch files by instinct, so the retry put the scratch file in the user's repo instead, which is the worse outcome. Writes now land in the cwd or the realpath'd system temp dir. Both sides are resolved with realpath, so a symlink inside either root still cannot escape, and validate_path runs first so SENSITIVE_NAMES keeps blocking e.g. /tmp/.ssh/config. The refusal message names both roots generically rather than echoing absolute paths. Per-file sync only: the two files are byte-identical to agenticCLI modulo the acli. -> dashscope.acli. import rewrite. examples/ was preserved by the sync script, and no version is bumped -- agenticCLI stays at 0.6.7.
1 parent db76344 commit bf1c084

2 files changed

Lines changed: 49 additions & 9 deletions

File tree

‎dashscope/acli/tools/filesystem.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ def read_file(
9696
name="write_file",
9797
description=(
9898
"Write content to a file; overwrites if it exists. "
99-
"The result includes a unified diff."
99+
"The result includes a unified diff. `path` must be inside the "
100+
"working directory or the system temp dir."
100101
),
101102
permission=PermissionLevel.CONFIRM,
102103
)

‎dashscope/acli/utils/paths.py‎

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,22 +61,61 @@ def validate_path(path: str) -> str:
6161
return path
6262

6363

64+
def _temp_write_roots() -> tuple[str, ...]:
65+
"""Realpath'd system temp directories a write is allowed to land in.
66+
67+
``run_command`` can already write here — the shell is not path-guarded —
68+
``delete_file`` reaches anywhere behind its own prompt, and the opt-in OS
69+
sandbox confines writes to "the workspace and temp areas". A cwd-only rule
70+
for write_file was therefore stricter than every layer around it without
71+
protecting anything, and it cost more than a refused call: models reach
72+
for /tmp for scratch files by instinct, so the retry put the scratch file
73+
in the user's repo instead, which is the worse outcome.
74+
75+
``tempfile.gettempdir()`` is what makes this portable — it honours
76+
TMPDIR/TEMP/TMP, which on macOS is a per-user /var/folders path rather than
77+
/tmp. /tmp is listed separately because that is what a model actually
78+
writes, and realpath is what ties it to /private/tmp there.
79+
"""
80+
import tempfile
81+
82+
candidates = [tempfile.gettempdir()]
83+
if os.name != "nt":
84+
candidates += ["/tmp", "/private/tmp"]
85+
roots: list[str] = []
86+
for candidate in candidates:
87+
if not candidate:
88+
continue
89+
try:
90+
resolved = os.path.realpath(candidate)
91+
except OSError:
92+
continue
93+
if resolved not in roots and os.path.isdir(resolved):
94+
roots.append(resolved)
95+
return tuple(roots)
96+
97+
6498
def validate_write_path(path: str) -> str:
6599
"""Validate a path for write operations (stricter than read).
66100
67-
In addition to sensitive-path blocking, prevents writes outside cwd.
101+
In addition to sensitive-path blocking, confines writes to the working
102+
directory and the system temp dir.
68103
"""
69104
path = validate_path(path)
70105
# Resolve both sides with realpath: abspath doesn't follow symlinks, so
71106
# a symlink inside cwd pointing outside would otherwise pass the check.
72107
abs_path = os.path.realpath(path)
73-
cwd = os.path.realpath(".")
74-
if not abs_path.startswith(cwd + os.sep) and abs_path != cwd:
75-
raise ValueError(
76-
f"Write path '{path}' is outside the current working "
77-
"directory (path traversal guard)",
78-
)
79-
return path
108+
roots = (os.path.realpath("."),) + _temp_write_roots()
109+
inside = any(
110+
abs_path == root or abs_path.startswith(root + os.sep)
111+
for root in roots
112+
)
113+
if inside:
114+
return path
115+
raise ValueError(
116+
f"Write path '{path}' is outside the writable roots: the working "
117+
"directory and the system temp dir (path traversal guard)",
118+
)
80119

81120

82121
def atomic_write_text(path: Path, text: str, encoding: str = "utf-8") -> None:

0 commit comments

Comments
 (0)