Repository navigation
bug: is_absolute() repo-relative guards accept paths that escape the repo on Windows #72
Description
What happened?
Two guards that promise a path is "repo-relative" accept paths that escape the repo on Windows, because they are built on Path.is_absolute().
PureWindowsPath reports two anchored forms as relative:
| input | is_absolute() |
drive |
root |
|---|---|---|---|
/tmp/wiki |
False |
'' |
'\' |
C:wiki |
False |
'C:' |
'' |
Both still discard the left-hand side when joined, which is exactly what the guard exists to prevent:
PureWindowsPath("C:/repo") / "/tmp/wiki" # -> C:/tmp/wiki
PureWindowsPath("D:/repo") / "C:wiki" # -> C:wikiWhat did you expect?
The guards' own error messages state the intent:
SourceRuntimeContext.ignored_directories— "source runtime ignored directories must be repo-relative"require_default_almanac_root— "Almanac root must be a repo-relative path"
Reproduction
The repo already asserts this input is unsafe, and the assertion fails on Windows:
uv run pytest "tests/test_filesystem_source_runtime.py::test_source_runtime_context_rejects_unsafe_ignored_directories" -q
# FAILED ...[directory0] - Failed: DID NOT RAISE ValidationErrorThat directory0 parameter is Path("/tmp/wiki").
Environment
- OS: Windows 11
- Python version: 3.13
- CodeAlmanac version: 0.4.7 /
mainat 0f15350 - Install method: local checkout
- Almanac root:
almanac/
Logs or output
directory = WindowsPath('/tmp/wiki')
@pytest.mark.parametrize(
"directory",
(Path("/tmp/wiki"), Path("."), Path("../wiki"), Path("docs/../wiki")),
)
def test_source_runtime_context_rejects_unsafe_ignored_directories(directory):
> with pytest.raises(ValidationError):
E Failed: DID NOT RAISE ValidationError
Neither later check catches it either: len(parts) is 3, and parts is ('\', 'tmp', 'wiki'), so the {"..", "~"} filter never matches.
Extra context
This matters most for ignored_directories, since source runtime context is populated from wiki page frontmatter that agents author — so the value is not necessarily developer-typed. require_default_almanac_root is lower severity because its later != almanac check already rejects these inputs; that half is defence in depth rather than a live escape, but it is the same latent mistake and becomes load-bearing if the "fixed at almanac/" pin is ever relaxed.
Current blast radius is limited: the ignored_directories sink is pathspec pattern generation (integrations/sources/filesystem/ignore.py), which yields an anchored /tmp/wiki/ pattern rather than a filesystem traversal. So it is a broken invariant with a currently-benign sink — but the guard is the stated defence and it does not hold off POSIX.
I have a fix up in #65: both guards share one core.paths.is_rooted predicate that rejects any anchor. On POSIX a root implies an absolute path and drive is always empty, so it is provably equivalent to the old check there and macOS/Linux behaviour is unchanged. The tests use explicit PureWindowsPath/PurePosixPath rather than the platform-native Path, so both platforms' semantics are pinned on any runner — meaning it is fully reviewable on the current Linux-only CI without a Windows machine.
Activity