From 60f70942d2fb86c5871660ab2d3ff6407f71b5a2 Mon Sep 17 00:00:00 2001 From: Ziiii <273036393+zixuanguo786-ctrl@users.noreply.github.com> Date: Mon, 6 Jul 2026 15:42:39 +0800 Subject: [PATCH] Resolve Windows-style relative paths when linkifying The linkifier recognized backslashes as path separators, but existence checks used the raw token. On POSIX that treats docs\readme.md as a literal filename, so Windows-style relative paths were not linked even when docs/readme.md exists. Constraint: Preserve the existing markdown label and security containment check. Rejected: Drop backslash support | the public path-detection logic already treats backslashes as supported separators. Confidence: high Scope-risk: narrow Reversibility: clean Directive: Normalize path separators for filesystem lookup while keeping display text unchanged. Tested: uv run pytest tests/test_executor/test_linkify.py -q Tested: uv run ruff check src/conductor/executor/linkify.py tests/test_executor/test_linkify.py Not-tested: Windows filesystem runtime. --- src/conductor/executor/linkify.py | 2 +- tests/test_executor/test_linkify.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/conductor/executor/linkify.py b/src/conductor/executor/linkify.py index acd8a484..67d294fa 100644 --- a/src/conductor/executor/linkify.py +++ b/src/conductor/executor/linkify.py @@ -236,7 +236,7 @@ def _try_linkify_path(token: str, base_dir: Path | None) -> str | None: if base_dir is not None: try: resolved_base = base_dir.resolve() - candidate = (base_dir / stripped).resolve() + candidate = (base_dir / normalized).resolve() # Security: must be within base_dir (path-aware containment, not # string-prefix — `/foo/bar` must not match `/foo/barbaz/...`). if not candidate.is_relative_to(resolved_base): diff --git a/tests/test_executor/test_linkify.py b/tests/test_executor/test_linkify.py index ad890c39..1d040af6 100644 --- a/tests/test_executor/test_linkify.py +++ b/tests/test_executor/test_linkify.py @@ -100,6 +100,13 @@ def test_nested_path(self, tmp_path: Path) -> None: result = linkify_markdown("Plan at docs/projects/plan.md", base_dir=tmp_path) assert "[docs/projects/plan.md](docs/projects/plan.md)" in result + def test_windows_relative_path_separator(self, tmp_path: Path) -> None: + (tmp_path / "docs").mkdir() + (tmp_path / "docs" / "readme.md").write_text("hello") + + result = linkify_markdown(r"See docs\readme.md for details", base_dir=tmp_path) + assert r"[docs\readme.md](docs/readme.md)" in result + def test_nonexistent_file_not_linked(self, tmp_path: Path) -> None: result = linkify_markdown("See docs/missing.md for details", base_dir=tmp_path) assert "[docs/missing.md]" not in result