diff --git a/tests/runner-e2e/cursor-native-cases.ts b/tests/runner-e2e/cursor-native-cases.ts index 92000539e5f..bd646bb76ed 100644 --- a/tests/runner-e2e/cursor-native-cases.ts +++ b/tests/runner-e2e/cursor-native-cases.ts @@ -81,14 +81,14 @@ export const CURSOR_DENIAL_SAMPLE_PHASES = ["before-request", "pending", "browse /** Samples are taken independently by the fixture, never supplied by the model. */ export function hasCursorDenialBoundary(input: { request: unknown; expectedRequestId: string; expectedToolCallId: string; - path: string; notices: readonly CursorToolNotice[]; runId: string; turnId: string; samples: readonly { phase: string; path: string; absent: boolean; observedAt: number }[]; + path: string; allowBootstrapReads?: boolean; notices: readonly CursorToolNotice[]; runId: string; turnId: string; samples: readonly { phase: string; path: string; absent: boolean; observedAt: number }[]; }): boolean { const request = record(input.request); const origin = record(request.origin); const details = record(request.details); if (!input.expectedRequestId.trim() || !input.expectedToolCallId.trim() || !input.path.trim() || request.requestId !== input.expectedRequestId || details.toolCallId !== input.expectedToolCallId || request.type !== "permission" || request.status !== "pending" || origin.provider !== "cursor" || origin.adapter !== "acpx-runtime" || origin.method !== "session/request_permission") return false; - if (request.turnId !== input.turnId || !hasCursorDeniedCommand({ notices: input.notices, runId: input.runId, turnId: input.turnId, requestId: input.expectedRequestId, toolCallId: input.expectedToolCallId, commandSha256: cursorDeniedCommand(input.path).commandSha256 })) return false; + if (request.turnId !== input.turnId || !hasCursorDeniedCommand({ notices: input.notices, runId: input.runId, turnId: input.turnId, requestId: input.expectedRequestId, toolCallId: input.expectedToolCallId, commandSha256: cursorDeniedCommand(input.path).commandSha256, allowBootstrapReads: input.allowBootstrapReads })) return false; const choices = request.choices; if (!Array.isArray(choices)) return false; const keys = choices.map(value => record(value).key); diff --git a/tests/runner-e2e/cursor-native-evidence.test.ts b/tests/runner-e2e/cursor-native-evidence.test.ts index a8c357b748c..b2c257c9f22 100644 --- a/tests/runner-e2e/cursor-native-evidence.test.ts +++ b/tests/runner-e2e/cursor-native-evidence.test.ts @@ -41,3 +41,52 @@ it("requires acknowledged cancellation and an actual correlated terminal after t expect(hasCursorCancellation({ ...input, turnId: "foreign" })).toBe(false); input.run.resultJson.nativeCancellation.dispatched = false; expect(hasCursorCancellation(input)).toBe(false); }); + +it("requires remote lease identity and actual Linux process retirement, not empty host observations", async () => { + const { assertCursorRemoteSnapshot, hasCursorRemoteRetirement } = await import("./cursor-native-evidence.js"); + const binding = { companyId: "company", environmentId: "environment", runId: "run", leaseId: "lease", sandboxId: "sandbox", remoteCwd: "/home/daytona/paperclip-workspace", image: `runner@sha256:${"a".repeat(64)}` }; + const root = { pid: 51, startTicks: "3021", bootId: "12345678-1234-1234-1234-123456789abc" }; + const snapshot = { binding, observedAtMs: 50, complete: true, workspace: { "file.txt": `sha256:${"b".repeat(64)}` }, targets: {}, watcher: { complete: true, targetMutationCount: 0, workspaceMutationCount: 0 }, processes: { captured: true, root, journal: [{ ...root, ppid: 1 }], live: [] as number[] } }; + expect(() => assertCursorRemoteSnapshot(snapshot, binding)).not.toThrow(); expect(hasCursorRemoteRetirement(snapshot, binding)).toBe(true); + for (const key of Object.keys(binding)) { + const changed = { ...binding, [key]: "foreign" }; expect(() => assertCursorRemoteSnapshot({ ...snapshot, binding: changed }, binding)).toThrow(/authority/); + } + expect(hasCursorRemoteRetirement({ ...snapshot, processes: { ...snapshot.processes, captured: false } }, binding)).toBe(false); + expect(hasCursorRemoteRetirement({ ...snapshot, processes: { ...snapshot.processes, live: [51] } }, binding)).toBe(false); + expect(hasCursorRemoteRetirement({ ...snapshot, processes: { ...snapshot.processes, journal: [{ ...root, ppid: 1, startTicks: "4000" }] } }, binding)).toBe(false); + expect(() => assertCursorRemoteSnapshot({ ...snapshot, workspace: { "../host-copy": "directory" } }, binding)).toThrow(/workspace/); + expect(() => assertCursorRemoteSnapshot({ ...snapshot, watcher: { complete: false, targetMutationCount: 0, workspaceMutationCount: 0 } }, binding)).toThrow(/authority/); +}); + +it("binds remote absent-target samples to stable directory identity and transient-write observation", async () => { + const { cursorRemoteDeniedSample } = await import("./cursor-native-evidence.js"); + const binding = { companyId: "company", environmentId: "environment", runId: "run", leaseId: "lease", sandboxId: "sandbox", remoteCwd: "/home/daytona/paperclip-workspace", image: `runner@sha256:${"a".repeat(64)}` }; + const target = { absent: true, sha256: null, parent: { dev: "1", ino: "2" }, mutationCount: 0, complete: true }; + const snapshot = { binding, observedAtMs: 50, complete: true, workspace: {}, targets: { "denied.txt": target }, watcher: { complete: true, targetMutationCount: 0, workspaceMutationCount: 0 }, processes: { captured: false, root: null, journal: [], live: [] } }; + expect(cursorRemoteDeniedSample(snapshot, binding, "denied.txt", "pending", target.parent)).toMatchObject({ absent: true, path: "/home/daytona/paperclip-workspace/denied.txt", phase: "pending" }); + expect(() => cursorRemoteDeniedSample({ ...snapshot, targets: {} }, binding, "denied.txt", "pending")).toThrow(/incomplete/); + expect(() => cursorRemoteDeniedSample(snapshot, binding, "denied.txt", "pending", { dev: "1", ino: "3" })).toThrow(/changed/); + expect(() => cursorRemoteDeniedSample({ ...snapshot, targets: { "denied.txt": { ...target, mutationCount: 2 } } }, binding, "denied.txt", "pending")).toThrow(/changed/); + expect(() => cursorRemoteDeniedSample(snapshot, binding, "../denied.txt", "pending")).toThrow(/Invalid/); +}); + +it("rejects transient remote workspace writes even when final hashes match", async () => { + const { hasCursorRemoteWorkspaceUnchanged } = await import("./cursor-native-evidence.js"); + const binding = { companyId: "company", environmentId: "environment", runId: "run", leaseId: "lease", sandboxId: "sandbox", remoteCwd: "/home/daytona/paperclip-workspace", image: `runner@sha256:${"a".repeat(64)}` }; + const baseline = { binding, observedAtMs: 50, complete: true, workspace: {}, targets: {}, watcher: { complete: true, targetMutationCount: 0, workspaceMutationCount: 0 }, processes: { captured: false, root: null, journal: [], live: [] } }; + expect(hasCursorRemoteWorkspaceUnchanged({ ...baseline, observedAtMs: 60 }, baseline)).toBe(true); + expect(hasCursorRemoteWorkspaceUnchanged({ ...baseline, watcher: { ...baseline.watcher, workspaceMutationCount: 2 } }, baseline)).toBe(false); + expect(hasCursorRemoteWorkspaceUnchanged({ ...baseline, workspace: { "new-file": `sha256:${"b".repeat(64)}` } }, baseline)).toBe(false); + expect(hasCursorRemoteWorkspaceUnchanged({ ...baseline, observedAtMs: 40 }, baseline)).toBe(false); +}); + +it("allows explicit earlier remote bootstrap reads while rejecting unknown kinds and later extra work", () => { + const rows = denialNotices().map(row => ({ ...row, seq: row.seq + 3 })); + const read: CursorToolNotice = { ...rows[0]!, seq: 1, toolCallId: "bootstrap-read", stage: "tool", status: "completed", operation: "read", commandSha256: undefined }; + const input = { notices: [read, ...rows], runId: "run", turnId: "turn", requestId: "request", toolCallId: "tool", commandSha256: cursorDeniedCommand("/fixture/denied.txt").commandSha256 }; + expect(hasCursorDeniedCommand(input)).toBe(false); + expect(hasCursorDeniedCommand({ ...input, allowBootstrapReads: true })).toBe(true); + for (const operation of [undefined, "edit", "execute"]) expect(hasCursorDeniedCommand({ ...input, allowBootstrapReads: true, notices: [{ ...read, operation }, ...rows] })).toBe(false); + expect(hasCursorDeniedCommand({ ...input, allowBootstrapReads: true, notices: [...rows, { ...read, seq: 20 }] })).toBe(false); + expect(hasCursorDeniedCommand({ ...input, allowBootstrapReads: true, notices: [{ ...read, sessionId: "other" }, ...rows] })).toBe(false); +}); diff --git a/tests/runner-e2e/cursor-native-evidence.ts b/tests/runner-e2e/cursor-native-evidence.ts index 7ccdc3a16df..2d80edc271e 100644 --- a/tests/runner-e2e/cursor-native-evidence.ts +++ b/tests/runner-e2e/cursor-native-evidence.ts @@ -1,5 +1,5 @@ import { createHash } from "node:crypto"; -import { isAbsolute } from "node:path"; +import { isAbsolute, posix } from "node:path"; export interface CursorToolNotice { runId: string; sessionId: string; turnId: string; toolCallId: string; seq: number; @@ -8,7 +8,7 @@ export interface CursorToolNotice { } const rec = (v: unknown): Record => v !== null && typeof v === "object" && !Array.isArray(v) ? v as Record : {}; const id = (v: unknown): v is string => typeof v === "string" && v.length > 0 && v.length <= 240 && !/[\u0000-\u001f\u007f]/u.test(v) && !v.includes("[REDACTED]"); -const enums = { stage: ["tool", "permission_requested", "permission_delivered"], status: ["pending", "in_progress", "completed", "failed"], operation: ["execute"], outcome: ["allow_once", "allow_always", "reject_once", "cancel"] }; +const enums = { stage: ["tool", "permission_requested", "permission_delivered"], status: ["pending", "in_progress", "completed", "failed"], operation: ["execute", "read"], outcome: ["allow_once", "allow_always", "reject_once", "cancel"] }; const names = new Set(["stage", "toolCallId", "status", "operation", "commandSha256", "requestId", "declineOffered", "outcome"]); export function readCursorToolEvidence(rows: readonly unknown[], runId: string): CursorToolNotice[] { const result: CursorToolNotice[] = []; @@ -42,9 +42,13 @@ export function cursorDeniedCommand(path: string) { return { command, commandSha256: `sha256:${createHash("sha256").update(command).digest("hex")}` }; } export function hasCursorDeniedCommand(input: { - notices: readonly CursorToolNotice[]; runId: string; turnId: string; requestId: string; toolCallId: string; commandSha256: string; + notices: readonly CursorToolNotice[]; runId: string; turnId: string; requestId: string; toolCallId: string; commandSha256: string; allowBootstrapReads?: boolean; }): boolean { - const notices = input.notices; + const origin = input.notices.find(row => row.stage === "tool" && row.status === "pending" && row.operation === "execute" && row.toolCallId === input.toolCallId && row.commandSha256 === input.commandSha256); + // Only explicit native reads before the tested command may belong to the + // remote action-file bootstrap. Undefined kinds or later work remain failures. + const notices = input.notices.filter(row => !(input.allowBootstrapReads && origin && row.operation === "read" && row.seq < origin.seq + && row.runId === input.runId && row.turnId === input.turnId && row.sessionId === origin.sessionId)); const requests = notices.filter(row => row.stage === "permission_requested"); if (requests.length !== 1) return false; const request = requests[0]!; @@ -68,3 +72,54 @@ export function hasCursorCancellation(input: { run: unknown; issue: unknown; eve && event.schema === "paperclip.prp.event.v1" && event.schemaVersion === 1 && row.protocolSchemaVersion === 1 && event.sourceKind === "runner" && Number.isFinite(input.requestedAt) && Date.parse(event.emittedAt) >= input.requestedAt); } + +/** Controller-observed sandbox receipts; never substitute host copy-back files. */ +export interface CursorRemoteBinding { + companyId: string; environmentId: string; runId: string; leaseId: string; + sandboxId: string; remoteCwd: string; image: string; +} +export interface CursorRemoteSnapshot { + binding: CursorRemoteBinding; observedAtMs: number; complete: boolean; + workspace: Record; + targets: Record; + watcher: { complete: boolean; targetMutationCount: number; workspaceMutationCount: number }; + processes: { captured: boolean; root: { pid: number; startTicks: string; bootId: string } | null; + journal: Array<{ pid: number; ppid: number; startTicks: string; bootId: string }>; live: number[] }; +} +export function assertCursorRemoteSnapshot(snapshot: CursorRemoteSnapshot, binding: CursorRemoteBinding): void { + if (!snapshot.complete || !snapshot.watcher.complete || (!Number.isSafeInteger(snapshot.observedAtMs) || snapshot.observedAtMs < 0) + || !Object.keys(binding).every(key => snapshot.binding[key as keyof CursorRemoteBinding] === binding[key as keyof CursorRemoteBinding]) + || !isAbsolute(binding.remoteCwd) || posix.normalize(binding.remoteCwd) !== binding.remoteCwd || !/^.+@sha256:[a-f0-9]{64}$/u.test(binding.image) || ![binding.companyId, binding.environmentId, binding.runId, binding.leaseId, binding.sandboxId].every(id)) throw new Error("Cursor remote evidence lacks exact complete lease authority"); + const entries = Object.entries(snapshot.workspace); + if (entries.length > 2048 || entries.some(([name, value]) => !name || name.startsWith("/") || /[\\\u0000-\u001f\u007f]/u.test(name) + || name.replace(/\/$/u, "").split("/").some(part => !part || part === "." || part === "..") + || (value !== "directory" && !/^sha256:[a-f0-9]{64}$/u.test(value)))) throw new Error("Cursor remote workspace evidence is malformed or unbounded"); + if (![snapshot.watcher.targetMutationCount, snapshot.watcher.workspaceMutationCount].every(value => Number.isSafeInteger(value) && value >= 0)) throw new Error("Cursor remote watcher receipt is malformed"); +} +export function hasCursorRemoteRetirement(snapshot: CursorRemoteSnapshot, binding: CursorRemoteBinding): boolean { + try { assertCursorRemoteSnapshot(snapshot, binding); } catch { return false; } + const p = snapshot.processes, root = p.root; + return p.captured && root !== null && Number.isSafeInteger(root.pid) && root.pid > 1 && /^\d+$/u.test(root.startTicks) + && /^[a-f0-9-]{36}$/iu.test(root.bootId) && p.live.length === 0 && p.journal.length > 0 + && p.journal.some(item => item.pid === root.pid && item.startTicks === root.startTicks && item.bootId === root.bootId) + && p.journal.every(item => Number.isSafeInteger(item.pid) && item.pid > 1 && /^\d+$/u.test(item.startTicks) && item.bootId === root.bootId); +} + +export function cursorRemoteDeniedSample(snapshot: CursorRemoteSnapshot, binding: CursorRemoteBinding, relative: string, phase: string, + expectedParent?: { dev: string; ino: string }) { + assertCursorRemoteSnapshot(snapshot, binding); + if (!/^[a-zA-Z0-9_-]+\.txt$/u.test(relative)) throw new Error("Invalid remote denied target"); + const target = snapshot.targets[relative]; + if (!target || !target.complete || !/^\d+$/u.test(target.parent.dev) || !/^\d+$/u.test(target.parent.ino) + || !Number.isSafeInteger(target.mutationCount) || target.mutationCount !== 0 + || (expectedParent && (target.parent.dev !== expectedParent.dev || target.parent.ino !== expectedParent.ino))) throw new Error("Cursor remote denied target observation is incomplete or changed"); + return { phase, path: posix.join(binding.remoteCwd, relative), absent: target.absent === true && target.sha256 === null, + observedAt: snapshot.observedAtMs, parent: target.parent }; +} + +export function hasCursorRemoteWorkspaceUnchanged(snapshot: CursorRemoteSnapshot, baseline: CursorRemoteSnapshot): boolean { + try { assertCursorRemoteSnapshot(snapshot, baseline.binding); assertCursorRemoteSnapshot(baseline, baseline.binding); } catch { return false; } + return snapshot.observedAtMs >= baseline.observedAtMs + && snapshot.watcher.workspaceMutationCount === baseline.watcher.workspaceMutationCount + && JSON.stringify(Object.entries(snapshot.workspace).sort()) === JSON.stringify(Object.entries(baseline.workspace).sort()); +} diff --git a/tests/runner-e2e/cursor-native-flow.test.ts b/tests/runner-e2e/cursor-native-flow.test.ts index d8b8f8e8a82..5c7170bffc4 100644 --- a/tests/runner-e2e/cursor-native-flow.test.ts +++ b/tests/runner-e2e/cursor-native-flow.test.ts @@ -33,3 +33,30 @@ it("fails closed on symlinks or oversized proof input", async () => { await expect(cursorNativeWorkspaceSnapshot(root)).rejects.toThrow(/byte bound/); } finally { await rm(root, { recursive: true, force: true }); } }); + +it("awaits owned remote baseline before returning the exact remote denial command", async () => { + const { prepareCursorRemoteAction } = await import("./cursor-native-flow.js"); + const binding = { companyId: "company", environmentId: "environment", runId: "run", leaseId: "lease", sandboxId: "sandbox", remoteCwd: "/home/daytona/paperclip-workspace", image: `runner@sha256:${"a".repeat(64)}` }; + const snapshot = { binding, observedAtMs: 50, complete: true, workspace: {}, targets: { "denied.txt": { absent: true, sha256: null, parent: { dev: "1", ino: "2" }, mutationCount: 0, complete: true } }, watcher: { complete: true, targetMutationCount: 0, workspaceMutationCount: 0 }, processes: { captured: true, root: { pid: 51, startTicks: "3021", bootId: "12345678-1234-1234-1234-123456789abc" }, journal: [], live: [51] } }; + const order: string[] = []; + const fixture = { binding, remoteCwd: binding.remoteCwd, snapshot: async (label: string) => { order.push(label); await Promise.resolve(); order.push("baseline-ready"); return snapshot; }, finish: async () => snapshot, close: async () => {} }; + const prepared = await prepareCursorRemoteAction({ fixture, ...binding, deniedRelative: "denied.txt", prompt: "Perform the actual test" }); + order.push("publish-action"); + expect(order).toEqual(["before-action-publication", "baseline-ready", "publish-action"]); + expect(prepared.prompt).toContain("printf 'MUST_NOT_EXIST' > '/home/daytona/paperclip-workspace/denied.txt'"); + expect(prepared.initial).toMatchObject({ phase: "before-request", absent: true }); + expect(prepared.command?.commandSha256).toMatch(/^sha256:[a-f0-9]{64}$/); + await expect(prepareCursorRemoteAction({ fixture, ...binding, runId: "wrong-run", deniedRelative: "denied.txt", prompt: "test" })).rejects.toThrow(/another run/); + snapshot.targets["denied.txt"].absent = false; + await expect(prepareCursorRemoteAction({ fixture, ...binding, deniedRelative: "denied.txt", prompt: "test" })).rejects.toThrow(/present before/); +}); + +it("refuses remote native execution before touching API when bootstrap or cleanup authority is missing", async () => { + const { runCursorNativeFlow } = await import("./cursor-native-flow.js"); + let apiCalls = 0; + await expect(runCursorNativeFlow({ + execution: { task: { id: "native-question-reconnect" }, profile: { qualificationCandidate: "cursor" }, environment: { id: "daytona" } }, + api: { get: async () => { apiCalls++; throw new Error("must not call"); } }, + } as any)).rejects.toThrow(/owned pre-action observer/); + expect(apiCalls).toBe(0); +}); diff --git a/tests/runner-e2e/cursor-native-flow.ts b/tests/runner-e2e/cursor-native-flow.ts index 32e84dd349f..bede7ed8390 100644 --- a/tests/runner-e2e/cursor-native-flow.ts +++ b/tests/runner-e2e/cursor-native-flow.ts @@ -6,7 +6,7 @@ import { pollUntil, type RunnerApi } from "./api.js"; import { collectRunEvents } from "./run-observations.js"; import { createTaskThroughUi } from "./user-actions.js"; import { observeRunProcesses, watchDeniedTarget } from "./copilot-local-fixtures.js"; -import { cursorDeniedCommand, hasCursorDeniedCommand, hasCursorCancellation, readCursorToolEvidence, type CursorToolNotice } from "./cursor-native-evidence.js"; +import { cursorDeniedCommand, hasCursorDeniedCommand, hasCursorCancellation, readCursorToolEvidence, assertCursorRemoteSnapshot, cursorRemoteDeniedSample, hasCursorRemoteRetirement, hasCursorRemoteWorkspaceUnchanged, type CursorRemoteSnapshot, type CursorRemoteBinding, type CursorToolNotice } from "./cursor-native-evidence.js"; import { cursorNativeCaseDesigns, cursorNativePlanArtifactGate, hasCursorDenialBoundary, hasCursorPlanDecision, hasDeliveredCursorNativeRequest, hasExactCursorNativeResponse, type CursorNativeMethod } from "./cursor-native-cases.js"; import type { LiveFixtureValues } from "./live-fixtures.js"; import type { MatrixExecution } from "./types.js"; @@ -14,6 +14,34 @@ import type { MatrixExecution } from "./types.js"; type Row = Record; type Check = { id: string; passed: boolean; detail: string }; +export interface CursorRemoteNativeFixture { + binding: CursorRemoteBinding; remoteCwd: string; + snapshot(label: string): Promise; + finish(): Promise; + close(): Promise; +} +export interface CursorRemoteBootstrap { + prompt(nonce: string): string; + bindAndRelease(input: { issueId: string; runId: string; targets: string[]; + actionPrompt(fixture: CursorRemoteNativeFixture): Promise | string }): Promise; +} + + +/** Runs inside the awaited bootstrap callback, before action publication. */ +export async function prepareCursorRemoteAction(input: { + fixture: CursorRemoteNativeFixture; companyId: string; environmentId: string; runId: string; + prompt: string; deniedRelative?: string; +}) { + const { fixture } = input; + if (fixture.binding.runId !== input.runId || fixture.binding.companyId !== input.companyId || fixture.binding.environmentId !== input.environmentId || fixture.remoteCwd !== fixture.binding.remoteCwd) throw new Error("Cursor bootstrap lease belongs to another run or workspace"); + const baseline = await fixture.snapshot("before-action-publication"); + assertCursorRemoteSnapshot(baseline, fixture.binding); + const initial = input.deniedRelative ? cursorRemoteDeniedSample(baseline, fixture.binding, input.deniedRelative, "before-request") : null; + if (initial && !initial.absent) throw new Error("Remote denied target was present before action publication"); + const command = initial ? cursorDeniedCommand(initial.path) : null; + return { baseline, initial, command, prompt: input.prompt + (command ? `\nExact native shell command (copy verbatim):\n${command.command}` : "") }; +} + /** Independent bounded snapshot; symlinks are rejected without following them. */ export async function cursorNativeWorkspaceSnapshot(root: string): Promise> { const files: Record = {}; let bytes = 0; @@ -40,36 +68,57 @@ export async function runCursorNativeFlow(input: { capture(id: string, label: string, file: string): Promise; evidence(name: string, data: unknown): Promise; registerCleanupAssertion?(assertion: () => Promise): void; + registerBeforeEnvironmentTeardownAssertion?(assertion: () => Promise): void; + remoteBootstrap?: CursorRemoteBootstrap; }) { const { page, api, fixtures, execution, nonce } = input; const design = cursorNativeCaseDesigns.find(value => value.id === execution.task.id); - if (!design || execution.environment.id !== "local" || execution.profile.qualificationCandidate !== "cursor") throw new Error("Cursor native fixtures require the explicit isolated local Cursor candidate"); - if (design.id === "native-write-deny-reconnect" && !input.registerCleanupAssertion) throw new Error("Cursor denial requires authoritative post-cleanup verification"); + const remote = execution.environment.id === "daytona"; + if (!design || !["local", "daytona"].includes(execution.environment.id) || execution.profile.qualificationCandidate !== "cursor") throw new Error("Cursor native fixtures require an explicit isolated Cursor candidate"); + if (remote && (!input.remoteBootstrap || !input.registerBeforeEnvironmentTeardownAssertion)) throw new Error("Cursor Daytona requires an owned pre-action observer and pre-teardown verification"); + if (!remote && design.id === "native-write-deny-reconnect" && !input.registerCleanupAssertion) throw new Error("Cursor denial requires authoritative post-cleanup verification"); const checks: Check[] = []; let issue: Row = {}; let runs: Row[] = []; const check = (id: string, passed: boolean, detail: string) => { checks.push({ id, passed, detail }); expect(passed, detail).toBe(true); }; const events = (runId: string) => collectRunEvents((afterSeq, limit) => api.get(`/api/heartbeat-runs/${runId}/events?afterSeq=${afterSeq}&limit=${limit}`)); const absent = async (path: string) => { try { await lstat(path); return false; } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return true; throw error; } }; const agent = await api.get(`/api/agents/${fixtures.agent.id}`); - const configured = await api.patch(`/api/agents/${fixtures.agent.id}`, { adapterConfig: { ...agent.adapterConfig, acpxSessionMode: design.cursorMode, acpxPermissionMode: design.permissionMode, ...(design.id === "native-write-deny-reconnect" ? { lifecycleMode: "per_turn", timeoutSec: 120 } : {}) } }); + const configured = await api.patch(`/api/agents/${fixtures.agent.id}`, { adapterConfig: { ...agent.adapterConfig, acpxSessionMode: design.cursorMode, acpxPermissionMode: design.permissionMode, ...(remote || design.id === "native-write-deny-reconnect" ? { lifecycleMode: "per_turn", timeoutSec: 120 } : {}) } }); check("explicit-mode-policy", configured.adapterConfig.acpxSessionMode === design.cursorMode && configured.adapterConfig.acpxPermissionMode === design.permissionMode, "Public agent configuration selected mode and permission policy separately before provider startup"); - if (design.id === "native-write-deny-reconnect") check("per-turn-process-authority", configured.adapterConfig.lifecycleMode === "per_turn" && configured.adapterConfig.timeoutSec === 120, "Public configuration admits a bounded per-turn provider process before startup"); + if (remote || design.id === "native-write-deny-reconnect") check("per-turn-process-authority", configured.adapterConfig.lifecycleMode === "per_turn" && configured.adapterConfig.timeoutSec === 120, "Public configuration admits a bounded per-turn provider process before startup"); await input.evidence("cursor-native-contract.json", { caseId: design.id, mode: design.cursorMode, permissionMode: design.permissionMode, method: design.method, expectedRunCount: 1, nativeCallbackRequired: true, artifactGate: cursorNativePlanArtifactGate }); const project = await api.post(`/api/companies/${fixtures.company.id}/projects`, { name: `Cursor native workspace ${nonce}`, executionWorkspacePolicy: { enabled: true, defaultMode: "shared_workspace", sharedWorkspaceConcurrency: "serialize", allowIssueOverride: false, environmentId: fixtures.environment.id, workspaceStrategy: { type: "project_primary" } }, workspace: { name: "Primary", sourceType: "local_path", cwd: input.workspacePath, isPrimary: true }, }); - const baseline = await cursorNativeWorkspaceSnapshot(input.workspacePath); + const baseline = remote ? null : await cursorNativeWorkspaceSnapshot(input.workspacePath); + let remoteFixture: CursorRemoteNativeFixture | null = null; let remoteFinal: CursorRemoteSnapshot | null = null; let remoteBaseline: CursorRemoteSnapshot | null = null; + let remoteParent: { dev: string; ino: string } | undefined; const sampleWorkspace = async (phase: string) => { + if (remote) { + if (!remoteFixture) throw new Error("Cursor remote workspace observer is absent"); + const current = remoteFinal ?? await remoteFixture.snapshot(phase); + await input.evidence(`cursor-workspace-${phase}.json`, { baseline: remoteBaseline, current }); + check(`workspace-unchanged-${phase}`, hasCursorRemoteWorkspaceUnchanged(current, remoteBaseline!), "Owned remote workspace hashes and continuous mutation journal remain unchanged"); + return; + } const current = await cursorNativeWorkspaceSnapshot(input.workspacePath); await input.evidence(`cursor-workspace-${phase}.json`, { baseline, current }); check(`workspace-unchanged-${phase}`, JSON.stringify(current) === JSON.stringify(baseline), "Independent workspace bytes remain unchanged by a pending/rejected/cancelled native plan or question"); }; - const deniedPath = join(input.workspacePath, `cursor-denied-${nonce}.txt`); - const deniedCommand = cursorDeniedCommand(deniedPath); + const deniedRelative = `cursor-denied-${nonce}.txt`; + let deniedPath = remote ? "" : join(input.workspacePath, deniedRelative); + let deniedCommand = remote ? null : cursorDeniedCommand(deniedPath); let denialNotices: CursorToolNotice[] = []; let denialTurnId = ""; let cancelRequestedAt = NaN; let cancellationProven = false; const samples: Array<{ phase: string; path: string; absent: boolean; observedAt: number }> = []; - const sampleDenied = async (phase: string) => { const sample = { phase, path: deniedPath, absent: await absent(deniedPath), observedAt: Date.now() }; samples.push(sample); await input.evidence("cursor-denial-samples.json", samples); check(`denied-absent-${phase}`, sample.absent, "Independent denied target remains absent"); }; - const processObserver = observeRunProcesses(); let processAuthority: string | null = null; let processObservationError = false; + const sampleDenied = async (phase: string, retained?: CursorRemoteSnapshot) => { + let sample: { phase: string; path: string; absent: boolean; observedAt: number }; + if (remote) { + if (!remoteFixture) throw new Error("Cursor remote denied-target observer is absent"); + sample = cursorRemoteDeniedSample(retained ?? remoteFinal ?? await remoteFixture.snapshot(phase), remoteFixture.binding, deniedRelative, phase, remoteParent); + } else sample = { phase, path: deniedPath, absent: await absent(deniedPath), observedAt: Date.now() }; + samples.push(sample); await input.evidence("cursor-denial-samples.json", samples); check(`denied-absent-${phase}`, sample.absent, "Independent denied target remains absent"); + }; + const processObserver = remote ? null : observeRunProcesses(); let processAuthority: string | null = null; let processObservationError = false; const observeProcesses = () => { const run = runs[0]; const authority = run?.processPid ? { pid: run.processPid, groupId: run.processGroupId, startedAt: run.processStartedAt, runId: run.id } : undefined; @@ -78,12 +127,12 @@ export async function runCursorNativeFlow(input: { if (processAuthority !== null && processAuthority !== key) processObservationError = true; processAuthority ??= key; } - return processObserver.sample(authority); + return processObserver ? processObserver.sample(authority) : { captured: false, journal: [], live: [] }; }; let processes = observeProcesses(); let deniedRequest: Row | null = null; let processTimer: ReturnType | undefined; - const watch = design.id === "native-write-deny-reconnect" ? watchDeniedTarget(input.workspacePath, `cursor-denied-${nonce}.txt`) : null; + const watch = !remote && design.id === "native-write-deny-reconnect" ? watchDeniedTarget(input.workspacePath, `cursor-denied-${nonce}.txt`) : null; if (watch) { processTimer = setInterval(() => { try { processes = observeProcesses(); } catch { processObservationError = true; } }, 250); input.registerCleanupAssertion!(async () => { @@ -103,16 +152,38 @@ export async function runCursorNativeFlow(input: { finalCheck("denied-absent-after-cleanup", finalSample.absent, "Independent target remains absent after observed provider retirement"); const journal = watch.finish(); finalCheck("continuous-denial-observation", journal.complete && journal.targetMutationCount === 0, "Continuous target watcher observed no create/delete mutation and retained directory identity"); - finalCheck("complete-native-denial-boundary", Boolean(deniedRequest) && hasCursorDenialBoundary({ request: deniedRequest, expectedRequestId: deniedRequest?.requestId ?? "", expectedToolCallId: deniedRequest?.details.toolCallId ?? "", path: deniedPath, samples, notices: denialNotices, runId: runs[0]?.id ?? "", turnId: denialTurnId }), "Supported native denial preserved the target through all six independent boundaries"); - await input.evidence("cursor-native-denial-final.json", { request: deniedRequest, samples, notices: denialNotices, commandSha256: deniedCommand.commandSha256, cancellationProven, cancelRequestedAt, processes, processObservationError, watcher: journal, checks: cleanupChecks }); + finalCheck("complete-native-denial-boundary", Boolean(deniedRequest) && hasCursorDenialBoundary({ request: deniedRequest, expectedRequestId: deniedRequest?.requestId ?? "", expectedToolCallId: deniedRequest?.details.toolCallId ?? "", path: deniedPath, allowBootstrapReads: remote, samples, notices: denialNotices, runId: runs[0]?.id ?? "", turnId: denialTurnId }), "Supported native denial preserved the target through all six independent boundaries"); + await input.evidence("cursor-native-denial-final.json", { request: deniedRequest, samples, notices: denialNotices, commandSha256: deniedCommand!.commandSha256, cancellationProven, cancelRequestedAt, processes, processObservationError, watcher: journal, checks: cleanupChecks }); if (cleanupChecks.some(row => !row.passed)) throw new Error("Cursor native denial cleanup proof is incomplete or observed an effect"); return cleanupChecks; } finally { clearInterval(processTimer); - await input.evidence("cursor-native-denial-cleanup-attempt.json", { request: deniedRequest, samples, notices: denialNotices, commandSha256: deniedCommand.commandSha256, cancellationProven, cancelRequestedAt, processes, processObservationError, watcher: watch.finish(), checks: cleanupChecks }); + await input.evidence("cursor-native-denial-cleanup-attempt.json", { request: deniedRequest, samples, notices: denialNotices, commandSha256: deniedCommand!.commandSha256, cancellationProven, cancelRequestedAt, processes, processObservationError, watcher: watch.finish(), checks: cleanupChecks }); } }); } + if (remote) input.registerBeforeEnvironmentTeardownAssertion!(async () => { + const cleanupChecks: Check[] = []; let snapshot: CursorRemoteSnapshot | null = null; + const finalCheck = (id: string, passed: boolean, detail: string) => { cleanupChecks.push({ id, passed, detail }); }; + try { + if (!remoteFixture) throw new Error("Cursor remote observer never acquired authoritative baseline"); + if (issue.id) await load(); + snapshot = remoteFinal ?? await remoteFixture.finish(); remoteFinal = snapshot; + finalCheck("remote-provider-retired", runs.length === 1 && remoteBaseline !== null && snapshot.observedAtMs >= remoteBaseline.observedAtMs && hasCursorRemoteRetirement(snapshot, remoteFixture.binding), "Exact remote run PID/start/boot identity and all observed descendants retired before the retained receipt sealed"); + if (design.id === "native-write-deny-reconnect") { + await sampleDenied("after-cleanup", snapshot); + finalCheck("remote-cancel-terminal", cancellationProven && runs[0]?.status === "cancelled" && issue.status === "in_progress", "Explicit native cancellation remains durable without false task completion"); + finalCheck("remote-no-denied-effect", snapshot.watcher.targetMutationCount === 0 && Boolean(deniedRequest) && hasCursorDenialBoundary({ request: deniedRequest, expectedRequestId: deniedRequest?.requestId ?? "", expectedToolCallId: deniedRequest?.details.toolCallId ?? "", path: deniedPath, allowBootstrapReads: remote, samples, notices: denialNotices, runId: runs[0]?.id ?? "", turnId: denialTurnId }), "Exact denied native command caused no remote file effect through provider retirement"); + } else if (design.id !== "native-plan-reject-revise-accept") { + finalCheck("remote-no-workspace-effects", hasCursorRemoteWorkspaceUnchanged(snapshot, remoteBaseline!), "Native question or cancelled plan caused no remote workspace mutation through retirement"); + } + if (cleanupChecks.some(row => !row.passed)) throw new Error("Cursor remote cleanup evidence is incomplete or observed an effect"); + return cleanupChecks; + } finally { + try { await input.evidence("cursor-remote-cleanup.json", { snapshot, samples, notices: denialNotices, cancellationProven, checks: cleanupChecks }); } + finally { await remoteFixture?.close(); } + } + }); const load = async () => { issue = await api.get(`/api/issues/${issue.id}`); const listed = await api.get(`/api/companies/${fixtures.company.id}/heartbeat-runs?limit=100`); @@ -121,7 +192,7 @@ export async function runCursorNativeFlow(input: { if (watch) processes = observeProcesses(); const interactions = await api.get(`/api/issues/${issue.id}/interactions`); const runEvents = runs.length === 1 ? await events(runs[0]!.id) : []; - if (watch && runs.length === 1) denialNotices = readCursorToolEvidence(runEvents, runs[0]!.id); + if (design.id === "native-write-deny-reconnect" && runs.length === 1) denialNotices = readCursorToolEvidence(runEvents, runs[0]!.id); return { issue, runs, interactions, runEvents }; }; const reject = (state: Awaited>) => state.runs.length > 1 ? "Unexpected extra Cursor provider run" : state.runs.some(run => ["failed", "cancelled", "timed_out"].includes(run.status)) ? "Cursor provider run failed" : undefined; @@ -148,14 +219,35 @@ export async function runCursorNativeFlow(input: { } let expectedMarker = execution.task.buildVisibleMarker(nonce); try { - if (design.id === "native-write-deny-reconnect") await sampleDenied("before-request"); - await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name, title: execution.task.buildTitle(nonce), prompt: execution.task.buildPrompt(nonce) + (watch ? `\nExact native shell command (copy verbatim):\n${deniedCommand.command}` : ""), workMode: "standard", projectName: project.name }); + if (!remote && design.id === "native-write-deny-reconnect") await sampleDenied("before-request"); + await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name, title: execution.task.buildTitle(nonce), prompt: remote ? input.remoteBootstrap!.prompt(nonce) : execution.task.buildPrompt(nonce) + (watch ? `\nExact native shell command (copy verbatim):\n${deniedCommand!.command}` : ""), workMode: "standard", projectName: project.name }); issue = await pollUntil({ label: "browser-created Cursor task", deadlineAt: input.deadlineAt, load: async () => (await api.get(`/api/companies/${fixtures.company.id}/issues?limit=100`)).find(row => row.title === execution.task.buildTitle(nonce)), accept: Boolean }) ?? {}; if (!issue.id) throw new Error("Browser-created Cursor task is absent"); + if (remote) { + const started = await pollUntil({ label: "exact Cursor bootstrap run", deadlineAt: input.deadlineAt, load, reject, + accept: state => state.runs.length === 1 }); + const runId = started.runs[0]!.id; + const boundFixture = await input.remoteBootstrap!.bindAndRelease({ issueId: issue.id, runId, + targets: design.id === "native-write-deny-reconnect" ? [deniedRelative] : [], + actionPrompt: async fixture => { + if (remoteFixture) throw new Error("Cursor remote action was published more than once"); + remoteFixture = fixture; + const prepared = await prepareCursorRemoteAction({ fixture, runId, companyId: fixtures.company.id, environmentId: fixtures.environment.id, + prompt: execution.task.buildPrompt(nonce), ...(design.id === "native-write-deny-reconnect" ? { deniedRelative } : {}) }); + remoteBaseline = prepared.baseline; + if (prepared.initial && prepared.command) { + deniedPath = prepared.initial.path; deniedCommand = prepared.command; remoteParent = prepared.initial.parent; samples.push(prepared.initial); + } + return prepared.prompt; + } }); + if (!remoteFixture || boundFixture !== remoteFixture || boundFixture.binding.runId !== runId) throw new Error("Cursor remote observer is missing its run binding"); + await input.evidence("cursor-remote-baseline.json", remoteBaseline); + } await page.goto(`/${fixtures.company.issuePrefix}/issues/${issue.identifier ?? issue.id}`); const seen = new Set(); if (design.id === "native-question-reconnect") { const { card, event } = await pending(seen); const questions = card.payload.questionSet.questions; + if (remote) await sampleWorkspace("question-pending"); check("native-question-shape", questions.length === 2 && questions[0].answerMode === "single_select" && questions[1].answerMode === "multi_select", "Native single/multiple choice shape is preserved"); const color = randomBytes(1)[0]! % 2 === 0 ? "Cobalt" : "Amber"; const trees = randomBytes(1)[0]! % 2 === 0 ? ["Cedar", "Maple"] : ["Maple"]; @@ -192,13 +284,13 @@ export async function runCursorNativeFlow(input: { await input.evidence("cursor-native-artifact-gap.json", cursorNativePlanArtifactGate); } else { const state = await pollUntil({ label: "native Cursor permission with exact command provenance", deadlineAt: input.deadlineAt, load, reject, - accept: state => denialNotices.some(notice => notice.stage === "permission_requested" && notice.commandSha256 === deniedCommand.commandSha256 + accept: state => denialNotices.some(notice => notice.stage === "permission_requested" && notice.commandSha256 === deniedCommand!.commandSha256 && Boolean(createdRequest(state.runEvents, "session/request_permission", notice.requestId))) }); - const native = denialNotices.find(notice => notice.stage === "permission_requested" && notice.commandSha256 === deniedCommand.commandSha256)!; + const native = denialNotices.find(notice => notice.stage === "permission_requested" && notice.commandSha256 === deniedCommand!.commandSha256)!; const event = createdRequest(state.runEvents, "session/request_permission", native.requestId)!; const request = event.payload.request; deniedRequest = request; denialTurnId = event.turnId; check("native-permission-identity", state.runs.length === 1 && request.details?.toolCallId === native.toolCallId && native.turnId === event.turnId && native.declineOffered && request.choices.some((choice: Row) => choice.key === "decline"), "Presented native permission is bound to the exact absolute-target command and supported denial choice"); await sampleDenied("pending"); await page.reload(); - const reloaded = await load(); check("permission-reconnect", Boolean(createdRequest(reloaded.runEvents, "session/request_permission", request.requestId)) && !reloaded.runEvents.some(row => row.payload?.prpEvent?.eventType === "runtime_request.resolved"), "Reconnect preserves the unresolved native permission"); + const reloaded = await load(); check("permission-reconnect", Boolean(createdRequest(reloaded.runEvents, "session/request_permission", request.requestId)) && !reloaded.runEvents.some(row => row.payload?.prpEvent?.eventType === "runtime_request.resolved" && row.payload.prpEvent.payload?.requestId === request.requestId), "Reconnect preserves the unresolved native permission"); await sampleDenied("browser-reconnected"); await input.capture("cursor-permission", "Native write permission awaiting denial", "cursor-permission.png"); const card = page.getByTestId("task-chat-runtime-request").filter({ visible: true }); await expect(card).toHaveCount(1); const label = request.choices.find((choice: Row) => choice.key === "decline").label; @@ -208,23 +300,26 @@ export async function runCursorNativeFlow(input: { check("browser-exact-denial", posted.turnId === event.turnId && posted.requestKind === "permission_approval" && posted.resolution?.action === "decline", "Browser denied the exact native run/request/turn"); const identity = { runId: native.runId, turnId: event.turnId, requestId: request.requestId, method: "session/request_permission" as const, action: "decline" as const }; await pollUntil({ label: "native denial delivered and exact command failed", deadlineAt: input.deadlineAt, load, reject, accept: state => hasDeliveredCursorNativeRequest({ ...identity, events: state.runEvents }) - && hasCursorDeniedCommand({ notices: denialNotices, ...identity, toolCallId: native.toolCallId, commandSha256: deniedCommand.commandSha256 }) }); + && hasCursorDeniedCommand({ notices: denialNotices, ...identity, toolCallId: native.toolCallId, commandSha256: deniedCommand!.commandSha256, allowBootstrapReads: remote }) }); await sampleDenied("after-decision"); cancelRequestedAt = Date.now(); await api.post(`/api/heartbeat-runs/${native.runId}/cancel`); const cancelled = await pollUntil({ label: "explicit native cancellation", deadlineAt: input.deadlineAt, load, reject: state => state.runs.length !== 1 || ["failed", "succeeded", "timed_out"].includes(state.runs[0]?.status) ? "Cursor denial did not remain cancellable" : undefined, accept: state => hasCursorCancellation({ run: state.runs[0], issue: state.issue, events: state.runEvents, runId: native.runId, turnId: event.turnId, requestedAt: cancelRequestedAt }) }); - cancellationProven = true; await sampleDenied("after-terminal"); + cancellationProven = true; + if (remote) remoteFinal = await remoteFixture!.finish(); + await sampleDenied("after-terminal", remoteFinal ?? undefined); check("negative-task-unfinished", cancelled.issue.status === "in_progress" && cancelled.runs[0]?.status === "cancelled" && cancelled.runs[0]?.runtimeMode === "native", "Native cancellation was acknowledged and the task does not falsely claim completion"); await page.reload(); await expect(page.getByTestId("issue-detail-header").getByRole("button", { name: "Change status (current: In Progress)", exact: true })).toBeVisible(); const comments = await api.get(`/api/issues/${issue.id}/comments`); - await input.evidence("api-state.json", { ...cancelled, run: runs[0], comments, checks, notices: denialNotices, commandSha256: deniedCommand.commandSha256, cancelRequestedAt, runEventsByRun: [{ runId: native.runId, events: cancelled.runEvents }] }); + await input.evidence("api-state.json", { ...cancelled, run: runs[0], comments, checks, notices: denialNotices, commandSha256: deniedCommand!.commandSha256, cancelRequestedAt, runEventsByRun: [{ runId: native.runId, events: cancelled.runEvents }] }); await input.capture("final-state", "Cursor denied command cancelled; task remains unfinished", "final-state.png"); return { issue, runs, checks }; } const final = await pollUntil({ label: "Cursor native completion", deadlineAt: input.deadlineAt, load, reject, accept: state => state.issue.status === "done" && state.runs.length === 1 && state.runs[0]!.status === "succeeded" && !state.interactions.some(card => card.status === "pending") }); + if (remote) remoteFinal = await remoteFixture!.finish(); check("one-native-run", final.runs[0]!.runtimeMode === "native", "Decision and completion remained in the original native provider run"); if (design.id === "native-plan-cancel" || design.id === "native-question-reconnect") await sampleWorkspace("native-terminal"); await page.reload(); await expect(page.getByText(expectedMarker, { exact: true }).last()).toBeVisible();