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:wiki
What 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 ValidationError
That directory0 parameter is Path("/tmp/wiki").
Environment
- OS: Windows 11
- Python version: 3.13
- CodeAlmanac version: 0.4.7 /
main at 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.
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().PureWindowsPathreports two anchored forms as relative:is_absolute()driveroot/tmp/wikiFalse'''\'C:wikiFalse'C:'''Both still discard the left-hand side when joined, which is exactly what the guard exists to prevent:
What 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:
That
directory0parameter isPath("/tmp/wiki").Environment
mainat 0f15350almanac/Logs or output
Neither later check catches it either:
len(parts)is 3, andpartsis('\', '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_rootis lower severity because its later!= almanaccheck 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 atalmanac/" pin is ever relaxed.Current blast radius is limited: the
ignored_directoriessink 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_rootedpredicate that rejects any anchor. On POSIX a root implies an absolute path anddriveis always empty, so it is provably equivalent to the old check there and macOS/Linux behaviour is unchanged. The tests use explicitPureWindowsPath/PurePosixPathrather than the platform-nativePath, so both platforms' semantics are pinned on any runner — meaning it is fully reviewable on the current Linux-only CI without a Windows machine.