From ee754c46f992dd4024e56e93246421246d16d13f Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:05:21 +1000 Subject: [PATCH] fix(win32): normalize paths at permission boundaries (#14738) --- packages/opencode/src/tool/external-directory.ts | 2 +- packages/opencode/src/util/wildcard.ts | 5 ++++- packages/opencode/test/tool/bash.test.ts | 4 ++-- packages/opencode/test/tool/read.test.ts | 4 ++-- packages/opencode/test/util/wildcard.test.ts | 15 +++++++++++++++ 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/tool/external-directory.ts b/packages/opencode/src/tool/external-directory.ts index 1d3958fc4..5d8885b2a 100644 --- a/packages/opencode/src/tool/external-directory.ts +++ b/packages/opencode/src/tool/external-directory.ts @@ -18,7 +18,7 @@ export async function assertExternalDirectory(ctx: Tool.Context, target?: string const kind = options?.kind ?? "file" const parentDir = kind === "directory" ? target : path.dirname(target) - const glob = path.join(parentDir, "*") + const glob = path.join(parentDir, "*").replaceAll("\\", "/") await ctx.ask({ permission: "external_directory", diff --git a/packages/opencode/src/util/wildcard.ts b/packages/opencode/src/util/wildcard.ts index 4a6eba96f..f54b6c85f 100644 --- a/packages/opencode/src/util/wildcard.ts +++ b/packages/opencode/src/util/wildcard.ts @@ -2,6 +2,8 @@ import { sortBy, pipe } from "remeda" export namespace Wildcard { export function match(str: string, pattern: string) { + if (str) str = str.replaceAll("\\", "/") + if (pattern) pattern = pattern.replaceAll("\\", "/") let escaped = pattern .replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape special regex chars .replace(/\*/g, ".*") // * becomes .* @@ -13,7 +15,8 @@ export namespace Wildcard { escaped = escaped.slice(0, -3) + "( .*)?" } - return new RegExp("^" + escaped + "$", "s").test(str) + const flags = process.platform === "win32" ? "si" : "s" + return new RegExp("^" + escaped + "$", flags).test(str) } export function all(input: string, patterns: Record) { diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index 3bd923b60..db05f8f62 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -203,8 +203,8 @@ describe("tool.bash permissions", () => { await bash.execute( { - command: "rm tmpfile", - description: "Remove tmpfile", + command: `rm -rf ${path.join(tmp.path, "nested")}`, + description: "remove nested dir", }, testCtx, ) diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index 88228f14e..b22fc3e71 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -74,7 +74,7 @@ describe("tool.read external_directory permission", () => { await read.execute({ filePath: path.join(outerTmp.path, "secret.txt") }, testCtx) const extDirReq = requests.find((r) => r.permission === "external_directory") expect(extDirReq).toBeDefined() - expect(extDirReq!.patterns.some((p) => p.includes(outerTmp.path))).toBe(true) + expect(extDirReq!.patterns.some((p) => p.includes(outerTmp.path.replaceAll("\\", "/")))).toBe(true) }, }) }) @@ -100,7 +100,7 @@ describe("tool.read external_directory permission", () => { await read.execute({ filePath: path.join(outerTmp.path, "external") }, testCtx) const extDirReq = requests.find((r) => r.permission === "external_directory") expect(extDirReq).toBeDefined() - expect(extDirReq!.patterns).toContain(path.join(outerTmp.path, "external", "*")) + expect(extDirReq!.patterns).toContain(path.join(outerTmp.path, "external", "*").replaceAll("\\", "/")) }, }) }) diff --git a/packages/opencode/test/util/wildcard.test.ts b/packages/opencode/test/util/wildcard.test.ts index 9cd0e9b94..56e753d12 100644 --- a/packages/opencode/test/util/wildcard.test.ts +++ b/packages/opencode/test/util/wildcard.test.ts @@ -73,3 +73,18 @@ test("allStructured handles sed flags", () => { expect(Wildcard.allStructured({ head: "sed", tail: ["-n", "1p", "file"] }, rules)).toBe("allow") expect(Wildcard.allStructured({ head: "sed", tail: ["-i", "-n", "/./p", "myfile.txt"] }, rules)).toBe("ask") }) + +test("match normalizes slashes for cross-platform globbing", () => { + expect(Wildcard.match("C:\\Windows\\System32\\*", "C:/Windows/System32/*")).toBe(true) + expect(Wildcard.match("C:/Windows/System32/drivers", "C:\\Windows\\System32\\*")).toBe(true) +}) + +test("match handles case-insensitivity on Windows", () => { + if (process.platform === "win32") { + expect(Wildcard.match("C:\\windows\\system32\\hosts", "C:/Windows/System32/*")).toBe(true) + expect(Wildcard.match("c:/windows/system32/hosts", "C:\\Windows\\System32\\*")).toBe(true) + } else { + // Unix paths are case-sensitive + expect(Wildcard.match("/users/test/file", "/Users/test/*")).toBe(false) + } +})