diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 04ddf9551c..cb7e0a0b81 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -2471,7 +2471,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper const uri = tab.input.uri if (uri.scheme === "file") { const rel = path.relative(dir, uri.fsPath) - if (!rel.startsWith("..") && controller.validateAccess(uri.fsPath)) { + if (!rel.startsWith("..") && !path.isAbsolute(rel) && controller.validateAccess(uri.fsPath)) { result.add(rel.replaceAll("\\", "/")) } } @@ -2505,7 +2505,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper return undefined } const relative = path.relative(workspaceDir, fsPath) - if (relative.startsWith("..")) { + if (relative.startsWith("..") || path.isAbsolute(relative)) { return undefined } return relative diff --git a/packages/kilo-vscode/src/services/autocomplete/shims/FileIgnoreController.ts b/packages/kilo-vscode/src/services/autocomplete/shims/FileIgnoreController.ts index c314aacaff..1b8c639cb4 100644 --- a/packages/kilo-vscode/src/services/autocomplete/shims/FileIgnoreController.ts +++ b/packages/kilo-vscode/src/services/autocomplete/shims/FileIgnoreController.ts @@ -10,6 +10,11 @@ const GITIGNORE = ".gitignore" */ const SENSITIVE_PATTERNS = [".env", ".env.*"] +// Matches Windows drive-letter absolute paths (e.g. "C:/" or "c:\"). +// path.isAbsolute() on POSIX does not recognise these, so we check explicitly +// to avoid passing them to the `ignore` package which throws a RangeError. +const WINDOWS_DRIVE = /^[a-zA-Z]:[/\\]/ + function toPosix(filePath: string): string { return filePath.replace(/\\/g, "/") } @@ -87,7 +92,7 @@ export class FileIgnoreController { } const relative = path.relative(this.workspacePath, resolved) - if (!relative || relative.startsWith("..")) { + if (!relative || relative.startsWith("..") || path.isAbsolute(relative) || WINDOWS_DRIVE.test(relative)) { return null } diff --git a/packages/kilo-vscode/tests/unit/file-ignore-controller.test.ts b/packages/kilo-vscode/tests/unit/file-ignore-controller.test.ts index 5b4e313121..e590f92b36 100644 --- a/packages/kilo-vscode/tests/unit/file-ignore-controller.test.ts +++ b/packages/kilo-vscode/tests/unit/file-ignore-controller.test.ts @@ -2,8 +2,16 @@ import { afterEach, describe, expect, it } from "bun:test" import os from "node:os" import path from "node:path" import fs from "node:fs/promises" +import ignore from "ignore" import { FileIgnoreController } from "../../src/services/autocomplete/shims/FileIgnoreController" +// Activate Windows drive-letter detection in the `ignore` package. +// On actual Windows this runs automatically (process.platform === 'win32'); +// here we enable it explicitly so the test reproduces the Windows-only +// RangeError on any platform. +const setup = (ignore as any)[Symbol.for("setupWindows")] +if (typeof setup === "function") setup() + const tempDirs: string[] = [] afterEach(async () => { @@ -102,6 +110,55 @@ describe("FileIgnoreController", () => { }) }) + describe("Windows cross-drive paths", () => { + it("does not throw for a Windows-style absolute path from another drive", async () => { + const workspace = await createTempWorkspace() + await fs.writeFile(path.join(workspace, ".gitignore"), "node_modules/\n") + + const controller = new FileIgnoreController(workspace) + await controller.initialize() + + // Simulates a VS Code tab open on a file from a different Windows drive. + // On Windows, path.relative("D:\\project", "C:\\Users\\file") returns + // "C:\\Users\\file" (absolute), which the `ignore` package rejects via + // RangeError: path should be a `path.relative()`d string. + // + // On macOS, path.resolve joins "c:/..." relative to the workspace, + // producing "c:/Users/..." as the relative portion — still detected as + // a Windows drive letter by ignore's setupWindows() regex. + const cross = + "c:/Users/User/AppData/Roaming/Code/User/globalStorage/kilocode.kilo-code/settings/mcp_settings.json" + + expect(() => controller.validateAccess(cross)).not.toThrow() + expect(controller.validateAccess(cross)).toBe(false) + }) + + it("does not throw for file:// URIs with Windows drive letters", async () => { + const workspace = await createTempWorkspace() + await fs.writeFile(path.join(workspace, ".gitignore"), "node_modules/\n") + + const controller = new FileIgnoreController(workspace) + await controller.initialize() + + const uri = + "file:///c:/Users/User/AppData/Roaming/Code/User/globalStorage/kilocode.kilo-code/settings/mcp_settings.json" + + expect(() => controller.validateAccess(uri)).not.toThrow() + expect(controller.validateAccess(uri)).toBe(false) + }) + + it("still allows workspace files after cross-drive check", async () => { + const workspace = await createTempWorkspace() + await fs.writeFile(path.join(workspace, ".gitignore"), "node_modules/\n") + + const controller = new FileIgnoreController(workspace) + await controller.initialize() + + expect(controller.validateAccess(path.join(workspace, "src", "main.ts"))).toBe(true) + expect(controller.validateAccess(path.join(workspace, "node_modules", "foo.js"))).toBe(false) + }) + }) + describe("when constructed with empty workspace path", () => { it("denies all access", async () => { const controller = new FileIgnoreController("") diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index 01f07c9afa..2c181479be 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -596,12 +596,15 @@ export namespace File { const fullPath = path.join(resolved, entry.name) const relativePath = path.relative(Instance.directory, fullPath) const type = entry.isDirectory() ? "directory" : "file" + // On Windows, path.relative() across drives returns an absolute path; + // skip the gitignore check in that case to avoid a RangeError from `ignore`. + const canIgnore = !path.isAbsolute(relativePath) nodes.push({ name: entry.name, path: relativePath, absolute: fullPath, type, - ignored: ignored(type === "directory" ? relativePath + "/" : relativePath), + ignored: canIgnore && ignored(type === "directory" ? relativePath + "/" : relativePath), }) } return nodes.sort((a, b) => {