diff --git a/src/lib/onboard/host-mount/host-mount.test.ts b/src/lib/onboard/host-mount/host-mount.test.ts index bb45600b549..a2abe4a216e 100644 --- a/src/lib/onboard/host-mount/host-mount.test.ts +++ b/src/lib/onboard/host-mount/host-mount.test.ts @@ -45,7 +45,7 @@ describe("read-only host mount validation", () => { }); it.each(["\u001b[31m", "\u202e", "\u2028", "\u2029"])( - "rejects missing, relative, symlinked, non-normalized, and terminal-control paths [%s]", + "rejects missing, unreadable, relative, symlinked, non-normalized, and terminal-control paths [%s]", (terminalControl) => { const source = workspaceTempDir(); const symlink = `${source}-link`; @@ -56,7 +56,7 @@ describe("read-only host mount validation", () => { "host directory must be an absolute path", ); expect(() => parseReadOnlyHostMount(`${source}-missing:/sandbox/project`)).toThrow( - "does not exist", + "no such file or directory", ); expect(() => parseReadOnlyHostMount(`${symlink}:/sandbox/project`)).toThrow( "must not contain symlinks", @@ -65,6 +65,19 @@ describe("read-only host mount validation", () => { "normalized absolute path below /sandbox", ); + const file = path.join(source, "alpha.txt"); + fs.writeFileSync(file, "test"); + let unreadable: unknown; + try { + parseReadOnlyHostMount(`${file}/child:/sandbox/project`); + } catch (error) { + unreadable = error; + } + expect((unreadable as NodeJS.ErrnoException | undefined)?.cause).toMatchObject({ + code: "ENOTDIR", + }); + expect(unreadable).toMatchObject({ message: expect.stringContaining("ENOTDIR") }); + let message = ""; try { parseReadOnlyHostMount(`${source}:/sandbox/project${terminalControl}forged`); @@ -108,7 +121,7 @@ describe("read-only host mount validation", () => { created.splice(created.indexOf(source), 1); expect(() => normalizePersistedSandboxHostMounts([{ source, target: "/sandbox/project", readOnly: true }]), - ).toThrow("host directory does not exist"); + ).toThrow("no such file or directory"); }); it("revalidates and compares unordered declarations before sandbox reuse", () => { diff --git a/src/lib/state/registry/host-mount.ts b/src/lib/state/registry/host-mount.ts index 0fe40f31485..2d81a5ab671 100644 --- a/src/lib/state/registry/host-mount.ts +++ b/src/lib/state/registry/host-mount.ts @@ -13,8 +13,19 @@ export function hasUnsafeHostMountTerminalText(value: string): boolean { return UNSAFE_TERMINAL_TEXT.test(value); } -function failHostMount(value: string, detail: string): never { - throw new Error(`Invalid --host-mount '${value}': ${detail}`); +function failHostMount(value: string, detail: string, cause?: unknown): never { + throw new Error( + `Invalid --host-mount '${value}': ${detail}`, + cause === undefined ? undefined : { cause }, + ); +} + +function failUnreadableHostMountSource(value: string, source: string, error: unknown): never { + failHostMount( + value, + `host directory is not usable: ${source}. ${error instanceof Error ? error.message : String(error)}`, + error, + ); } function assertNoSymlinkComponents(source: string, original: string): void { @@ -25,8 +36,8 @@ function assertNoSymlinkComponents(source: string, original: string): void { let stat: fs.Stats; try { stat = fs.lstatSync(current); - } catch { - failHostMount(original, `host directory does not exist: ${source}`); + } catch (error) { + failUnreadableHostMountSource(original, source, error); } if (stat.isSymbolicLink()) { failHostMount(original, `host path must not contain symlinks: ${current}`); @@ -52,8 +63,8 @@ export function parseReadOnlyHostMount(value: string): SandboxHostMount { let sourceStat: fs.BigIntStats; try { sourceStat = fs.statSync(source, { bigint: true }); - } catch { - failHostMount(value, `host directory does not exist: ${source}`); + } catch (error) { + failUnreadableHostMountSource(value, source, error); } if (!sourceStat.isDirectory()) { failHostMount(value, `host path must be a directory: ${source}`);