mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-09 21:49:37 +00:00
fix(sandbox): mask output tails into POSIX-style virtual paths (#5247)
* fix(sandbox): mask output tails into POSIX-style virtual paths The output maskers slice the matched path tail from the original output. With separator-agnostic matching, a Windows-spelled nested tail kept its backslashes and was spliced into the POSIX-style virtual path, so glob results and masked read output showed mixed paths like /mnt/user-data/workspace/pkg\util.py or /mnt/skills/integrations/lark-cli\lark-doc\SKILL.md. Virtual paths are always POSIX-style, so normalize nested tails to forward slashes the same way depth-1 tails already end up. Depth-1 tails and the callable replacer (LocalSandbox._reverse_resolve_path) were unaffected. Pin the nested-tail contract in test_sandbox_path_patterns; the previously failing glob-tool and skills-masking regressions now pass on Windows hosts. * refactor(sandbox): share the mask tail-splicing rule; guard it on Linux CI Review follow-up for #5247: - hoist the tail-splicing rule (slice off the base, strip leading separators, normalize the rest to "/") into path_patterns.normalize_mask_tail and import it at both call sites, so the two maskers can only drift in their matching logic, not in the splice; - add test_mask_local_paths_normalizes_windows_spelled_skill_tails, which spells the skills host root and the output with Windows-style strings so the nested tail keeps backslashes on every platform. Reverting the mask_local_paths_in_output-side normalization now goes red on Linux CI too, not only on Windows hosts.
This commit is contained in:
parent
0b3dadbc9b
commit
fa89a12526
@ -47,6 +47,18 @@ _SEGMENT_BOUNDARY_CHAR = re.compile(r"[^\w./-]")
|
||||
_PATH_TAIL_TERMINATORS = frozenset("\"';&|<>()")
|
||||
|
||||
|
||||
def normalize_mask_tail(tail: str) -> str:
|
||||
"""Normalize a matched output tail for splicing onto a virtual prefix.
|
||||
|
||||
Virtual paths are always POSIX-style, so drop leading separators and
|
||||
convert any remaining backslashes (Windows-spelled output) to forward
|
||||
slashes. Shared by the static-pattern closure in ``sandbox.tools`` and
|
||||
the direct scanner here so the splicing rule exists in exactly one copy
|
||||
and the two sites can only drift in their *matching* logic.
|
||||
"""
|
||||
return tail.lstrip("/\\").replace("\\", "/")
|
||||
|
||||
|
||||
def build_output_mask_pattern(base: str, *, separator_agnostic: bool = False) -> re.Pattern[str]:
|
||||
"""Compile the matcher for one host ``base`` in model-visible output.
|
||||
|
||||
@ -118,7 +130,7 @@ def replace_output_path_matches(
|
||||
if callable(replacement):
|
||||
replaced_path = replacement(matched_path)
|
||||
else:
|
||||
relative = matched_path[len(base) :].lstrip("/\\")
|
||||
relative = normalize_mask_tail(matched_path[len(base) :])
|
||||
replaced_path = f"{replacement}/{relative}" if relative else replacement
|
||||
|
||||
chunks.append(output[copied_until:match_start])
|
||||
|
||||
@ -39,7 +39,7 @@ from deerflow.sandbox.lease import (
|
||||
sandbox_lease_owner,
|
||||
)
|
||||
from deerflow.sandbox.overwrite import unwrap_sandbox
|
||||
from deerflow.sandbox.path_patterns import build_output_mask_pattern, replace_output_path_matches
|
||||
from deerflow.sandbox.path_patterns import build_output_mask_pattern, normalize_mask_tail, replace_output_path_matches
|
||||
from deerflow.sandbox.sandbox import Sandbox
|
||||
from deerflow.sandbox.sandbox_provider import SandboxProvider, get_sandbox_provider
|
||||
from deerflow.sandbox.search import GrepMatch
|
||||
@ -867,7 +867,7 @@ def mask_local_paths_in_output(output: str, thread_data: ThreadDataState | None)
|
||||
matched_path = match.group(0)
|
||||
if matched_path == _base:
|
||||
return _virtual
|
||||
relative = matched_path[len(_base) :].lstrip("/\\")
|
||||
relative = normalize_mask_tail(matched_path[len(_base) :])
|
||||
return f"{_virtual}/{relative}" if relative else _virtual
|
||||
|
||||
result = pattern.sub(replace_match, result)
|
||||
|
||||
@ -110,6 +110,31 @@ def test_direct_replacer_matches_the_shared_boundary_and_tail_contract() -> None
|
||||
assert replacer("root /host/skills, done", "/host/skills", "/mnt/skills", separator_agnostic=True) == "root /mnt/skills, done"
|
||||
|
||||
|
||||
def test_direct_replacer_normalizes_nested_tail_to_virtual_posix_style() -> None:
|
||||
# The tail is sliced from the original output, so a Windows-spelled nested
|
||||
# path kept its backslashes and was spliced into the POSIX-style virtual
|
||||
# path as e.g. /mnt/skills/pkg\\a.md. Virtual paths are always POSIX, so
|
||||
# nested tails must be normalized the same way depth-1 tails already are.
|
||||
assert (
|
||||
path_patterns_module.replace_output_path_matches(
|
||||
"see \\host\\skills\\pkg\\a.md",
|
||||
"/host/skills",
|
||||
"/mnt/skills",
|
||||
separator_agnostic=True,
|
||||
)
|
||||
== "see /mnt/skills/pkg/a.md"
|
||||
)
|
||||
assert (
|
||||
path_patterns_module.replace_output_path_matches(
|
||||
"see C:\\host\\skills\\pkg\\a.md",
|
||||
"C:\\host\\skills",
|
||||
"/mnt/skills",
|
||||
separator_agnostic=True,
|
||||
)
|
||||
== "see /mnt/skills/pkg/a.md"
|
||||
)
|
||||
|
||||
|
||||
def test_separator_agnostic_replacer_avoids_normalization_without_backslashes() -> None:
|
||||
class ReplaceTrackingString(str):
|
||||
def __init__(self, value: str) -> None:
|
||||
|
||||
@ -309,6 +309,24 @@ def test_mask_local_paths_no_thread_data_still_masks_skills() -> None:
|
||||
assert "/mnt/skills/a/b.md" in masked
|
||||
|
||||
|
||||
def test_mask_local_paths_normalizes_windows_spelled_skill_tails() -> None:
|
||||
"""The static-pattern splice normalizes nested Windows-spelled tails.
|
||||
|
||||
This is the Linux-CI guard for the ``mask_local_paths_in_output`` splice:
|
||||
the host root and the output are Windows-spelled *strings*, so the tail
|
||||
keeps backslashes on every platform. Reverting the normalization here
|
||||
turns this test red on Linux CI too, not only on Windows hosts.
|
||||
"""
|
||||
windows_root = "C:\\Users\\alice\\deer-flow\\skills"
|
||||
with (
|
||||
patch("deerflow.sandbox.tools._get_skills_container_path", return_value="/mnt/skills"),
|
||||
patch("deerflow.sandbox.tools._get_skills_host_path", return_value=windows_root),
|
||||
):
|
||||
masked = mask_local_paths_in_output(f"Reading: {windows_root}\\lark-cli\\lark-doc\\SKILL.md", None)
|
||||
|
||||
assert masked == "Reading: /mnt/skills/lark-cli/lark-doc/SKILL.md"
|
||||
|
||||
|
||||
def test_mask_local_paths_hides_global_integration_skill_paths(tmp_path: Path) -> None:
|
||||
from deerflow.config.paths import Paths
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user