diff --git a/src/ida/IdaWorkspace.ts b/src/ida/IdaWorkspace.ts index 38e0c5d46..009754d35 100644 --- a/src/ida/IdaWorkspace.ts +++ b/src/ida/IdaWorkspace.ts @@ -1,6 +1,6 @@ import { createHash, randomUUID } from "node:crypto"; import { createReadStream, constants } from "node:fs"; -import { copyFile } from "node:fs/promises"; +import { copyFile, realpath } from "node:fs/promises"; import { tmpdir } from "node:os"; import { basename, join, normalize } from "node:path"; import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; @@ -39,8 +39,10 @@ export class IdaWorkspace { target: BinaryTarget, root = tmpdir(), ): Promise { + // Upstream reports canonical paths (macOS /var -> /private/var), so the + // private input path must already be canonical for identity checks. const runtime = await PrivateRuntimeRoot.create({ - parent: normalize(root), + parent: await realpath(normalize(root)), prefix: "rea-ida-", }); const inputPath = join(runtime.path, basename(target.path)); diff --git a/src/ida/IdaWorkspaceOwnership.test.ts b/src/ida/IdaWorkspaceOwnership.test.ts index b416b7ab0..b39c6e916 100644 --- a/src/ida/IdaWorkspaceOwnership.test.ts +++ b/src/ida/IdaWorkspaceOwnership.test.ts @@ -1,9 +1,11 @@ -import { chmod, readdir, rm } from "node:fs/promises"; +import { chmod, readdir, realpath, rm, symlink } from "node:fs/promises"; +import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { createIdaTarget, RecordingIdaMcp, } from "../../tests/fixtures/idaMcp.js"; +import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js"; import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; import { IdaSessionClient } from "./IdaSessionClient.js"; @@ -33,6 +35,26 @@ const clientFor = (root: string, target: BinaryTarget) => { }; describe("IDA workspace ownership before input preparation", () => { + it.skipIf(process.platform === "win32")( + "opens the private input through a canonical path when the workspace root is a symlink", + async () => { + const { root, target } = await createIdaTarget(); + roots.push(root); + const linked = join( + await createTestTempDirectory("rea-ida-link-"), + "root", + ); + await symlink(root, linked); + const { client, producer } = clientFor(linked, target); + expect((await client.execute("health", {})).ok).toBe(true); + const open = producer.calls.find(({ name }) => name === "idb_open"); + expect( + String(open?.args.input_path).startsWith(`${await realpath(root)}/`), + ).toBe(true); + expect((await client.close()).ok).toBe(true); + }, + ); + it("retains cleanup ownership when copying the admitted input fails", async () => { const { root, target } = await createIdaTarget(); roots.push(root);