fix(router-gate-v4): Smoke 5 REAL fix — path-normalization separator bug
Smoke 5 restart-test (chistaa session) refuted stale-process hypothesis and
identified the real bug: Stream A's pathNormalize() returned OS-native paths
(backslashes on win32) while DEFAULT_PROTECTED_PATTERNS regexes are forward-slash
only.
Trace confirmation:
Stream A pathNormalize('~/foo/bar.jsonl') on win32:
BEFORE: 'c:\\users\\admin\\foo\\bar.jsonl' — backslashes
AFTER: 'c:/users/admin/foo/bar.jsonl' — forward slashes
isProtectedPath now matches → Bash/PowerShell hooks block correctly.
Root cause: path.resolve() + fs.realpathSync() on Windows produce backslashes,
caseFold lowercases them but doesn't change separators. DEFAULT_PROTECTED_PATTERNS
in shell-content-rules.mjs are forward-slash regexes (e.g. /(^|\/)\.claude\/projects/i).
defaultPathNormalize fallback in shell-content-rules.mjs DID normalize separators,
which is why my emergency commit 1072fae4 unit-tests passed but live behavior
failed — live hooks use resolvePathNormalize() which returns Stream A's
buggy implementation.
Fix:
- path-normalization.mjs: append .split('\\').join('/') to pathNormalize output.
- path-normalization.test.mjs: +1 RED→GREEN test for win32 separator normalization.
Why previous commit 1072fae4 was incomplete:
- Added pattern to protected list ✓
- Added enforce-read-path-deny.mjs ✓ (Read tool — works because hook uses
defaultPathNormalize directly, not resolvePathNormalize)
- Did NOT detect Bash/PowerShell path-normalize integration bug (debug script
bypassed Stream A by passing defaultPathNormalize directly).
Side observation (recorded as Stream H TODO by chistaa session):
- extractPathArgs/pathDenyOverlay — non-reading path in non-first position is
not checked fully. Independent latent bug, separate fix.
Regression: 1715/1715 vitest tools GREEN (+1 separator test).
Critical: re-run Smoke 5 in clean session — expected PASS all 6 vectors now.
This commit is contained in:
@@ -94,5 +94,8 @@ export function pathNormalize(target, opts = {}) {
|
||||
if (e && e.code && e.code !== 'ENOENT') throw e; // surface real FS errors; fail-close handled by caller
|
||||
real = resolved; // ENOENT — best-effort resolved path for unknown-state files
|
||||
}
|
||||
return caseFold(real, platform);
|
||||
// Smoke 5 integration fix (2026-05-30): normalize ALL separators to forward slashes
|
||||
// regardless of platform. DEFAULT_PROTECTED_PATTERNS regexes are forward-slash only.
|
||||
// Without this, win32 path.resolve + realpath returns backslashes and patterns miss.
|
||||
return caseFold(real, platform).split('\\').join('/');
|
||||
}
|
||||
|
||||
@@ -78,6 +78,20 @@ describe('pathNormalize', () => {
|
||||
it('expands ~ and resolves', () => {
|
||||
expect(pathNormalize('~/x', { homedir: '/h', realpath: (p) => p, resolve: (p) => p, platform: 'linux', env: {} })).toBe('/h/x');
|
||||
});
|
||||
// Smoke 5 integration bug 2026-05-30 — Stream A pathNormalize returned backslashes
|
||||
// on win32, breaking DEFAULT_PROTECTED_PATTERNS regex (forward-slash-only) match.
|
||||
// Fix: normalize all separators to forward slashes in output, regardless of platform.
|
||||
it('normalizes backslashes to forward slashes on win32 (integration fix for protected patterns)', () => {
|
||||
const result = pathNormalize('~/foo/bar.jsonl', {
|
||||
homedir: 'C:\\Users\\Admin',
|
||||
realpath: (p) => p,
|
||||
resolve: path.win32.resolve,
|
||||
platform: 'win32',
|
||||
env: {},
|
||||
});
|
||||
expect(result).not.toMatch(/\\/);
|
||||
expect(result.includes('/')).toBe(true);
|
||||
});
|
||||
it('collapses .. via path.resolve', () => {
|
||||
// Use path.posix.resolve to simulate POSIX behaviour cross-platform
|
||||
const result = pathNormalize('a/../b', { realpath: (p) => p, resolve: path.posix.resolve, platform: 'linux', homedir: '/h', env: {} });
|
||||
|
||||
Reference in New Issue
Block a user