fix(runner): mirror production context in direct evals

This commit is contained in:
Dotta 2026-09-06 10:56:27 -05:00
parent ef53c45b29
commit 75289b06b1
5 changed files with 150 additions and 15 deletions

View File

@ -50,6 +50,12 @@ versions and patched ACP server bytes. Do not replace this step with a fresh
`patchedDependencies`, so the resulting ACPX executables no longer match their
qualified digests.
The direct eval CLI also materializes a minimal immutable native runtime
context in each isolated attempt workspace. This keeps the direct layer on the
same `paperclip.native-execution-input.v3` contract as production, including
the AgentCore HarnessSkill upload path, without borrowing any browser E2E
setup.
`all` is intentionally literal. A disabled driver, missing remote profile, or
unavailable provider is retained as an infrastructure result; it is not
silently omitted. In particular, the ACPX Pi roster remains visible while Pi

View File

@ -1,5 +1,10 @@
import { chmod, mkdtemp, readFile, rm, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { parseNativeRuntimeContext } from "../contracts/runtime-context.js";
import type { CapabilityLiveSessionSnapshot } from "../live/live-session.js";
import {
evalSessionUsage,
@ -8,6 +13,7 @@ import {
import {
boundedEvalSessionUsage,
evalSessionProviderVersion,
prepareEvalRuntimeContext,
} from "./eval-session.js";
function request(overrides: Record<string, unknown> = {}): unknown {
@ -55,6 +61,30 @@ function agentCoreProfile(overrides: Record<string, unknown> = {}) {
}
describe("eval-session request contract", () => {
it("materializes a production-v3 runtime context for direct live providers", async () => {
const workspace = await mkdtemp(join(tmpdir(), "paperclip-eval-context-"));
let instructionRoot: string | null = null;
try {
const context = await prepareEvalRuntimeContext(workspace);
instructionRoot = context.instructions.bundle.rootPath;
expect(parseNativeRuntimeContext(context)).toEqual(context);
expect(context.skills).toEqual([]);
expect(context.mcp.assignmentSetId).toBe("paperclip-runner-direct-eval-v1");
expect(context.instructions.entryPath).toBe("AGENTS.md");
expect(await readFile(
join(context.instructions.bundle.rootPath, "AGENTS.md"),
"utf8",
)).toContain("Paperclip direct live evaluation");
expect((await stat(context.instructions.bundle.rootPath)).mode & 0o777)
.toBe(0o555);
} finally {
if (instructionRoot !== null) {
await chmod(instructionRoot, 0o700).catch(() => undefined);
}
await rm(workspace, { recursive: true, force: true });
}
});
it("normalizes the current local live-session provider contract", () => {
expect(parseEvalSessionRequest(request())).toMatchObject({
provider: "codex",

View File

@ -1,9 +1,18 @@
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { chmod, mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import {
NATIVE_RUNTIME_ASSET_SCHEMA,
PAPERCLIP_EXECUTION_PROMPT,
PAPERCLIP_EXECUTION_PROMPT_REVISION,
canonicalNativeRuntimeContextDigest,
nativeRuntimePromptDigest,
parseNativeRuntimeContext,
type NativeRuntimeContextSnapshot,
} from "../contracts/runtime-context.js";
import { projectCapabilityDevtools } from "../devtools/index.js";
import { resolveQualifiedAcpxProfile } from "../drivers/acpx/qualified-profiles.js";
import { PAPERCLIP_RUNNER_BUILD_METADATA } from "../evals/build-metadata.js";
@ -53,6 +62,85 @@ async function sha256(path: string): Promise<string> {
return createHash("sha256").update(await readFile(path)).digest("hex");
}
const EVAL_RUNTIME_INSTRUCTIONS = [
"# Paperclip direct live evaluation",
"",
"Use the provided Paperclip semantic tools to inspect and act on the assigned task.",
"Treat the seeded control-plane state as authoritative and keep every action within the requested scope.",
"",
].join("\n");
/** Materializes the minimal immutable v3 context used by production-native providers. */
export async function prepareEvalRuntimeContext(
workingDirectory: string,
): Promise<NativeRuntimeContextSnapshot> {
const contextRoot = await mkdtemp(
resolve(workingDirectory, ".paperclip-eval-runtime-context-"),
);
const instructionRoot = resolve(contextRoot, "instructions");
const entryPath = "AGENTS.md";
const entry = Buffer.from(EVAL_RUNTIME_INSTRUCTIONS);
const entryDigest = createHash("sha256").update(entry).digest("hex");
const manifestFiles = [{
path: entryPath,
sha256: entryDigest,
mode: 0o444,
size: entry.byteLength,
}];
const assetDigest = createHash("sha256")
.update(JSON.stringify(manifestFiles))
.digest("hex");
const manifestText = `${JSON.stringify({
schema: "paperclip.runtime-asset-manifest.v1",
digest: assetDigest,
fileCount: manifestFiles.length,
totalBytes: entry.byteLength,
files: manifestFiles,
})}\n`;
const manifestDigest = createHash("sha256")
.update(manifestText)
.digest("hex");
await mkdir(instructionRoot, { recursive: true, mode: 0o700 });
const entryFile = resolve(instructionRoot, entryPath);
await writeFile(entryFile, entry, { flag: "wx", mode: 0o444 });
await chmod(entryFile, 0o444);
await chmod(instructionRoot, 0o555);
const semanticCatalogDigest =
PAPERCLIP_RUNNER_BUILD_METADATA.semanticCatalog.sha256.replace(
/^sha256:/,
"",
);
const context = {
prompt: {
revision: PAPERCLIP_EXECUTION_PROMPT_REVISION,
text: PAPERCLIP_EXECUTION_PROMPT,
digest: nativeRuntimePromptDigest(),
},
instructions: {
entryPath,
bundle: {
schema: NATIVE_RUNTIME_ASSET_SCHEMA,
digest: assetDigest,
manifestDigest,
rootPath: instructionRoot,
fileCount: 1,
totalBytes: entry.byteLength,
},
},
skills: [],
mcp: {
assignmentSetId: "paperclip-runner-direct-eval-v1",
digest: semanticCatalogDigest,
bindingId: null,
},
} satisfies Omit<NativeRuntimeContextSnapshot, "aggregateDigest">;
return parseNativeRuntimeContext({
...context,
aggregateDigest: canonicalNativeRuntimeContextDigest(context),
});
}
export function evalSessionProviderVersion(
request: EvalSessionRequest,
): string | null {
@ -189,10 +277,14 @@ export async function runEvalSessionCli(
const requestedDriver = request.driver ??
expectedEvalSessionDriver(requestedProvider);
const requestedProviderVersion = evalSessionProviderVersion(request);
const runtimeContext = await prepareEvalRuntimeContext(
resolve(request.session.workingDirectory ?? process.cwd()),
);
const service = options.serviceFactory?.(runnerdPath) ??
new CapabilityLiveSessionService({
transportOptions: {
runnerBinary: runnerdPath,
runtimeContext,
// The transport performs the provider-specific allowlisting. Supplying
// the source environment here is still required: without it the
// isolated Codex home has no credential source and runnerd receives no

View File

@ -635,11 +635,17 @@ it("derives the ACPX package authority only from the verified dist/cli layout",
});
it("keeps a deployed ACPX package inside its pnpm-owned dependency root", async () => {
const deployedPackageRoot = await mkdtemp(
join(tmpdir(), "paperclip-deployed-provider-package-"),
const deploymentRoot = await mkdtemp(
join(tmpdir(), "paperclip-deployed-provider-root-"),
);
const deployedPackageRoot = join(
deploymentRoot,
"node_modules",
"@paperclipai",
"paperclip-runner",
);
await mkdir(join(deployedPackageRoot, "dist", "cli"), { recursive: true });
await mkdir(join(deployedPackageRoot, "node_modules", ".pnpm"), {
await mkdir(join(deploymentRoot, "node_modules", ".pnpm"), {
recursive: true,
});
try {
@ -654,11 +660,11 @@ it("keeps a deployed ACPX package inside its pnpm-owned dependency root", async
deployedPackageRoot,
),
).toEqual({
root: deployedPackageRoot,
root: deploymentRoot,
manifest: join(deployedPackageRoot, "package.json"),
});
} finally {
await rm(deployedPackageRoot, { recursive: true, force: true });
await rm(deploymentRoot, { recursive: true, force: true });
}
});

View File

@ -1567,17 +1567,18 @@ function acpxProviderPackageAuthority(
);
}
const sidecarPackageRoot = resolve(cliDirectory, "../..");
// A local source build consumes pnpm's workspace-owned node_modules tree.
// A deployed provider pack owns a closed node_modules tree at its own root.
// Source builds resolve provider dependencies from the monorepo root. A
// `pnpm deploy`, however, owns a complete virtual store below the deployed
// package root and must not escape to its caller's filesystem. This marker
// is generated by pnpm itself and keeps the two layouts unambiguous.
// A local source build lives at <workspace>/packages/paperclip-runner and
// resolves dependencies from <workspace>/node_modules. A `pnpm deploy`
// package lives at <deploy>/node_modules/@paperclipai/paperclip-runner,
// beside <deploy>/node_modules/.pnpm. The provider verifier receives the
// directory that owns node_modules, so step out one more level only for the
// deployed shape.
const sourceDependencyRoot = resolve(ownerPackageRoot, "../..");
const localDependencyRoot = existsSync(
resolve(ownerPackageRoot, "node_modules", ".pnpm"),
resolve(sourceDependencyRoot, ".pnpm"),
)
? ownerPackageRoot
: resolve(ownerPackageRoot, "../..");
? resolve(sourceDependencyRoot, "..")
: sourceDependencyRoot;
return sidecarPackageRoot === ownerPackageRoot
? {
root: localDependencyRoot,