From 796ca28f550bfe8c43f64563383d8c4d1c8064d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=93=88=E5=9F=BA=E7=B1=B3?= <140241684+BlueX888@users.noreply.github.com> Date: Fri, 18 Sep 2026 08:03:37 +0800 Subject: [PATCH] fix(mcp): insert bare-filename rewrites literally (#5522) _rewrite_unique_bare_filenames handed the correlated /mnt/user-data virtual path to Pattern.subn as a replacement template. That path is built from the file's relative path, and a backslash is an ordinary character in a POSIX filename, so a file written literally as "screenshots\q3.png" -- the shape a model produces by passing a Windows-style path to a stdio server on a POSIX host -- turned \q into an unknown template escape. Pattern.subn compiles the template eagerly, so re.error escaped _convert_call_tool_result and failed the whole tool call even though the server had already written the file, and the agent never saw the path. When the backslash does start a known escape (\r, \t, \b ...), the bare-filename pass substituted that byte into the returned text instead, so "screenshots\raw.png" came back as a path with a raw CR in the middle of it. Insert the correlated path through a callable replacement, matching what _rewrite_local_paths_in_text already does, so it is never parsed as a template. --- .../packages/harness/deerflow/mcp/tools.py | 5 ++- backend/tests/test_mcp_file_migration.py | 42 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/backend/packages/harness/deerflow/mcp/tools.py b/backend/packages/harness/deerflow/mcp/tools.py index 3aba1cbff..101a5d2a3 100644 --- a/backend/packages/harness/deerflow/mcp/tools.py +++ b/backend/packages/harness/deerflow/mcp/tools.py @@ -277,7 +277,10 @@ def _rewrite_unique_bare_filenames( # Do not rewrite inside longer paths/words. A final sentence period is # allowed, but ".bak" or another path segment is not. pattern = re.compile(rf"(? %s", name, unique[name]) rewritten = rewritten_text diff --git a/backend/tests/test_mcp_file_migration.py b/backend/tests/test_mcp_file_migration.py index 23adb5106..ebe2223e9 100644 --- a/backend/tests/test_mcp_file_migration.py +++ b/backend/tests/test_mcp_file_migration.py @@ -426,6 +426,48 @@ class TestRewriteLocalPathsInText: assert result == text +@pytest.mark.skipif(os.name == "nt", reason="a literal backslash in a filename is POSIX-only") +class TestRewriteUniqueBareFilenames: + """The correlated virtual path must be inserted verbatim, never as a template. + + The replacement is built from the real file's relative path, where a + backslash is an ordinary character, so handing it to ``re.sub`` as a + template reads it as a regex escape instead. + """ + + def test_backslash_in_replacement_is_inserted_literally(self, paths: Paths): + workspace = paths.sandbox_work_dir("t1", user_id="u1") + src = _workspace_file(paths, r"screenshots\raw.png") + text = r"Saved as screenshots\raw.png" + + with _patch_paths(paths): + result = mcp_tools._rewrite_unique_bare_filenames( + text, + changed_files=[src], + thread_id="t1", + user_id="u1", + source_base_dir=workspace, + ) + + assert result == f"Saved as {VIRTUAL_PATH_PREFIX}/workspace/screenshots\\raw.png" + + def test_unknown_regex_escape_in_replacement_does_not_raise(self, paths: Paths): + workspace = paths.sandbox_work_dir("t1", user_id="u1") + src = _workspace_file(paths, r"screenshots\q3.png") + text = r"Saved as screenshots\q3.png" + + with _patch_paths(paths): + result = mcp_tools._rewrite_unique_bare_filenames( + text, + changed_files=[src], + thread_id="t1", + user_id="u1", + source_base_dir=workspace, + ) + + assert result == f"Saved as {VIRTUAL_PATH_PREFIX}/workspace/screenshots\\q3.png" + + class TestWorkspaceSnapshots: def test_changed_workspace_files_detects_created_and_modified_files(self, paths: Paths): import time