What happened?
PawWork's permission system on Windows does not normalize different path spellings that point to the same physical file into a single glob. Calling assertExternalDirectory or the read tool with variants such as D:\foo\bar.txt, d:\FOO\bar.txt, /foo/bar.txt (drive stripped, separators flipped, case folded) produces different permission patterns. Consequences:
- Authorizing one variant ("always") does not cover the others, so the same file can prompt for permission repeatedly.
- When CWD is on a different drive, a driveless rooted path is interpreted as a different physical file, so the actual coverage of the granted permission diverges from what the user thought they authorized.
#431 (merged) fixed the rooted-but-driveless case for paths that already exist on disk by probing known drive roots in normalizeWindowsAbsolutePath. The remaining variants below are still not handled and are kept tracked here.
Remaining scope (still open)
- UNC and extended-length prefixes:
\\?\D:\foo\bar.txt, \\server\share\... are not folded to the same canonical form as their plain counterparts.
- Ambiguous multi-drive matches: when a rooted-but-driveless path exists on more than one drive (e.g.
C:\foo\bar.txt and D:\foo\bar.txt both exist), the current probe picks the first hit by drive-root iteration order. Behavior should be defined and documented (error, prefer CWD drive, or explicit pick).
- Write targets / non-existing paths: for paths that do not yet exist (typical for write tools), the probe finds nothing and falls back to
win32.resolve, which still binds to CWD's drive. Permission globs for new files therefore still depend on CWD.
- Drive-root probe caching:
windowsDriveRoots() runs per call with up to 26 existsSync syscalls. Hot paths (LSP, ripgrep) that previously assumed normalizePath was pure now pay this cost on Windows. Cache invalidation strategy is a follow-up.
Which area seems affected?
App flow or product behavior
How much does this affect you?
Security or data-loss risk
Steps to reproduce
- On Windows, start PawWork or the corresponding dev test environment.
- Have the agent call
read or any tool guarded by assertExternalDirectory with a mangled path variant from the remaining-scope list above (UNC prefix, ambiguous multi-drive existence, or a non-existing write target with a driveless rooted path) pointing outside the project.
- Observe the generated permission request
patterns.
Expected: normalized to a canonical Windows path, with permission identical to what an access using the canonical spelling would receive.
Actual: different globs are produced, and for non-existing paths the resolved physical file still depends on the current CWD's drive.
What did you expect to happen?
assertExternalDirectory and read share a single path canonicalize helper. Any Windows path variant (drive case, separator, case fold, /-prefix without drive, UNC \\?\ prefix, ambiguous multi-drive, non-existing write target) is normalized to one canonical form before deriving permission globs, and permission resolution is derived from that canonical form rather than from the raw user-supplied string passed to fs.open.
PawWork version
dev branch, post-#431
OS version
Windows (CI: windows-latest); not validated on a local Windows machine.
Can you reproduce it again?
Yes, every time
Diagnostics
Original failing run: 25303162127, job unit-windows-opencode-server-tools.
Originally failing tests, both fixed by #431:
tool.assertExternalDirectory > normalizes Windows path variants to one glob (external-directory.test.ts:118)
tool.read external_directory permission > normalizes read permission paths on Windows (read.test.ts:149)
The remaining-scope items above do not yet have dedicated tests; add them when picking this back up.
What happened?
PawWork's permission system on Windows does not normalize different path spellings that point to the same physical file into a single glob. Calling
assertExternalDirectoryor thereadtool with variants such asD:\foo\bar.txt,d:\FOO\bar.txt,/foo/bar.txt(drive stripped, separators flipped, case folded) produces different permission patterns. Consequences:#431 (merged) fixed the rooted-but-driveless case for paths that already exist on disk by probing known drive roots in
normalizeWindowsAbsolutePath. The remaining variants below are still not handled and are kept tracked here.Remaining scope (still open)
\\?\D:\foo\bar.txt,\\server\share\...are not folded to the same canonical form as their plain counterparts.C:\foo\bar.txtandD:\foo\bar.txtboth exist), the current probe picks the first hit by drive-root iteration order. Behavior should be defined and documented (error, prefer CWD drive, or explicit pick).win32.resolve, which still binds to CWD's drive. Permission globs for new files therefore still depend on CWD.windowsDriveRoots()runs per call with up to 26existsSyncsyscalls. Hot paths (LSP, ripgrep) that previously assumednormalizePathwas pure now pay this cost on Windows. Cache invalidation strategy is a follow-up.Which area seems affected?
App flow or product behavior
How much does this affect you?
Security or data-loss risk
Steps to reproduce
reador any tool guarded byassertExternalDirectorywith a mangled path variant from the remaining-scope list above (UNC prefix, ambiguous multi-drive existence, or a non-existing write target with a driveless rooted path) pointing outside the project.patterns.Expected: normalized to a canonical Windows path, with permission identical to what an access using the canonical spelling would receive.
Actual: different globs are produced, and for non-existing paths the resolved physical file still depends on the current CWD's drive.
What did you expect to happen?
assertExternalDirectoryandreadshare a single path canonicalize helper. Any Windows path variant (drive case, separator, case fold,/-prefix without drive, UNC\\?\prefix, ambiguous multi-drive, non-existing write target) is normalized to one canonical form before deriving permission globs, and permission resolution is derived from that canonical form rather than from the raw user-supplied string passed tofs.open.PawWork version
dev branch, post-#431
OS version
Windows (CI:
windows-latest); not validated on a local Windows machine.Can you reproduce it again?
Yes, every time
Diagnostics
Original failing run:
25303162127, jobunit-windows-opencode-server-tools.Originally failing tests, both fixed by #431:
tool.assertExternalDirectory > normalizes Windows path variants to one glob(external-directory.test.ts:118)tool.read external_directory permission > normalizes read permission paths on Windows(read.test.ts:149)The remaining-scope items above do not yet have dedicated tests; add them when picking this back up.