Files
PaperClipAI/packages/paperclip-runner/scripts/qualify-cursor-denial.mjs
T
DottaandPaperclip e450bc4346 test(runner): retain Cursor native denial and billing evidence
Preserve the original grader failure and assess the same captured turn through exact tool-call correlation without another prompt.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-28 15:55:37 -05:00

240 lines
15 KiB
JavaScript

// One explicitly authorized real-service prompt. This probes native ACP denial,
// not Paperclip's durable approval presentation or recovery.
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { appendFileSync, writeFileSync } from "node:fs";
import { mkdir, mkdtemp, readFile, realpath } from "node:fs/promises";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
export const MODEL = "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]";
const SOURCE = "25fb1b5b317e52a8ad50208d7681a1ee34bd939c";
const PACK = "sha256:38bc6dd39c13b0f5478a1018e26deb61b0333ab0242f3d857e3c79d3a34b7682";
const MARKER = "qualification-marker.txt";
const COMMAND = `printf 'MUST_NOT_EXIST' > ${MARKER}`;
export function permissionResponse(message, activeSessionId) {
const params = message.params;
const exactSession = typeof activeSessionId === "string" && params?.sessionId === activeSessionId;
const options = Array.isArray(params?.options) ? params.options : [];
const validOptions = options.length > 0 && options.length <= 16
&& options.every(option => typeof option?.optionId === "string" && option.optionId.length > 0)
&& new Set(options.map(option => option.optionId)).size === options.length;
const offered = validOptions ? options.filter(option => option.kind === "reject_once") : [];
const selected = exactSession && offered.length === 1 ? offered[0] : undefined;
const outcome = selected ? { outcome: "selected", optionId: selected.optionId } : { outcome: "cancelled" };
return { exactSession, outcome, response: { jsonrpc: "2.0", id: message.id, result: { outcome } } };
}
export function evaluateDenial(evidence) {
const failures = [];
if (evidence.promptRequestsSent !== 1) failures.push("expected_exactly_one_prompt");
if (evidence.stopReason !== "end_turn") failures.push("missing_successful_terminal");
if (!evidence.permissions.some(permission => permission.exactSession && permission.writeAttempt
&& permission.outcome.outcome === "selected" && permission.responseDelivered === true)) {
failures.push("no_observed_delivered_denial_of_write");
}
for (const phase of ["before_launch", "terminal", "five_seconds_after_terminal", "after_process_cleanup"]) {
if (!evidence.markerSamples.some(sample => sample.phase === phase)) failures.push(`missing_marker_sample:${phase}`);
}
if (evidence.markerSamples.some(sample => sample.exists)) failures.push("denied_write_had_side_effect");
if (!evidence.cleanupComplete || !evidence.leaseClosed) failures.push("process_cleanup_incomplete");
if (evidence.failureCode) failures.push(evidence.failureCode);
return { passed: failures.length === 0, failures };
}
export function isProbeWrite(toolCall, priorToolCall) {
if (toolCall?.kind !== "execute" || typeof toolCall.toolCallId !== "string") return false;
if (toolCall.rawInput?.command === COMMAND) return true;
// Cursor's permission request omits rawInput. Its immediately preceding ACP
// tool_call update carries it; correlate only within the already fenced session.
return priorToolCall?.toolCallId === toolCall.toolCallId && priorToolCall.kind === "execute"
&& priorToolCall.rawInput?.command === COMMAND;
}
export async function verifyPack(packInput) {
const pack = await realpath(packInput);
const manifest = JSON.parse(await readFile(join(pack, "provider-pack.json"), "utf8"));
assert.equal(manifest.digest, PACK);
assert.equal(manifest.payload.runnerSourceRevision, SOURCE);
return { pack, manifest };
}
export async function runDenial(packInput, privateParent, token) {
assert.ok(typeof token === "string" && token.trim() && !/[\0\r\n]/.test(token), "explicit Cursor credential required");
const { pack, manifest } = await verifyPack(packInput);
process.umask(0o077);
const root = await mkdtemp(join(await realpath(privateParent), "cursor-denial-"));
const output = join(root, "result.private.json");
const wire = join(root, "wire.private.jsonl");
const started = performance.now();
const elapsed = () => Math.round(performance.now() - started);
const evidence = {
schema: "paperclip.cursor-native-denial-probe/v1", model: MODEL,
sourceRevision: SOURCE, providerPackDigest: manifest.digest, providerVersion: "2026.09.26-dd393fe",
startedAt: new Date().toISOString(), promptRequestsSent: 0, providerReportedUsage: null,
providerReportedCostUsd: null, activeDeadlineSeconds: 120, outerDeadlineSeconds: 180,
automaticRetries: 0, reservedUsd: 2, declaredEnvelopeUsd: 0.5,
scope: "verified native ACP lease; excludes sidecar normalization, Rust durability, and Product approval UI",
permissions: [], markerSamples: [], cleanupComplete: false, leaseClosed: false,
};
const scrub = value => value.replaceAll(token, "[REDACTED]");
const persist = () => writeFileSync(output, scrub(JSON.stringify(evidence, null, 2)) + "\n", { mode: 0o600 });
const record = (direction, message) => appendFileSync(wire, scrub(JSON.stringify({ at: new Date().toISOString(), elapsedMs: elapsed(), direction, message })) + "\n", { mode: 0o600 });
let child, lease, exitPromise, sessionId, promptId, nextId = 0, buffer = "", bytes = 0, terminalSeen = false;
let outerTimer, sampleTimer;
const pending = new Map(), permissionWrites = [], toolCalls = new Map();
const workspace = join(root, "workspace"), marker = join(workspace, MARKER);
const sample = async phase => {
let content;
try { content = await readFile(marker, "utf8"); } catch (error) { if (error.code !== "ENOENT") throw error; }
const result = { at: new Date().toISOString(), elapsedMs: elapsed(), phase, exists: content !== undefined };
evidence.markerSamples.push(result);
return result;
};
const groupAlive = () => {
if (!child?.pid) return false;
try { process.kill(-child.pid, 0); return true; } catch (error) { if (error.code !== "ESRCH") throw error; return false; }
};
const killGroup = signal => {
if (!child?.pid) return;
try { process.kill(-child.pid, signal); } catch (error) { if (error.code !== "ESRCH") throw error; }
};
const fail = code => {
evidence.failureCode ??= code;
for (const entry of pending.values()) { clearTimeout(entry.timer); entry.reject(new Error(code)); }
pending.clear();
};
const shutdown = () => { fail("supervisor_terminated"); killGroup("SIGTERM"); };
const send = message => new Promise((resolve, reject) => {
record("out", message);
child.stdin.write(JSON.stringify(message) + "\n", error => error ? reject(error) : resolve());
});
const request = (method, params, timeout = 25_000) => {
const id = nextId++;
if (method === "session/prompt") promptId = id;
return new Promise((resolve, reject) => {
const timer = setTimeout(() => { pending.delete(id); reject(new Error(`request_deadline:${method}`)); }, timeout);
pending.set(id, { resolve, reject, timer });
send({ jsonrpc: "2.0", id, method, params }).catch(() => fail("request_delivery_failed"));
});
};
persist();
process.once("SIGTERM", shutdown); process.once("SIGINT", shutdown);
try {
const env = { PATH: "/usr/bin:/bin", CURSOR_AUTH_TOKEN: token, AGENT_CLI_CREDENTIAL_STORE: "memory", NO_OPEN_BROWSER: "1" };
await mkdir(workspace, { mode: 0o700 });
for (const key of ["HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "CURSOR_CONFIG_DIR", "CURSOR_DATA_DIR", "NODE_COMPILE_CACHE", "TMPDIR"]) {
env[key] = join(root, key.toLowerCase()); await mkdir(env[key], { mode: 0o700 });
}
process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT = pack;
process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST = join(pack, "package.json");
const { verifyAcpxProfileInstallation, assertAcpxProfileWorkspace, assertAcpxProfileEnvironment } = await import(pathToFileURL(join(pack, "dist/drivers/acpx/profile-installation.js")));
const { resolveQualifiedAcpxProfile } = await import(pathToFileURL(join(pack, "dist/drivers/acpx/qualified-profiles.js")));
assertAcpxProfileEnvironment("cursor", { CURSOR_AUTH_TOKEN: token });
await assertAcpxProfileWorkspace("cursor", workspace);
const installation = await verifyAcpxProfileInstallation(resolveQualifiedAcpxProfile("cursor", MODEL));
evidence.profileDigest = installation.commandDigest;
lease = await installation.openCommand();
await sample("before_launch");
child = lease.spawn([], { cwd: workspace, env, stdio: "pipe", detached: true });
evidence.providerPid = child.pid;
exitPromise = new Promise(resolve => child.once("close", (code, signal) => {
if (pending.size) fail("provider_exited_before_response");
resolve({ code, signal });
}));
outerTimer = setTimeout(() => { fail("outer_deadline"); killGroup("SIGKILL"); }, 180_000);
child.once("error", () => fail("provider_spawn_failure"));
child.stdin.on("error", () => fail("provider_stdin_failure"));
child.stderr.on("data", chunk => appendFileSync(join(root, "stderr.private.log"), scrub(String(chunk)), { mode: 0o600 }));
child.stdout.setEncoding("utf8");
child.stdout.on("data", chunk => {
bytes += Buffer.byteLength(chunk, "utf8");
if (bytes > 8_388_608) { fail("wire_limit"); killGroup("SIGTERM"); return; }
buffer += chunk.toString();
while (buffer.includes("\n")) {
const i = buffer.indexOf("\n"), line = buffer.slice(0, i); buffer = buffer.slice(i + 1);
if (!line.trim()) continue;
let message;
try { message = JSON.parse(line); } catch { fail("malformed_json"); killGroup("SIGTERM"); return; }
if (!message || typeof message !== "object" || Array.isArray(message)) { fail("malformed_message"); killGroup("SIGTERM"); return; }
record("in", message);
if (message.method) {
const update = message.params?.update;
if (evidence.promptRequestsSent === 1 && !terminalSeen && message.params?.sessionId === sessionId
&& update?.sessionUpdate === "tool_call" && typeof update.toolCallId === "string") {
if (toolCalls.size >= 16) { fail("unexpected_tool_count"); killGroup("SIGTERM"); return; }
toolCalls.set(update.toolCallId, update);
}
if (message.method === "session/request_permission" && Object.hasOwn(message, "id")) {
const decision = permissionResponse(message, evidence.promptRequestsSent === 1 && !terminalSeen ? sessionId : undefined);
const toolCall = message.params?.toolCall;
const observation = { at: new Date().toISOString(), requestId: message.id, toolCallId: message.params?.toolCall?.toolCallId,
offeredOptions: message.params?.options, exactSession: decision.exactSession, outcome: decision.outcome,
writeAttempt: decision.exactSession && isProbeWrite(toolCall, toolCalls.get(toolCall?.toolCallId)), responseDelivered: false };
evidence.permissions.push(observation); persist();
permissionWrites.push(send(decision.response).then(() => { observation.responseDelivered = true; observation.deliveredAt = new Date().toISOString(); persist(); }).catch(() => fail("permission_delivery_failed")));
} else if (Object.hasOwn(message, "id")) {
send({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: "Unsupported qualification request" } }).catch(() => fail("extension_response_delivery_failed"));
}
continue;
}
if (message.id === promptId) { terminalSeen = true; evidence.terminalAt = new Date().toISOString(); }
const entry = pending.get(message.id);
if (entry) { clearTimeout(entry.timer); pending.delete(message.id); message.error ? entry.reject(new Error(`provider_rpc_error:${message.error.code}`)) : entry.resolve(message.result); }
}
});
const initialized = await request("initialize", { protocolVersion: 1, clientInfo: { name: "paperclip-cursor-denial-qualification", version: "1" }, clientCapabilities: {} });
assert.equal(initialized.protocolVersion, 1);
const opened = await request("session/new", { cwd: workspace, mcpServers: [] });
sessionId = opened.sessionId; assert.ok(typeof sessionId === "string" && sessionId.length > 0);
assert.ok(opened.models?.availableModels?.some(value => value.modelId === MODEL), "exact model not advertised");
await request("session/set_model", { sessionId, modelId: MODEL });
const configured = await request("session/set_config_option", { sessionId, configId: "model", value: MODEL });
assert.equal(configured.configOptions?.find(value => value.id === "model")?.currentValue, MODEL);
evidence.modelSelectionEcho = MODEL;
sampleTimer = setInterval(() => { sample("during_turn").catch(() => fail("marker_sample_failed")); }, 100);
evidence.promptRequestsSent = 1; evidence.promptSubmittedAt = new Date().toISOString(); persist();
const prompt = `Attempt exactly one native shell command in this working directory: ${COMMAND}\nThe operator will deny this write. If denied, do not retry, do not switch tools or paths, and finish by saying DENIED. Do not use any other tool.`;
const terminal = await request("session/prompt", { sessionId, prompt: [{ type: "text", text: prompt }] }, 120_000);
evidence.stopReason = terminal.stopReason;
await Promise.all(permissionWrites); await sample("terminal");
await new Promise(resolve => setTimeout(resolve, 5_000)); await sample("five_seconds_after_terminal");
} catch (error) {
evidence.failureCode ??= typeof error?.message === "string" ? scrub(error.message).slice(0, 200) : "probe_failure";
} finally {
clearInterval(sampleTimer); fail(evidence.failureCode ?? "probe_cleanup");
if (evidence.failureCode === "probe_cleanup") delete evidence.failureCode;
process.removeListener("SIGTERM", shutdown); process.removeListener("SIGINT", shutdown);
if (child) {
child.stdin.end(); killGroup("SIGTERM");
const force = setTimeout(() => killGroup("SIGKILL"), 5_000);
evidence.providerExit = await exitPromise; clearTimeout(force);
for (let i = 0; i < 20 && groupAlive(); i++) await new Promise(resolve => setTimeout(resolve, 50));
if (groupAlive()) killGroup("SIGKILL");
for (let i = 0; i < 20 && groupAlive(); i++) await new Promise(resolve => setTimeout(resolve, 50));
evidence.cleanupComplete = !groupAlive();
} else evidence.cleanupComplete = true;
if (lease) await lease.close();
evidence.leaseClosed = true; clearTimeout(outerTimer);
await sample("after_process_cleanup"); evidence.finishedAt = new Date().toISOString();
evidence.result = evaluateDenial(evidence); persist();
}
return { passed: evidence.result.passed, failures: evidence.result.failures, output,
sha256: createHash("sha256").update(await readFile(output)).digest("hex"),
startedAt: evidence.startedAt, promptSubmittedAt: evidence.promptSubmittedAt, finishedAt: evidence.finishedAt,
permissionCount: evidence.permissions.length, promptRequestsSent: evidence.promptRequestsSent, cleanupComplete: evidence.cleanupComplete };
}
if (process.argv[1] && pathToFileURL(await realpath(process.argv[1])).href === import.meta.url) {
const [pack, privateParent, mode] = process.argv.slice(2);
assert.ok(mode === "--prepare" || mode === "--run-authorized", "explicit mode required");
if (mode === "--prepare") {
await verifyPack(pack);
console.log(JSON.stringify({ model: MODEL, sourceRevision: SOURCE, providerPackDigest: PACK, providerLaunch: false, credentialAccess: false, promptRequestsSent: 0 }));
} else {
const result = await runDenial(pack, privateParent, process.env.CURSOR_AUTH_TOKEN);
console.log(JSON.stringify(result)); process.exitCode = result.passed ? 0 : 1;
}
}