Skip to content

[Bug] Windows 宿主上 write_file / edit_file 必然失败:命令中的双引号在传参往返中被丢弃 #3262

Open

Description

@1076572358

环境:agentscope-harness 2.0.3,JVM 运行在 Windows(容器服务端为 linux/amd64,docker 模式)

现象:write_file 恒定返回 Error: Failed to write file '<path>'(不带任何原因);
edit_file 同样失败;read_file / list_files / execute 均正常。

根因(已定位到具体代码):

  1. BaseSandboxFilesystem.write() 的命令为
    if [ -e X ]; then echo 'EXISTS'; exit 1; fi; mkdir -p "$(dirname X)" 2>&1
    其中 "$(dirname X)" 的引号内含一个空格。
  2. BaseSandboxFilesystem.edit() 用 python3 -c "<含大量空格与换行的脚本>",同理。
  3. Windows 的 CreateProcess 只接受一整条命令行字符串,Java 需把 argv 序列化后由 docker.exe
    解析回来;该往返中内层双引号丢失,引号内的空格退化为参数分隔符。
    最小复现(不依赖 docker,Java → 子进程):
    LAUNCHER sent 1 argument, length=93
    TARGET argc=2
    [1] = ...; mkdir -p $(dirname
    [2] = 'file1.txt') 2>&1
    容器内 dash 因此报:Syntax error: end of file unexpected (expecting ")") ,exit code=2。
  4. write() 第 214 行(return WriteResult.fail("Failed to write file '" + filePath + "'"))
    恰好把 checkResult.output()(含上述报错原文)丢弃,导致现象被误读为"工具故障"。
  5. read()/ls() 因命令中不含"引号内带空格"的写法而侥幸不受影响 —— 这正是"只有
    write/edit 坏"的原因。

对照:ShellExecuteTool.commandWithWorkingDirectory(..., boolean windows) 已按 OS 分支处理
shell 引号(Windows 用 ""、POSIX 用单引号),可见项目已知该差异;
BaseSandboxFilesystem 这两处漏了。

建议修复(任选,建议 a+d):
a) 让命令不再依赖"引号内的空格":write 改为
d=$(dirname X); mkdir -p "$d" 2>&1
b) edit 的 python 脚本改走 heredoc(本方法已用 heredoc 传 payload,把脚本也移过去即可):
python3 - <<'EDIT_PY' ... EDIT_PY
c) 更彻底:命令统一经 stdin 传入(docker exec -i sh -s),彻底绕开 argv 的转义往返。
d) 可诊断性:write()/edit() 失败时把 checkResult.output() 带进错误信息(一行改动;
本次排查的全部成本几乎都耗在这条信息被丢弃上)。

