From 9799b4d3e8fb4b2f0cdcc3a4b98bdffd66833faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Sat, 30 May 2026 08:39:52 +0300 Subject: [PATCH] =?UTF-8?q?fix(router-gate-v4):=20Smoke=205=20REAL=20fix?= =?UTF-8?q?=20=E2=80=94=20path-normalization=20separator=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- tools/path-normalization.mjs | 5 ++++- tools/path-normalization.test.mjs | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/tools/path-normalization.mjs b/tools/path-normalization.mjs index 97f8bc234..1be3c5f64 100644 --- a/tools/path-normalization.mjs +++ b/tools/path-normalization.mjs @@ -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('/'); } diff --git a/tools/path-normalization.test.mjs b/tools/path-normalization.test.mjs index b6c77d6a7..2a9611812 100644 --- a/tools/path-normalization.test.mjs +++ b/tools/path-normalization.test.mjs @@ -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: {} });