diff --git a/packages/paperclip-runner/scripts/controlled-evals/controller.test.mjs b/packages/paperclip-runner/scripts/controlled-evals/controller.test.mjs index bceb4a58568..92ed44792c9 100644 --- a/packages/paperclip-runner/scripts/controlled-evals/controller.test.mjs +++ b/packages/paperclip-runner/scripts/controlled-evals/controller.test.mjs @@ -237,3 +237,16 @@ for (const replaced of ['runnerCli', 'runnerd']) test(`launches verified bytes a assert.equal((await summarizeControlledCampaign(f.manifest)).incompleteAttempts, 0); await assert.rejects(readdir(join(attemptDirectory(f.manifest, 'attempt-0'), 'executables')), { code: 'ENOENT' }); }); + +test('staged CLI relative imports survive a symlinked evidence directory', async t => { + const f = await fixture(t, { requests: 1 }); + await writeFile(join(f.root, 'helper.mjs'), "export const marker = 'trusted sibling';\n"); + await writeFile(f.manifest.runnerCli.path, "import { marker } from './helper.mjs';\n" + childScript({ requests: 1 }) + "\nassert.equal(marker, 'trusted sibling');\n"); + f.manifest.runnerCli.sha256 = sha256(await readFile(f.manifest.runnerCli.path)); + const alias = join(f.root, 'evidence-alias'); + await mkdir(join(f.root, 'evidence-real')); await symlink(join(f.root, 'evidence-real'), alias); + f.manifest.outputDirectory = join(alias, 'controller'); await f.save(); + const result = await f.run(); + assert.equal(result.code, 0); + assert.equal((await summarizeControlledCampaign(f.manifest)).incompleteAttempts, 0); +}); diff --git a/packages/paperclip-runner/scripts/controlled-evals/run.mjs b/packages/paperclip-runner/scripts/controlled-evals/run.mjs index fc9fa6d217e..07740fe3a29 100644 --- a/packages/paperclip-runner/scripts/controlled-evals/run.mjs +++ b/packages/paperclip-runner/scripts/controlled-evals/run.mjs @@ -57,6 +57,7 @@ export async function runControlledEval({ manifestPath, requestPath, outputPath, const suffix = relative(resolve(request.session.workingDirectory), resolve(manifest.outputDirectory)); if (!suffix || suffix !== '..' && !suffix.startsWith(`..${sep}`) && !isAbsolute(suffix)) throw new Error('Controller evidence must be outside the disposable workspace'); } + const originalCliUrl = pathToFileURL(await realpath(manifest.runnerCli.path)).href; const verified = {}; for (const name of ['runnerCli', 'runnerPackage', 'runnerd']) { const artifact = manifest[name], bytes = await readFile(artifact.path); @@ -86,7 +87,7 @@ export async function runControlledEval({ manifestPath, requestPath, outputPath, const stagedCli = join(executables, 'entry.mjs'), stagedRunnerd = join(executables, 'runnerd'), loader = join(executables, 'imports.mjs'); await writeFile(stagedCli, verified.runnerCli, { flag: 'wx', mode: 0o400 }); await writeFile(stagedRunnerd, verified.runnerd, { flag: 'wx', mode: 0o500 }); - const stagedUrl = JSON.stringify(pathToFileURL(stagedCli).href), originalUrl = JSON.stringify(pathToFileURL(manifest.runnerCli.path).href); + const stagedUrl = JSON.stringify(pathToFileURL(await realpath(stagedCli)).href), originalUrl = JSON.stringify(originalCliUrl); await writeFile(loader, `import { registerHooks } from 'node:module'; registerHooks({ resolve(specifier, context, nextResolve) { return nextResolve(specifier, context.parentURL === ${stagedUrl} ? { ...context, parentURL: ${originalUrl} } : context);