diff --git a/src/roots.test.ts b/src/roots.test.ts index 90bdaa243..c8fe90f74 100644 --- a/src/roots.test.ts +++ b/src/roots.test.ts @@ -20,9 +20,20 @@ assert.equal( resolve(home, "personal", "devspace"), ); +// Home expansion applies before working-directory resolution, so a `~` path +// resolves against the home directory rather than becoming a literal `~` +// directory inside the workspace. assert.equal( - resolveAllowedPath("~/file.txt", "/workspace", ["/workspace"]), - resolve("/workspace", "~/file.txt"), + resolveAllowedPath("~/personal/devspace", "/workspace", [join(home, "personal")]), + resolve(home, "personal", "devspace"), +); + +// A `~` path outside the allowed roots is denied instead of being silently +// mapped inside the workspace. Skill reads rely on this: the denial lets the +// read fall through to the skill-path resolver. +assert.throws( + () => resolveAllowedPath("~/file.txt", "/workspace", ["/workspace"]), + /Path is outside allowed roots/, ); if (process.platform === "win32") { diff --git a/src/roots.ts b/src/roots.ts index 284726f49..c681f1cc3 100644 --- a/src/roots.ts +++ b/src/roots.ts @@ -42,7 +42,7 @@ export function assertAllowedPath(path: string, allowedRoots: string[]): string } export function resolveAllowedPath(inputPath: string, cwd: string, allowedRoots: string[]): string { - const absolutePath = resolve(cwd, inputPath); + const absolutePath = resolve(cwd, expandHomePath(inputPath)); return assertAllowedPath(absolutePath, allowedRoots); }