mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-19 11:06:18 +00:00
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.
This commit is contained in:
parent
d811143b52
commit
796ca28f55
@ -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"(?<![\w./-]){re.escape(name)}(?!(?:[\w/-]|\.[\w]))")
|
||||
rewritten_text, count = pattern.subn(unique[name], rewritten)
|
||||
# A callable replacement, not a template: the virtual path is built from
|
||||
# the real file's relative path, where a backslash is an ordinary
|
||||
# character, so it must never be read as a regex escape.
|
||||
rewritten_text, count = pattern.subn(lambda _match: unique[name], rewritten)
|
||||
if count:
|
||||
logger.debug("MCP bare filename rewrite: %s -> %s", name, unique[name])
|
||||
rewritten = rewritten_text
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user