Merge branch 'codex/runner-cursor-acp' into codex/runner-copilot-acp

* codex/runner-cursor-acp:
  Admit the verified Cursor v3 identity in Rust
  Admit current ACP profiles and verify cross-language pin agreement
  Prove Cursor SDK interactions and retain profile v3 build evidence
This commit is contained in:
Dotta committed 2026-09-29 10:08:55 -05:00
commit 550801f956
8 files changed
+386 -8

No files matched your search

@@ -211,3 +211,32 @@ Prior Product/Runner results above measured the earlier unpatched runtime. The
profile must retain pending qualification until the rebuilt final foundation
runtime passes the remaining native interaction, durable approval, cancellation,
recovery, accounting, and authenticated target-platform gates.
### Frozen profile v3 build
[The retained build record](../../packages/paperclip-runner/test/fixtures/cursor-acp/production-v3-offline-build.json)
identifies exact runtime source `526081b9d6c905de5e2da680e3a37c620899b380`,
profile v3, the ARM64 portable package and matching release daemon, both lock
hashes, the committed ACPX patch hash, and credential-free launch results.
Dependencies were installed into a fresh private store with copied files to
avoid borrowing mutable package bytes from another worktree. The initial frozen
install failed because the committed lock's patch configuration differed; that
failure is retained. A private derived staging lock then passed frozen install.
The repository lock was not changed.
The package's real immutable lease passed ACP initialization, required
authentication for model/session discovery and creation, rejected unsupported
fork/resume methods, and closed with no stderr. The TypeScript and release daemon
builds passed. Twenty-six installed-package contracts passed. Another 184 control
tests passed, including two Cursor-specific tests using the real installed ACP
SDK stream: sessionless native single/multi-selection and complete revision-bound
plan replies preserve request ID zero, await response pipe delivery, reject stale
plan revisions, and suppress late answers after cancellation. The surrounding
suite covers mocked admission, permission identity, provider-loss expiry,
recovery fences, and cleanup ownership. These are deterministic tests, not live
native interaction, durable browser approval, or provider-death qualification.
The new `cursor-runtime-patch.mjs` is present in the Docker build context. It is
also a required Daytona content-identity input: changing patch source must change
the image identity even when another declared input is unchanged. Publishing or
qualification must use an image whose content hash includes that script.
@@ -178,7 +178,7 @@ impl AcpxProviderDescriptor {
"2026.09.26-dd393fe",
None,
None,
"sha256:1157a5d071abbd57ab132f22bace75c65e84cc47a045b0023475488755e14899",
"sha256:c91aa592ec867071ec4457b9b7230399ce42ad918787ceed99b4b651ea5607e2",
),
"copilot" => (
self.model.as_str(),
@@ -2722,7 +2722,7 @@ mod tests {
"cursor",
"cursor-agent",
"2026.09.26-dd393fe",
"sha256:1157a5d071abbd57ab132f22bace75c65e84cc47a045b0023475488755e14899",
"sha256:c91aa592ec867071ec4457b9b7230399ce42ad918787ceed99b4b651ea5607e2",
None,
None,
"explicit-model",
@@ -1,4 +1,5 @@
import { createHash } from "node:crypto";
import { QUALIFIED_ACPX_PROFILES } from "../drivers/acpx/qualified-profiles.js";
import { describe, expect, it } from "vitest";
import { buildNativeModelEnvelope, parseNativeExecutionInput, NATIVE_EXECUTION_INPUT_SCHEMA, type NativeExecutionInputV1 } from "./native-execution.js";
@@ -280,7 +281,7 @@ describe("NativeExecutionInputV1", () => {
})).toThrow("eventExpiryDays");
});
it.each([1, 2, 3, 4, 5] as const)("accepts only a closed ACPX profile matching the driver and agent at profile version %s", (agentProfileVersion) => {
it.each([1, 2, 3, 4, 5, 6] as const)("accepts only a closed ACPX profile matching the driver and agent at profile version %s", (agentProfileVersion) => {
const provider = {
kind: "acpx",
agent: "pi",
@@ -312,7 +313,7 @@ describe("NativeExecutionInputV1", () => {
profile: provider.profile,
});
expect(parseNativeExecutionInput(parsed)).toEqual(parsed);
for (const unsupportedVersion of [0, 6, 1.5, "5", null]) {
for (const unsupportedVersion of [0, 7, 1.5, "6", null]) {
expect(() => parseNativeExecutionInput({
...input,
session: { ...input.session, driverKind: "acpx_runtime" },
@@ -332,6 +333,26 @@ describe("NativeExecutionInputV1", () => {
})).toThrow("does not match");
});
it.each(Object.values(QUALIFIED_ACPX_PROFILES))(
"admits the current $agent profile through the native execution boundary",
(declaration) => {
const { qualificationModel, reportedModelId: _reported, permissionPolicy,
modelPolicy: _modelPolicy, qualificationStatus: _status, ...profile } = declaration;
const provider = {
kind: "acpx", agent: declaration.agent,
model: qualificationModel || "explicit-provider-model",
permissionPolicy, profile,
};
const parsed = parseNativeExecutionInput({
...input,
session: { ...input.session, driverKind: "acpx_runtime" },
provider,
});
expect(parsed.provider).toEqual(provider);
expect(parseNativeExecutionInput(parsed)).toEqual(parsed);
},
);
it("defaults legacy lifecycle state to per-turn and validates warm timeouts", () => {
const legacy = structuredClone(input) as Record<string, unknown>;
delete (legacy.session as Record<string, unknown>).lifecyclePolicy;
@@ -90,7 +90,7 @@ export interface NativeAcpxProfileSnapshot {
protocolVersion: 1;
acpxVersion: "0.13.1";
agent: NativeAcpxAgent;
agentProfileVersion: 1 | 2 | 3 | 4 | 5;
agentProfileVersion: 1 | 2 | 3 | 4 | 5 | 6;
agentServerPackage: string;
agentServerVersion: string;
agentRuntimePackage: string | null;
@@ -596,7 +596,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
|| profile.protocolVersion !== 1
|| profile.acpxVersion !== "0.13.1"
|| profile.agent !== provider.agent
|| (profile.agentProfileVersion !== 1 && profile.agentProfileVersion !== 2 && profile.agentProfileVersion !== 3 && profile.agentProfileVersion !== 4 && profile.agentProfileVersion !== 5)
|| (profile.agentProfileVersion !== 1 && profile.agentProfileVersion !== 2 && profile.agentProfileVersion !== 3 && profile.agentProfileVersion !== 4 && profile.agentProfileVersion !== 5 && profile.agentProfileVersion !== 6)
) {
throw new NativeExecutionInputError("input.provider.profile does not match the qualified ACPX v1 profile");
}
@@ -0,0 +1,76 @@
import { spawn } from "node:child_process";
import { createRequire } from "node:module";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { pathToFileURL } from "node:url";
import { expect, it } from "vitest";
import { createCursorProfileExtensionAdapter } from "./cursor-extensions.js";
const require = createRequire(import.meta.url);
const acpxRoot = dirname(require.resolve("acpx/package.json"));
const { k: AcpClient } = await import(pathToFileURL(join(acpxRoot, "dist/live-checkpoint-BSIrfgVo.js")).href);
// The fixture uses native Cursor's sessionless extension shape and request ID 0.
// The real installed ACP SDK handles the pipe; the production adapter normalizes
// answers. This is credential-free protocol coverage, not native availability.
const fixture = String.raw`
const send=value=>process.stdout.write(JSON.stringify({jsonrpc:'2.0',...value})+'\n');let prompt;
require('node:readline').createInterface({input:process.stdin}).on('line',line=>{
const m=JSON.parse(line);
if(m.method==='initialize')send({id:m.id,result:{protocolVersion:1,agentCapabilities:{},authMethods:[]}});
else if(m.method==='session/new')send({id:m.id,result:{sessionId:'cursor-session'}});
else if(m.method==='session/prompt'){
prompt=m.id;const plan=m.params.prompt[0].text==='plan';
send({id:0,method:plan?'cursor/create_plan':'cursor/ask_question',params:plan
?{toolCallId:'plan',name:'Complete revision',plan:'1. Read\n2. Verify',todos:[],phases:[]}
:{toolCallId:'question',questions:[{id:'native-single',prompt:'Color',options:[{id:'c',label:'Cobalt'},{id:'a',label:'Amber'}]},{id:'native-multi',prompt:'Trees',allowMultiple:true,options:[{id:'cedar',label:'Cedar'},{id:'maple',label:'Maple'}]}]}});
}else if(m.method==='session/cancel'){send({id:prompt,result:{stopReason:'cancelled'}});prompt=undefined;}
else if(m.id===0&&!m.method&&prompt!==undefined){
send({method:'session/update',params:{sessionId:'cursor-session',update:{sessionUpdate:'agent_message_chunk',content:{type:'text',text:JSON.stringify(m)}}}});
send({id:prompt,result:{stopReason:'end_turn'}});prompt=undefined;
}
});`;
type Context = { requestId: string | number; signal: AbortSignal; responseDelivery?: Promise<void> };
async function withClient(onRequest: (method: string, params: Record<string, unknown>, context: Context) => Promise<unknown>, run: (client: InstanceType<typeof AcpClient>, received: unknown[]) => Promise<void>) {
const cwd = await mkdtemp(join(tmpdir(), "cursor-sdk-")); const received: unknown[] = [];
const client = new AcpClient({ agentCommand: "owned-fixture", cwd, permissionMode: "approve-all", fs: false, terminal: false, authPolicy: "skip", suppressSdkConsoleErrors: true,
extensionMethods: ["cursor/ask_question", "cursor/create_plan"], onExtensionRequest: onRequest,
spawnAgent: () => spawn(process.execPath, ["-e", fixture], { cwd, env: { PATH: "/usr/bin:/bin" }, stdio: ["pipe", "pipe", "pipe"] }),
onSessionUpdate: (params: { update: { content?: { type: string; text: string } } }) => { if (params.update.content?.type === "text") received.push(JSON.parse(params.update.content.text)); },
});
try { await client.start(); await client.createSession(); await run(client, received); }
finally { await client.close(); await rm(cwd, { recursive: true, force: true }); }
}
it("roundtrips Cursor multi-select and revision-bound plan decisions through the installed ACP SDK", async () => {
const deliveries: Promise<void>[] = []; let turn = 0;
await withClient(async (method, params, context) => {
expect(context.requestId).toBe(0); expect(params).not.toHaveProperty("sessionId");
expect(context.responseDelivery).toBeInstanceOf(Promise); deliveries.push(context.responseDelivery!);
const adapter = createCursorProfileExtensionAdapter({ workspacePath: tmpdir(), sessionId: "cursor-session", turnId: `turn-${++turn}` });
const request = await adapter.request(method, { ...params, sessionId: "cursor-session" });
if (!("input" in request)) throw new Error("Expected blocking Cursor interaction");
const questionId = request.input.questionSet.questions[0]!.id;
const answers = method === "cursor/ask_question"
? { "question-1": { selectedOptionIds: ["option-1"] }, "question-2": { selectedOptionIds: ["option-1", "option-2"] } }
: { [questionId]: { selectedOptionIds: ["accept"] } };
if (method === "cursor/create_plan") {
expect(request.input.questionSet.description).toBe("1. Read\n2. Verify");
expect(() => request.input.resolve({ action: "submit", response: { schema: "paperclip.question_response.v1", answers: { "plan-stale": { selectedOptionIds: ["accept"] } } } })).toThrow();
}
return request.input.resolve({ action: "submit", response: { schema: "paperclip.question_response.v1", answers } });
}, async (client, received) => {
expect((await client.prompt("cursor-session", "question")).stopReason).toBe("end_turn");
expect((await client.prompt("cursor-session", "plan")).stopReason).toBe("end_turn"); await Promise.all(deliveries);
expect(received).toEqual([
{ jsonrpc: "2.0", id: 0, result: { outcome: { outcome: "answered", answers: [{ questionId: "native-single", selectedOptionIds: ["c"] }, { questionId: "native-multi", selectedOptionIds: ["cedar", "maple"] }] } } },
{ jsonrpc: "2.0", id: 0, result: { outcome: { outcome: "accepted" } } },
]);
});
});
it("cancels an unanswered Cursor request without delivering a late answer", async () => {
const started = Promise.withResolvers<void>(); const response = Promise.withResolvers<unknown>(); let signal: AbortSignal | undefined;
await withClient(async (_method, _params, context) => { signal = context.signal; started.resolve(); return response.promise; }, async (client, received) => {
const prompt = client.prompt("cursor-session", "question"); await started.promise; await client.cancel("cursor-session"); await prompt;
expect(signal?.aborted).toBe(true); response.resolve({ outcome: { outcome: "answered", answers: [] } });
await new Promise(resolve => setImmediate(resolve)); expect(received).toEqual([]);
});
});
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import {
QUALIFIED_ACPX_PROFILES,
@@ -6,6 +7,21 @@ import {
} from "./qualified-profiles.js";
describe("qualified ACPX profiles", () => {
it.each(Object.values(QUALIFIED_ACPX_PROFILES))(
"keeps the Rust admission digest synchronized with the current $agent profile",
(profile) => {
const rust = readFileSync(new URL("../../../runner/crates/runner-core/src/acpx_provider_backend.rs", import.meta.url), "utf8");
const start = rust.indexOf(`"${profile.agent}" => (`);
expect(start).toBeGreaterThan(0);
const end = rust.indexOf("\n ),", start);
expect(end).toBeGreaterThan(start);
const admission = rust.slice(start, end);
expect(admission).toContain(JSON.stringify(profile.agentServerPackage));
expect(admission).toContain(JSON.stringify(profile.agentServerVersion));
expect(admission).toContain(JSON.stringify(profile.commandDigest));
},
);
it("binds each agent to one immutable package and model declaration", () => {
for (const agent of ["pi", "claude", "codex", "grok"] as const) {
const profile = QUALIFIED_ACPX_PROFILES[agent];
@@ -2,7 +2,7 @@ export const QUALIFIED_ACPX_VERSION = "0.13.1" as const;
export const ACPX_DRIVER_KIND = "acpx_runtime" as const;
export const ACPX_DRIVER_PROTOCOL_VERSION = 1 as const;
import type { NativeAcpxAgent } from "../../contracts/native-execution.js";
import type { NativeAcpxAgent, NativeAcpxProfileSnapshot } from "../../contracts/native-execution.js";
export type QualifiedAcpxAgent = NativeAcpxAgent;
@@ -11,7 +11,7 @@ export interface QualifiedAcpxProfile {
readonly protocolVersion: typeof ACPX_DRIVER_PROTOCOL_VERSION;
readonly acpxVersion: typeof QUALIFIED_ACPX_VERSION;
readonly agent: QualifiedAcpxAgent;
readonly agentProfileVersion: 1 | 2 | 3 | 4 | 5;
readonly agentProfileVersion: NativeAcpxProfileSnapshot["agentProfileVersion"];
readonly qualificationStatus?: "pending";
readonly modelPolicy?: "explicit-provider-verified";
/** Wire identity: an npm package name or a runner-owned builtin: identifier. */
@@ -0,0 +1,236 @@
{
"schema": "paperclip.cursor.production-offline-build.v1",
"observedAt": "2026-09-29",
"runtimeSourceRevision": "526081b9d6c905de5e2da680e3a37c620899b380",
"providerVersion": "2026.09.26-dd393fe",
"profileVersion": 3,
"patchVersion": "paperclip-cursor-isolation-v1",
"providerPackDigest": "sha256:7166a42d06192c53191aad7b15396e291675624001fbd95dd4b3cbda197f87a0",
"candidate": {
"version": "2026.09.26-dd393fe",
"profileDigest": "sha256:c91aa592ec867071ec4457b9b7230399ce42ad918787ceed99b4b651ea5607e2",
"closureDigest": "sha256:8a22016ca832ef8c8e6a83b955208d74c63f2c8dbe1dd688cbf27b81ad01886a",
"qualification": "pending",
"path": "provider-assets/cursor/darwin-arm64",
"sha256": "sha256:e4c656debc32e8a6bf6e6fd7f620223b48ad0b8b18994096d09f056fdf9d809c"
},
"daemon": {
"sourceRevision": "526081b9d6c905de5e2da680e3a37c620899b380",
"platform": "darwin-arm64",
"sha256": "826611379af7b8148933c58c6c77826c5a4f0773b789aac9eafbbc66b8e3dcb5"
},
"dependencyProvenance": {
"source": "git archive of exact runtimeSourceRevision",
"isolation": "fresh private pnpm store; copied package files, no shared hardlinks",
"pnpmVersion": "9.15.4",
"committedLockSha256": "e0c928a494f90ddad3c00791e83f09315ee8c82df2a0418a809dcf93649a8ab3",
"stagingLockSha256": "9eea60187c6c808efb4d936a234a8d8000beac45253091abd4c5348132bd1408",
"acpxPatchSha256": "8b698d853fe6df6a34a8f89dddaccc6c8f1e5b7763af5ab5866b77acd42bc127",
"firstFrozenInstall": "failed: ERR_PNPM_LOCKFILE_CONFIG_MISMATCH",
"resolution": "retained original lock, generated private staging lock only, then frozen installation passed; repository lock unchanged"
},
"checks": {
"runnerTypescriptBuild": "passed",
"runnerReleaseDaemonBuild": "passed",
"freshPackageContracts": {
"passed": 26
},
"runtimeControlsAndCursorSdkInteractions": {
"passed": 184,
"scope": "credential-free real ACP SDK fixtures plus mocked admission, recovery, request delivery, lifecycle and cancellation; excludes native provider availability"
},
"verifiedNativeLease": {
"schema": "paperclip-cursor-offline-pack-proof/v1",
"capturedAt": "2026-09-29T14:50:08.614Z",
"runnerSourceRevision": "526081b9d6c905de5e2da680e3a37c620899b380",
"packDigest": "sha256:7166a42d06192c53191aad7b15396e291675624001fbd95dd4b3cbda197f87a0",
"target": {
"platform": "darwin",
"architecture": "arm64"
},
"candidate": {
"version": "2026.09.26-dd393fe",
"profileDigest": "sha256:c91aa592ec867071ec4457b9b7230399ce42ad918787ceed99b4b651ea5607e2",
"closureDigest": "sha256:8a22016ca832ef8c8e6a83b955208d74c63f2c8dbe1dd688cbf27b81ad01886a",
"qualification": "pending",
"path": "provider-assets/cursor/darwin-arm64",
"sha256": "sha256:e4c656debc32e8a6bf6e6fd7f620223b48ad0b8b18994096d09f056fdf9d809c"
},
"productionLock": "sha256:9eea60187c6c808efb4d936a234a8d8000beac45253091abd4c5348132bd1408",
"launch": {
"entrypoint": "verified bundled node + index.js",
"arguments": [
"--disable-project-configs",
"--disable-auto-update",
"acp"
],
"credentials": "none; private home/config/data/cache; memory credential store",
"verification": "generic profile installation registry, whole closure digest verification, retained immutable native snapshot, closed module bootstrap",
"modelAdmission": "offline-installation-probe is a factory identity sentinel only; no ACP model selection or prompt; model remains unverified"
},
"requests": [
{
"method": "initialize",
"params": {
"protocolVersion": 1,
"clientCapabilities": {
"fs": {
"readTextFile": false,
"writeTextFile": false
},
"terminal": false,
"_meta": {
"subagents": true
}
},
"clientInfo": {
"name": "paperclip-offline-pack-probe",
"version": "1"
}
}
},
{
"method": "cursor/list_available_models",
"params": {}
},
{
"method": "session/list",
"params": {}
},
{
"method": "session/fork",
"params": {
"sessionId": "probe"
}
},
{
"method": "session/resume",
"params": {
"sessionId": "probe",
"cwd": "/qualification/private/workspace",
"mcpServers": []
}
},
{
"method": "session/new",
"params": {
"cwd": "/qualification/private/workspace",
"mcpServers": []
}
}
],
"responses": [
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": 1,
"agentCapabilities": {
"loadSession": true,
"mcpCapabilities": {
"http": true,
"sse": true
},
"promptCapabilities": {
"audio": false,
"embeddedContext": false,
"image": true
},
"sessionCapabilities": {
"list": {},
"subagents": {}
}
},
"authMethods": [
{
"id": "cursor_login",
"name": "Cursor Login",
"description": "Authenticate using existing Cursor login credentials. Run 'agent login' first if not logged in."
}
]
}
},
{
"jsonrpc": "2.0",
"id": 2,
"error": {
"code": -32000,
"message": "Authentication required",
"data": {
"message": "Authentication required. Please run 'agent login' first, then call authenticate() with methodId 'cursor_login'."
}
}
},
{
"jsonrpc": "2.0",
"id": 3,
"error": {
"code": -32000,
"message": "Authentication required",
"data": {
"message": "Authentication required. Please run 'agent login' first, then call authenticate() with methodId 'cursor_login'."
}
}
},
{
"jsonrpc": "2.0",
"id": 4,
"error": {
"code": -32601,
"message": "\"Method not found\": session/fork",
"data": {
"method": "session/fork"
}
}
},
{
"jsonrpc": "2.0",
"id": 5,
"error": {
"code": -32601,
"message": "\"Method not found\": session/resume",
"data": {
"method": "session/resume"
}
}
},
{
"jsonrpc": "2.0",
"id": 6,
"error": {
"code": -32000,
"message": "Authentication required",
"data": {
"message": "Authentication required. Please run 'agent login' first, then call authenticate() with methodId 'cursor_login'."
}
}
}
],
"process": {
"exitCode": 143,
"signalCode": null,
"leaseClosed": true,
"stderrBytes": 0,
"unexpectedMessageCount": 0
},
"qualification": {
"status": "pending",
"model": null,
"billablePrompts": 0,
"costUsd": 0,
"environment": "local macOS ARM64; no Daytona execution"
},
"redactions": [
"ephemeral workspace and private directories replaced with /qualification/private"
]
}
},
"qualification": {
"status": "pending",
"paidPrompts": 0,
"authenticatedNativeInteractions": "not attempted",
"durableRestrictiveApprovals": "not qualified",
"authenticatedMacosX64": "not qualified",
"authenticatedDaytona": "not qualified"
}
}