Activity

  1. oss-maintainer commented on Sep 23, 2026

    @oss-maintainer
    Collaborator

    Issue Evaluation

    Category: bug / area/harness (sandbox filesystem) | Status: Confirmed — the quoted-space command construction in BaseSandboxFilesystem is real on main, and the discarded error output matches the reported "failure without reason" symptom.

    Verified against main @ 667590c99 (static review; the Windows + docker.exe argv round-trip itself was not executed here — see Caveats).

    1. The quoted-space commands exist exactly as described.

    • write() builds ... mkdir -p "$(dirname <path>)" 2>&1 — double quotes containing a space, passed as one argv element all the way to sh -c: BaseSandboxFilesystem.java:197-204 (the literal "\"$(dirname " is at :201).
    • edit() embeds a whole multi-line Python program inside python3 -c "..." with unquoted spaces on every line: BaseSandboxFilesystem.java:258-283.
    • Execution path: DockerSandbox.doExec passes the command as a single argv element to docker exec ... sh -c <command> (DockerSandbox.java:150-161) via ProcessBuilder(List<String>). On POSIX hosts the JVM quotes each element back into the command line and docker.exe's re-parse round-trips cleanly; on a Windows host the argv → command-line-string → re-parse round-trip is exactly where the inner double quotes (and the spaces they protected) degrade into argv separators, as the reporter's minimum reproduction shows.

    2. The bare, reason-less failure message is real. write() returns WriteResult.fail("Failed to write file '" + filePath + "'") when the pre-check exits non-zero — checkResult.output() (which carries the container's dash: Syntax error: end of file unexpected (expecting ")")) is discarded at BaseSandboxFilesystem.java:214. Note line :228 (the upload failure branch) does append the error, so dropping checkResult.output() at :214 is the outlier, not the convention.

    3. Why only write/edit break. ls/read/grep/find quote arguments via FilesystemUtils.shellQuote (single quotes, FilesystemUtils.java:101-106) and never produce a double-quoted span containing a space (BaseSandboxFilesystem.java:73-80, :138, :159-163, :372-378), so they survive the round-trip unchanged — matching the reported asymmetry.

    4. The project already knows about this OS difference. ShellExecuteTool.commandWithWorkingDirectory(..., boolean windows) branches on os.name for quoting (ShellExecuteTool.java:72-77, :92-98), and mv() already uses the quote-free form mkdir -p $(dirname <escaped>) (BaseSandboxFilesystem.java:433) — i.e. suggested fix (a) is literally the existing convention elsewhere in the same file.

    Impact: deterministic failure of write_file/edit_file for any Windows host driving the docker sandbox backend (every quoted-space construct is broken, and edit()'s python3 -c script is the most fragile); non-Windows hosts unaffected.

    Fix assessment (agree with a + d as the minimal complete change):

    • (a) write(): d=$(dirname <path>); mkdir -p "$d" — removes the quoted space; no behavior change on POSIX.
    • (b/c) edit(): a quoted-space-free rewrite is not sufficient — the script contains many unquoted spaces and newlines, so route the payload via stdin (python3 - <<'PY' ... or docker exec -i ... sh -s), reusing the existing base64-payload plumbing.
    • (d) append checkResult.output() at :214 (one line, removes the "silent failure" class entirely).
    • Tests: extend BaseSandboxFilesystemTest to assert no constructed command contains a double-quoted span with a space (cheap regression guard for all current and future call sites).

    Caveats: the Windows command-line round-trip was reproduced by the reporter, not re-executed in this evaluation; the code-side evidence above is consistent with their minimum repro, and the fix is safe regardless of host OS. This is a self-contained harness fix — filing a PR is welcome.


    Automated evaluation by github-manager-bot

  2. aMetric commented on Sep 23, 2026

    @aMetric
    Contributor

    I'd like to take this — implementing the evaluation's (a) + (d) minimal scope:

    • (a) write(): replace the quoted-space mkdir -p "$(dirname <p>)" construction with d=$(dirname <p>); mkdir -p "$d" — the same quote-free-spread pattern move() already uses in the same file.
    • (d) write(): append the discarded checkResult.output() to the failure message so the container's error text (e.g. the dash syntax error) is visible instead of a bare Failed to write file '...'.

    Out of scope for this PR (matching the evaluation): the edit() heredoc/stdin rewrite — that overlaps the in-flight #3084 / #3226, so I'll leave it to those. I'll add a regression test asserting no constructed write() command contains a double-quoted span with a space.

  3. aMetric commented on Sep 23, 2026

    @aMetric
    Contributor

    PR submitted: #3269 — implements the agreed (a)+(d) scope: d=$(dirname <p>); mkdir -p "$d" (same pattern move() already uses) plus the discarded checkResult.output() now surfaced in the failure message. Three new all-platform regression tests, sabotage-verified (restoring the old construction fails the new test). edit() left to the in-flight #3084 / #3226 as discussed.

  4. littlebird69 commented on Oct 17, 2026

    @littlebird69
    Contributor

    +1, still reproducible on 2.0.4 (Windows 11 host, Docker Desktop, JDK 25).

    DockerSandbox builds docker exec ... sh -c <command> with ProcessBuilder (DockerSandbox.java#L150-L160). On Windows, the JDK joins the argument list into one command line without escaping embedded double quotes, so echo "a b" reaches the container as echo a.

    As a result, every file write fails. BaseSandboxFilesystem#write first runs the pre-check mkdir -p "$(dirname '<path>')" (BaseSandboxFilesystem.java#L195-L214). The quotes break that check, and its output is discarded, so all you get back is a generic Failed to write file '…'. The new docker cp-based SandboxFileTransfer in 2.0.4 does not help, because write() still runs that pre-check first.

    Our workaround: base64-encode the command, because the base64 alphabet contains no quotes, and decode it inside the container. The script is fed through fd 3 while stdin stays on /dev/null:

    echo <b64> | base64 -d | sh /dev/fd/3 3<&0 0</dev/null

    The plain echo <b64> | base64 -d | sh would feed the script through stdin. Any command in it that reads stdin would then consume the rest of the script, and callers that expect an empty stdin would behave differently. This works unchanged on Linux and Windows hosts, and it needs only base64 and /dev/fd, which Debian/Ubuntu, BusyBox and the Docker default images all provide.

  5. aiyili commented on Oct 23, 2026

    @aiyili
    Contributor

    Cross-linking: this is the same root cause as #2924 (Windows ProcessBuilder/cmd.exe argument re-parsing strips nested double quotes from the sh -c payload). A fix is up in PR #3383 — the Windows path now feeds the script to docker exec -i ... sh via stdin instead of the command line, so quotes survive. If you can, please test PR #3383's branch on your setup (Windows 11 + Docker Desktop) and confirm it resolves this too — it would let the maintainers close both issues with one fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions