Fix public Cursor asset ownership and add runtime-loss qualification

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-03 13:33:37 -05:00
1 parent e2b89b6302
commit 37ba674378
16 files changed
+395 -12

No files matched your search

@@ -68,3 +68,38 @@ GitHub Actions ownership of lockfile commits. Its SHA-256 is
`70af8ab3d7051c85fc1a55c11e9afe8887d9711232e3c6e97666006562217e5f`;
retain these exact resolved bytes with candidate artifacts and pass their digest
to the immutable image build.
## Qualification preparation and demonstrated repairs
Candidate `9ba53fced5de6dabd1e931b7438d9dd118e45f0f` passed local
completion, semantic question continuation, and file-edit/validation Product E2E
cells with the explicit Luna model, serial execution, no retries, and successful
cleanup. Their original campaign identities remain unchanged. Measured per-run
dollar usage is unavailable, not zero. The existing Cursor account-cycle cap is
counted once against the reconciled campaign envelope.
Fresh ordinary provider packs were built for macOS ARM64 and x64; the Linux x64
Daytona image also built successfully. These are preparation artifacts, not a
completed release certification.
Public-package inspection found that setup derived the standalone runner layout
when embedded in the server's vendored layout. The provisioner now selects its
contained public asset root explicitly and rejects an unbundled source invocation.
Three containment checks pass. `node scripts/verify-cursor-npm-install.mjs`
packages the actual public CLI/server dependency graph and verifies installation,
enabled npm hooks, explicit setup, and the installed Cursor execution closure in
an isolated consumer. It retains evidence under its printed temporary directory
and makes no model calls. npm hooks may fetch normal platform dependencies;
Cursor provisioning must remain absent until the public setup command runs.
The new explicit-only `native-provider-loss` Product E2E suite covers the missing
transport-loss gate locally and on Daytona. It loses only the observed per-turn
run owner while a native mutation is awaiting permission, then requires a visible
failed run, an unanswerable stale approval, an open task, no automatic replay,
no changed target, and retirement of the owned process tree. The remote fault uses
a Linux pidfd bound to the retained start ticks and boot ID.
The initial full test run retained resource/startup failures. Targeted reruns
passed 43 boundary/file-handoff checks, 196 real-runner checks, and 1,742 of 1,744
remaining server checks. The two remaining assertions compare macOS `/var` aliases
against canonical `/private/var` paths; no unrelated test repair is ported.
@@ -0,0 +1,12 @@
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
/** Both public server vendoring and the standalone runner own their assets. */
export function cursorProvisionerPackageRoot(moduleUrl) {
const url = new URL(moduleUrl);
if (url.protocol !== "file:" || url.search || url.hash) throw new Error("Cursor setup requires a published provisioner");
const path = fileURLToPath(url);
if (/\/dist\/vendor\/paperclip-runner\/cli\/provision-cursor\.(?:cjs|js)$/.test(path)) return resolve(dirname(path), "..");
if (/\/dist\/cli\/provision-cursor\.(?:cjs|js)$/.test(path)) return resolve(dirname(path), "../..");
throw new Error("Cursor setup requires a published provisioner");
}
@@ -0,0 +1,13 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { cursorProvisionerPackageRoot } from "./cursor-provisioner-layout.mjs";
test("public server setup owns the same asset root as its vendored runtime", () => {
assert.equal(cursorProvisionerPackageRoot("file:///consumer/node_modules/@paperclipai/server/dist/vendor/paperclip-runner/cli/provision-cursor.cjs"), "/consumer/node_modules/@paperclipai/server/dist/vendor/paperclip-runner");
});
test("standalone runner setup owns package assets", () => {
assert.equal(cursorProvisionerPackageRoot("file:///consumer/node_modules/@paperclipai/paperclip-runner/dist/cli/provision-cursor.cjs"), "/consumer/node_modules/@paperclipai/paperclip-runner");
});
test("unbundled and foreign layouts cannot provision outside an owned package", () => {
for (const url of ["file:///repo/packages/paperclip-runner/scripts/provision-cursor.mjs", "file:///tmp/provision-cursor.cjs", "https://example.com/dist/cli/provision-cursor.js", "file:///consumer/dist/cli/provision-cursor.js?redirect=1"]) assert.throws(() => cursorProvisionerPackageRoot(url), /published provisioner/);
});
@@ -1,13 +1,13 @@
#!/usr/bin/env node
import { lstat } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { join } from "node:path";
import { cursorProvisionerPackageRoot } from "./cursor-provisioner-layout.mjs";
import { cursorDistribution, materializePinnedCursorDistribution, verifyCursorDistribution } from "./materialize-cursor-distribution.mjs";
/** Explicit public installation; never invoked by an npm lifecycle hook. */
export async function provisionCursorRuntime() {
const distribution = await cursorDistribution();
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const packageRoot = cursorProvisionerPackageRoot(import.meta.url);
const destination = join(packageRoot, "provider-assets", "cursor", `${distribution.platform}-${distribution.architecture}`);
const installed = await lstat(destination).catch(error => {
if (error.code !== "ENOENT") throw error;
+5 -1
View File
@@ -119,7 +119,11 @@ if (Object.keys(sortedOptDeps).length > 0) {
}
const output = JSON.stringify(publishPkg, null, 2) + "\n";
const outPath = resolve(repoRoot, "cli/package.json");
const [outputFlag, outputPath, ...extraArgs] = process.argv.slice(2);
if (extraArgs.length || (outputFlag !== undefined && (outputFlag !== "--output" || !outputPath))) {
throw new Error("Usage: generate-npm-package-json.mjs [--output <manifest-path>]");
}
const outPath = outputPath ? resolve(outputPath) : resolve(repoRoot, "cli/package.json");
writeFileSync(outPath, output);
console.log(` ✓ Generated publishable package.json (${Object.keys(sortedDeps).length} deps)`);
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env node
// Public npm graph + explicit setup only. No credentials, inference, publication,
// checkout mount, candidate overrides, or native Cursor assets in the tarballs.
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import { materializePublishManifest, prepareBundledPackage } from './prepare-bundled-package.mjs';
import { grokConsumerDockerArgs, GROK_PUBLIC_INSTALL_LIFECYCLE, GROK_PUBLIC_INSTALL_IMAGE } from './grok-public-install-sandbox.mjs';
const repo = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const root = mkdtempSync(join(tmpdir(), 'paperclip-cursor-public-install-'));
console.log(`Verification evidence: ${root}`);
const env = { ...process.env, NODE_PATH: '', PAPERCLIP_RELEASE_REUSE_UI_DIST: '1', npm_config_ignore_scripts: 'false', npm_config_audit: 'false', npm_config_fund: 'false' };
for (const key of Object.keys(env)) if (/^(OPENAI|ANTHROPIC|OPENROUTER|DAYTONA|XAI|GROK|CURSOR|COPILOT|GITHUB|GH)(_|$)/.test(key) || /CANDIDATE/.test(key)) delete env[key];
const run = (command, args, cwd = root) => execFileSync(command, args, { cwd, env, stdio: 'pipe', maxBuffer: 64 * 1024 * 1024 });
const sourceRevision = run('git', ['rev-parse', 'HEAD'], repo).toString().trim();
const releaseVersion = `0.0.0-cursor-verify.${sourceRevision.slice(0, 12)}`;
const listing = run(process.execPath, [join(repo, 'scripts/release-package-map.mjs'), 'list'], repo).toString().trim().split('\n').map(line => line.split('\t'));
const packages = new Map(listing.map(([dir, name]) => [name, {dir, manifest: JSON.parse(readFileSync(join(repo, dir, 'package.json')))}]));
const cliManifestPath = join(repo, 'cli/package.json'); const original = readFileSync(cliManifestPath);
const generatedManifest = join(root, 'cli-manifest.json');
run(process.execPath, [join(repo, 'scripts/generate-npm-package-json.mjs'), '--output', generatedManifest], repo);
const publishedCli = JSON.parse(readFileSync(generatedManifest));
publishedCli.dependencies['@paperclipai/server'] = releaseVersion;
packages.set('paperclipai', { dir: 'cli', manifest: publishedCli });
const needed = new Set();
function visit(name) {
if (needed.has(name)) return;
const entry = packages.get(name); assert.ok(entry, `Missing public package ${name}`); needed.add(name);
for (const [dependency, version] of Object.entries({...entry.manifest.dependencies, ...entry.manifest.optionalDependencies})) if (version.startsWith('workspace:') || dependency === '@paperclipai/server' && name === 'paperclipai') visit(dependency);
}
visit('paperclipai');
// Run after the candidate build, including its required standalone adapters.
for (const name of needed) assert.ok(existsSync(join(repo, packages.get(name).dir, 'dist')), `Missing built public package ${name}`);
run('bash', [join(repo, 'scripts/prepare-server-ui-dist.sh')], repo);
const assets = join(root, 'assets'); const consumer = join(root, 'consumer'); const cache = join(root, 'cache');
for (const path of [assets, consumer, cache]) mkdirSync(path, { mode: 0o755 });
writeFileSync(join(consumer, 'package.json'), JSON.stringify({ private: true, type: 'module' }));
for (const [index, name] of [...needed].entries()) {
const { dir, manifest } = packages.get(name); const staged = join(root, `source-${index}`); const target = join(root, `package-${index}`); mkdirSync(staged);
for (const file of manifest.files ?? ['dist']) {
const skills = file === 'skills' && ['server', 'packages/adapters/claude-local', 'packages/adapters/codex-local'].includes(dir);
if (file === 'skills' && !skills && !existsSync(join(repo,dir,file))) continue;
cpSync(skills ? join(repo,'skills') : join(repo,dir,file), join(staged,file), { recursive: true });
}
const releaseManifest = { ...manifest, version: releaseVersion };
writeFileSync(join(staged,'package.json'), JSON.stringify(releaseManifest));
if ((manifest.bundleDependencies ?? manifest.bundledDependencies ?? []).length) prepareBundledPackage(staged,target);
else { cpSync(staged,target,{recursive:true}); writeFileSync(join(target,'package.json'),JSON.stringify(materializePublishManifest(releaseManifest))); }
run('npm',['pack','--ignore-scripts','--pack-destination',assets],target);
console.log(`Packed ${name}`);
}
const sentinelSource = join(root,'lifecycle-sentinel'); mkdirSync(sentinelSource);
writeFileSync(join(sentinelSource,'package.json'), JSON.stringify({name:'paperclip-verification-lifecycle-sentinel',version:'1.0.0',private:true,scripts:{postinstall:'node -e "require(\'node:fs\').writeFileSync(\'lifecycle-ran\', \'ok\')"'}}));
run('npm',['pack','--ignore-scripts','--pack-destination',assets],sentinelSource);
for (const file of readdirSync(assets)) chmodSync(join(assets,file),0o644);
const isolated = (command, options={}) => run('docker',grokConsumerDockerArgs({assets,consumer,cache,uid:process.getuid(),gid:process.getgid(),command,...options}));
isolated(['npm','install','--ignore-scripts','--omit=dev',...readdirSync(assets).filter(f=>f.endsWith('.tgz')).map(f=>`/packages/${f}`)],{download:true});
const sentinel = join(consumer,'node_modules/paperclip-verification-lifecycle-sentinel/lifecycle-ran');
assert.equal(existsSync(sentinel),false);
const lock = readFileSync(join(consumer,'package-lock.json'),'utf8');
// Ordinary npm hooks may fetch platform dependency assets (e.g. esbuild).
// Cursor itself must still remain absent until its explicit setup command.
isolated(GROK_PUBLIC_INSTALL_LIFECYCLE, { download: true });
assert.equal(readFileSync(sentinel,'utf8'),'ok'); assert.equal(readFileSync(join(consumer,'package-lock.json'),'utf8'),lock);
const installedRunner = join(consumer,'node_modules/@paperclipai/server/dist/vendor/paperclip-runner');
assert.equal(existsSync(join(installedRunner,'provider-assets/cursor')),false,'npm must never download Cursor');
const setupOutput = isolated(['node','node_modules/paperclipai/dist/index.js','runtime','setup','cursor'],{download:true}).toString();
console.log(setupOutput.trim());
assert.match(setupOutput,/Verified Cursor 2026\.09\.26-dd393fe \(linux-x64\), paperclip-cursor-usage-v4/);
const probe = `import assert from 'node:assert/strict';
import { verifyQualifiedAcpxInstallation } from '/consumer/node_modules/@paperclipai/server/dist/vendor/paperclip-runner/drivers/acpx/installation-integrity.js';
import { resolveQualifiedAcpxProfile } from '/consumer/node_modules/@paperclipai/server/dist/vendor/paperclip-runner/drivers/acpx/qualified-profiles.js';
const profile = resolveQualifiedAcpxProfile('cursor', 'gpt-5.6-luna[context=272k,reasoning=medium,fast=false]');
const installation = await verifyQualifiedAcpxInstallation(profile, () => { throw new Error('Cursor must not resolve an npm package'); });
assert.equal(installation.agentServerPackageJsonPath,null); assert.equal(installation.agentRuntimePackageJsonPath,null);
await (await installation.openCommand()).close();
console.log('Installed Cursor closure and command lease verified without credentials or candidate overrides');`;
writeFileSync(join(assets,'probe.mjs'),probe,{mode:0o644});
console.log(isolated(['node','/packages/probe.mjs']).toString().trim());
for (const name of needed) assert.equal(JSON.parse(readFileSync(join(consumer,'node_modules',name,'package.json'))).version,releaseVersion);
const report = {schema:'paperclip.cursor.public-npm-install.v1',sourceRevision,releaseVersion,consumerImage:GROK_PUBLIC_INSTALL_IMAGE,packageCount:needed.size,sourceCliManifestPreserved:readFileSync(cliManifestPath).equals(original),lifecycleScriptsEnabled:true,lifecycleSentinelVerified:true,lifecycleNetwork:'bridge',consumerLockPreserved:true,npmProvisionedCursor:false,publicSetupCommand:true,pinnedClosureVerified:true,providerCalls:0};
writeFileSync(join(root,'report.json'),JSON.stringify(report,null,2)+'\n'); console.log(JSON.stringify(report));
+3 -3
View File
@@ -136,10 +136,10 @@ describe("runner E2E catalog", () => {
expect(localIntegrityTasks).toHaveLength(2);
expect(openRouterBreadthTasks).toHaveLength(3);
expect(runnerSuites.map((suite) => suite.expectedMatrixSize)).toEqual([
6, 8, 2, 2, 30, 3, 16, 16, 2, 6, 8, 46, 23, 50, 6, 20, 26, 52, 28, 18, 2, 6, 6, 12, 10, 48, 16, 10, 2, 1, 1,
6, 8, 2, 2, 2, 30, 3, 16, 16, 2, 6, 8, 46, 23, 50, 6, 20, 26, 52, 28, 18, 2, 6, 6, 12, 10, 48, 16, 10, 2, 1, 1,
]);
expect(validateRunnerCatalog()).toHaveLength(482);
expect(new Set(runnerMatrix.map((entry) => entry.id)).size).toBe(482);
expect(validateRunnerCatalog()).toHaveLength(484);
expect(new Set(runnerMatrix.map((entry) => entry.id)).size).toBe(484);
expect(
runnerMatrix.filter((entry) => entry.suite.id === "core-compatibility"),
).toHaveLength(48);
+16
View File
@@ -1104,6 +1104,22 @@ export const runnerSuites: readonly RunnerSuiteFixture[] = [
environments: runnerEnvironments, tasks: nativeActiveStopTasks, expectedMatrixSize: 2,
definitionMetadata: { version: 4, qualification: "pending", scheduling: "explicit-only", evidence: "paperclip.e2e.native-active-stop-settlement.v2", pendingObservation: "retained-api-before-caller-uuid-stop", normalCompletionAccepted: false, permissionPolicy: "approve-reads", lifecycle: "per_turn", remoteEvidence: "paperclip.e2e.native-active-stop-remote-retirement.v1", remoteObservationCoverage: "continuous-through-owned-process-retirement", filesystemAfterRemoteRetirementObserved: false, localObservationCoverage: "four-phases-through-cleanup", providerDeath: "not-covered" },
},
{
id: "native-provider-loss", label: "Lose a runtime with an unanswered native permission", manualOnly: true,
description: "Lose the owned Cursor runtime while a native mutation remains unanswered; require a visible failed run, closed unanswerable input, stale-answer refusal and independent retirement with no effects or replay.",
groups: ["native"], profiles: extendedHarnessProfiles.filter(profile => profile.qualificationCandidate === "cursor"),
environments: runnerEnvironments,
tasks: [{
id: "pending-permission-provider-loss", label: "Owned runtime loss with pending permission", groups: [], workMode: "standard", flow: "native_provider_loss",
expectedRunCount: 1, attemptTimeoutMs: { local: 300_000, daytona: 300_000 }, turnTimeoutMs: 120_000,
expectedTerminalState: { issue: "in_progress", run: "failed" },
buildTitle: nonce => `Native provider loss ${nonce}`,
buildVisibleMarker: nonce => `NATIVE-PROVIDER-LOSS-${nonce}`,
buildPrompt: () => "Attempt the supplied exact native shell operation once and leave its permission callback unanswered. Do not retry, use another tool or path, answer your own permission, call paperclip_finish, or claim completion. The qualification fixture will deliberately lose the isolated runtime before any approval.",
buildMatchers: () => [],
}], expectedMatrixSize: 2,
definitionMetadata: { version: 1, qualification: "pending", scheduling: "explicit-only", fault: "observed-per-turn-run-root-loss", remoteFaultAuthority: "pidfd-start-ticks-boot-id", replayAllowed: false },
},
{
id: "rich-acp-warm-continuity", label: "Rich ACP warm continuity", manualOnly: true,
description: "Three browser-driven turns with stable native session, runner process and workspace identity for Cursor.",
+12
View File
@@ -76,6 +76,18 @@ function processTable(): ProcessIdentity[] {
return output.split("\n").flatMap(line => { const m = /^\s*(\d+)\s+(\d+)\s+(.+?)\s*$/u.exec(line); return m ? [{ pid: Number(m[1]), parent: Number(m[2]), start: m[3]! }] : []; });
}
export interface RunProcessAuthority { pid: number; groupId: number; startedAt: string; runId: string }
/** Closed fault injection for the already observed, isolated per-turn run.
* The caller cannot select a signal, executable, or an unobserved process.
*/
export function injectObservedRunLoss(authority: RunProcessAuthority, journal: readonly ProcessIdentity[]) {
const retained = journal.find(row => row.pid === authority.pid);
const current = processTable().find(row => row.pid === authority.pid);
if (!retained || !current || retained.start !== current.start || authority.pid === process.pid) throw new Error("Provider-loss run ownership changed");
const argv = execFileSync("/bin/ps", ["-p", String(current.pid), "-o", "command="], { encoding: "utf8", timeout: 3000, maxBuffer: 65536 });
if (!isPerTurnRunProcess(authority, current, argv) || !processTable().some(row => row.pid === current.pid && row.start === current.start)) throw new Error("Provider-loss run is not the retained per-turn owner");
process.kill(current.pid, "SIGKILL");
return { root: retained, signal: "SIGKILL", observedMonotonicNs: process.hrtime.bigint().toString() };
}
export function isPerTurnRunProcess(authority: RunProcessAuthority, observed: ProcessIdentity, command: string): boolean {
if (!/^[a-zA-Z0-9_-]{1,128}$/u.test(authority.runId) || authority.pid !== observed.pid || authority.groupId !== observed.pid) return false;
const started = Date.parse(authority.startedAt), actual = Date.parse(observed.start);
@@ -0,0 +1,153 @@
import { randomUUID } from "node:crypto";
import { join } from "node:path";
import { expect, type Page } from "@playwright/test";
import { pollUntil, type RunnerApi } from "./api.js";
import { collectRunEvents } from "./run-observations.js";
import { createTaskThroughUi } from "./user-actions.js";
import { createDeniedTargetFixture, exists, observeRunProcesses, injectObservedRunLoss } from "./native-local-fixtures.js";
import { assertNativeRemoteRetirement, nativeRemoteDeniedSample, prepareNativeRemoteAction, type NativeRemoteBootstrap, type NativeRemoteFixture, type NativeRemoteSnapshot } from "./native-remote-evidence.js";
import { cursorDeniedCommand } from "./cursor-native-evidence.js";
import { readActiveStopCaller, observeActiveStopPending, type ActiveStopPending, type ActiveStopScope } from "./native-active-stop-evidence.js";
import type { BootstrapReadProof } from "./native-bootstrap-read-proof.js";
import { bootstrapReadExecutionId } from "./native-bootstrap-read-proof.js";
import type { LiveFixtureValues } from "./live-fixtures.js";
import type { MatrixExecution } from "./types.js";
type Row = Record<string, any>;
type Check = { id: string; passed: boolean; detail: string };
/** Transport loss must never turn an unanswered native mutation into success. */
export function assertProviderLossSettlement(run: Row, issue: Row, events: readonly Row[], pending: ActiveStopPending) {
const fail = (ok: boolean, reason: string) => { if (!ok) throw new Error(`Native provider loss: ${reason}`); };
fail(run.id === pending.scope.runId && run.companyId === pending.scope.companyId && run.nativeIssueId === pending.scope.issueId
&& run.status === "failed" && run.runtimeMode === "native" && typeof run.error === "string" && run.error.length > 0
&& issue.id === pending.scope.issueId && issue.companyId === pending.scope.companyId && issue.status === "in_progress"
&& run.resultJson?.nativeCancellation == null, "failure must leave the task open without claiming Stop or success");
const frames = events.flatMap(row => row.payload?.prpEvent ? [row.payload.prpEvent] : []);
const closures = frames.filter(frame => ["runtime_request.resolved", "runtime_request.cancelled", "runtime_request.expired"].includes(frame.eventType)
&& frame.payload?.requestId === pending.requestId);
fail(closures.length === 1 && ["runtime_request.cancelled", "runtime_request.expired"].includes(closures[0].eventType)
&& closures[0].payload.turnId === pending.turnId && closures[0].payload.requestKind === "permission_approval"
&& closures[0].payload.action === undefined && closures[0].payload.response === undefined && closures[0].payload.replayAllowed !== true,
"undeliverable permission must close without acceptance or replay");
fail(!frames.some(frame => frame.eventType === "turn.completed" || frame.eventType === "completion.accepted"
|| frame.eventType === "tool.execution.completed" && frame.payload?.executionId === bootstrapReadExecutionId(pending.toolCallId)
&& frame.payload?.status === "completed"), "no completion or completed mutation after loss");
return { schema: "paperclip.e2e.native-provider-loss.v1", requestId: pending.requestId, turnId: pending.turnId,
requestClosure: closures[0].eventType, taskStillOpen: true, failedRun: true, replayAllowed: false };
}
export async function runNativeProviderLossFlow(input: {
page: Page; api: RunnerApi; fixtures: LiveFixtureValues; execution: MatrixExecution; nonce: string; workspacePath: string; deadlineAt: number;
remoteBootstrap?: NativeRemoteBootstrap;
registerCleanupAssertion(callback: () => Promise<Check[]>): void;
registerBeforeEnvironmentTeardownAssertion(callback: () => Promise<Check[]>): void;
observe(issue: Row, runs: Row[]): void;
capture(id: string, label: string, file: string): Promise<void>;
evidence(name: string, value: unknown): Promise<void>;
}) {
const { api, page, fixtures, execution, nonce } = input;
if (execution.profile.qualificationCandidate !== "cursor" || execution.task.id !== "pending-permission-provider-loss") throw new Error("Unknown provider-loss case");
const remote = execution.environment.id === "daytona";
if (remote && !input.remoteBootstrap) throw new Error("Provider loss requires its admitted remote observer");
const local = remote ? undefined : await createDeniedTargetFixture(input.workspacePath, `provider-loss-${nonce}.txt`);
const target = local?.targetRelativePath ?? `provider-loss-${nonce}.txt`;
let command = local ? cursorDeniedCommand(local.targetPath) : undefined;
const observer = remote ? undefined : observeRunProcesses();
let issue: Row = {}, runs: Row[] = [], events: Row[] = [];
let fixture: NativeRemoteFixture | undefined, baseline: NativeRemoteSnapshot | undefined, sealed: NativeRemoteSnapshot | undefined;
let processes: ReturnType<ReturnType<typeof observeRunProcesses>["sample"]> | undefined;
let pending: ActiveStopPending | undefined, settled = false;
const checks: Check[] = [], samples: Array<{ phase: string; absent: boolean }> = [];
const check = (id: string, ok: boolean, detail: string) => { checks.push({ id, passed: ok, detail }); expect(ok, detail).toBe(true); };
const scope = (): ActiveStopScope => ({ provider: "cursor", companyId: fixtures.company.id, issueId: issue.id, runId: runs[0]!.id, target, commandSha256: command!.commandSha256 });
const bootstrap = (): BootstrapReadProof | undefined => fixture ? { actionFile: fixture.actionFile, events } : undefined;
const authority = () => { const run = runs[0]!; return { pid: run.processPid, groupId: run.processGroupId, startedAt: run.processStartedAt, runId: run.id }; };
const load = async () => {
if (issue.id) issue = await api.get<Row>(`/api/issues/${issue.id}`);
const list = await api.get<Row[]>(`/api/companies/${fixtures.company.id}/heartbeat-runs?limit=100`);
runs = await Promise.all(list.map(run => api.get<Row>(`/api/heartbeat-runs/${run.id}`)));
if (runs.length > 1) throw new Error("Provider loss automatically dispatched another run");
events = runs[0] ? await collectRunEvents<Row>((afterSeq, limit) => api.get(`/api/heartbeat-runs/${runs[0]!.id}/events?afterSeq=${afterSeq}&limit=${limit}`)) : [];
if (observer && runs[0]?.processPid) processes = observer.sample(authority());
input.observe(issue, runs); return { run: runs[0] ?? {}, issue, events };
};
const sample = async (phase: string) => {
const absent = !await exists(local!.targetPath); samples.push({ phase, absent }); check(`no-effect-${phase}`, absent, "The exact unapproved target stays absent");
};
const cleanup = async () => {
const receipts: Check[] = [];
try {
if (remote) {
if (!fixture || !baseline) throw new Error("Provider-loss observer was not armed");
sealed ??= await fixture.finish(); assertNativeRemoteRetirement(sealed, baseline);
nativeRemoteDeniedSample(sealed, baseline, target, "after-cleanup");
} else {
await pollUntil({ label: "lost run's owned processes retire", deadlineAt: Date.now() + 5000, intervalMs: 100,
load: async () => { await load(); return processes; }, accept: value => Boolean(value?.captured && value.live.length === 0) });
await sample("after-cleanup"); const watch = local!.watcher.finish();
if (!watch.complete || watch.targetMutationCount !== 0 || samples.length !== 4 || !samples.every(row => row.absent)) throw new Error("Provider-loss continuous no-effects evidence is incomplete");
}
await load(); if (!settled || !pending) throw new Error("Provider-loss settlement was not established");
assertProviderLossSettlement(runs[0]!, issue, events, pending);
receipts.push({ id: "provider-loss-retired-no-replay", passed: true, detail: "Owned run and provider processes retired; pending mutation closed without replay or effects" });
return receipts;
} finally {
await input.evidence("native-provider-loss-cleanup.json", { processes, samples, sealed, receipts });
await fixture?.close();
}
};
(remote ? input.registerBeforeEnvironmentTeardownAssertion : input.registerCleanupAssertion)(cleanup);
const prompt = () => `${execution.task.buildPrompt(nonce)}\nUse this exact native shell command once:\n${command!.command}`;
try {
const caller = readActiveStopCaller(await api.get("/api/health"), await api.get("/api/auth/get-session"));
const agent = await api.get<Row>(`/api/agents/${fixtures.agent.id}`);
await api.patch(`/api/agents/${fixtures.agent.id}`, { adapterConfig: { ...agent.adapterConfig, acpxPermissionMode: "approve-reads", acpxSessionMode: "agent", timeoutSec: 120, lifecycleMode: "per_turn" } });
const project = await api.post<Row>(`/api/companies/${fixtures.company.id}/projects`, { name: `Native provider loss ${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 } });
if (!remote) await sample("before-request");
await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name, title: execution.task.buildTitle(nonce), prompt: remote ? input.remoteBootstrap!.prompt(nonce) : prompt(), workMode: "standard", projectName: project.name });
issue = (await pollUntil({ label: "browser-created provider-loss task", deadlineAt: input.deadlineAt,
load: async () => (await api.get<Row[]>(`/api/companies/${fixtures.company.id}/issues?limit=100`)).find(row => row.title === execution.task.buildTitle(nonce)), accept: Boolean }))!;
if (remote) {
await pollUntil({ label: "provider-loss bootstrap", deadlineAt: input.deadlineAt, load, accept: value => value.run.status === "running" });
fixture = await input.remoteBootstrap!.bindAndRelease({ issueId: issue.id, runId: runs[0]!.id, targets: [target], actionPrompt: async value => {
fixture = value; command = cursorDeniedCommand(join(value.remoteCwd, target));
const prepared = await prepareNativeRemoteAction({ fixture: value, companyId: fixtures.company.id, environmentId: fixtures.environment.id, runId: runs[0]!.id, target, prompt: prompt() });
baseline = prepared.baseline; await input.evidence("provider-loss-baseline.json", baseline); return prepared.prompt;
} });
}
await pollUntil({ label: "unanswered native permission before loss", deadlineAt: input.deadlineAt, intervalMs: 200, load,
accept: value => { pending = observeActiveStopPending({ ...value, scope: scope(), caller, cancellationRequestId: randomUUID(), bootstrap: bootstrap() }); return true; } });
await page.goto(`/${fixtures.company.issuePrefix}/issues/${issue.identifier ?? issue.id}`);
const card = page.getByTestId("task-chat-runtime-request").filter({ visible: true });
await expect(card).toHaveCount(1); await expect(card.getByRole("button", { name: "Deny", exact: true })).toBeEnabled();
await input.capture("pending-before-loss", "Unanswered native mutation before runtime loss", "pending-before-loss.png");
await input.evidence("provider-loss-pending.json", pending);
if (remote) {
const snap = await fixture!.snapshot("pending-before-loss"); nativeRemoteDeniedSample(snap, baseline!, target, "pending");
await input.evidence("provider-loss-pending-remote.json", snap);
if (!fixture!.injectOwnedRunLoss) throw new Error("Remote observer cannot inject an owned loss");
await input.evidence("provider-loss-injection.json", await fixture!.injectOwnedRunLoss());
} else {
await sample("pending"); await load();
if (!processes?.captured) throw new Error("Local run ownership was not observed");
await input.evidence("provider-loss-injection.json", injectObservedRunLoss(authority(), processes.journal));
}
await pollUntil({ label: "failed run after owned transport loss", deadlineAt: input.deadlineAt, intervalMs: 200, load,
accept: value => { if (["succeeded", "cancelled"].includes(value.run.status)) throw new Error("Provider loss falsely succeeded or became Stop"); return value.run.status === "failed"; } });
const settlement = assertProviderLossSettlement(runs[0]!, issue, events, pending!); settled = true;
const response = await api.request.post(`/api/heartbeat-runs/${runs[0]!.id}/runtime-requests/${encodeURIComponent(pending!.requestId)}/resolve`,
{ data: { turnId: pending!.turnId, requestKind: "permission_approval", resolution: { action: "accept" } } });
check("stale-acceptance-refused", response.status() === 409, "An approval cannot revive the lost provider or replay its mutation");
if (remote) { sealed = await fixture!.finish(); assertNativeRemoteRetirement(sealed, baseline!); nativeRemoteDeniedSample(sealed, baseline!, target, "terminal"); }
else await sample("after-loss");
await page.reload();
await expect(page.getByTestId("task-chat-thread").getByTestId("task-chat-collapsible-marker").filter({ has: page.getByText("Run failed", { exact: true }) })).toBeVisible({ timeout: 30000 });
await expect(page.getByTestId("task-chat-history-loading")).toHaveCount(0);
await expect(card.getByRole("button", { name: "Deny", exact: true })).toHaveCount(0);
await load(); assertProviderLossSettlement(runs[0]!, issue, events, pending!);
check("one-unfinished-failed-run", runs.length === 1 && issue.status === "in_progress", "No automatic mutation replay or false task completion");
await input.capture("final-state", "Lost runtime fails clearly and leaves the task open", "final-state.png");
await input.evidence("api-state.json", { issue, runs, checks, settlement, runEvents: events, runEventsByRun: [{ runId: runs[0]!.id, events }] });
return { issue, runs, checks };
} finally { await input.evidence("provider-loss-checks.json", { issue, runs, checks, samples, settled }); }
}
@@ -0,0 +1,30 @@
import { expect, it } from "vitest";
import { assertProviderLossSettlement } from "./native-provider-loss-flow.js";
import { bootstrapReadExecutionId } from "./native-bootstrap-read-proof.js";
import type { ActiveStopPending } from "./native-active-stop-evidence.js";
function fixture() {
const pending = { scope: { runId: "run", companyId: "company", issueId: "issue" }, requestId: "permission", turnId: "turn", toolCallId: "mutation" } as ActiveStopPending;
const run = { id: "run", companyId: "company", nativeIssueId: "issue", runtimeMode: "native", status: "failed", error: "provider_transport_failed", resultJson: {} };
const issue = { id: "issue", companyId: "company", status: "in_progress" };
const events = [{ payload: { prpEvent: { eventType: "runtime_request.expired", payload: { requestId: "permission", turnId: "turn", requestKind: "permission_approval" } } } }];
return { pending, run, issue, events };
}
it("allows completed bootstrap reads while fencing the unanswered mutation", () => {
const f = fixture();
f.events.push({ payload: { prpEvent: { eventType: "tool.execution.completed", payload: { executionId: "bootstrap-read", status: "completed" } } } } as any);
expect(assertProviderLossSettlement(f.run, f.issue, f.events, f.pending)).toMatchObject({ failedRun: true, replayAllowed: false });
});
it.each(["succeeded", "cancelled", "timed_out"])("does not certify %s as provider loss", status => {
const f = fixture(); f.run.status = status;
expect(() => assertProviderLossSettlement(f.run, f.issue, f.events, f.pending)).toThrow("failure must leave");
});
it("rejects accepting the stale permission", () => {
const f = fixture(); f.events[0]!.payload.prpEvent.eventType = "runtime_request.resolved";
expect(() => assertProviderLossSettlement(f.run, f.issue, f.events, f.pending)).toThrow("must close");
});
it("rejects a completed mutation even when the final run failed", () => {
const f = fixture();
f.events.push({ payload: { prpEvent: { eventType: "tool.execution.completed", payload: { executionId: bootstrapReadExecutionId(f.pending.toolCallId), status: "completed" } } } } as any);
expect(() => assertProviderLossSettlement(f.run, f.issue, f.events, f.pending)).toThrow("completed mutation");
});
+1 -1
View File
@@ -14,6 +14,7 @@ export interface NativeRemoteSnapshot {
export interface NativeRemoteFixture {
binding: NativeRemoteBinding; remoteCwd: string; actionFile: string;
snapshot(label: string): Promise<NativeRemoteSnapshot>;
injectOwnedRunLoss?(): Promise<unknown>;
setupAttachedCommand(input: { marker: string; markerText: string; delayMs: number }): Promise<{ command: string; commandSha256: string }>;
finish(): Promise<NativeRemoteSnapshot>; readFile(relative: string): Promise<Buffer>; close(): Promise<void>;
}
@@ -63,4 +64,3 @@ export async function prepareNativeRemoteAction(input: {
if (!target?.complete || !target.absent || target.sha256 !== null || target.mutationCount !== 0) throw new Error("Native remote target was present or unobserved before action");
return { baseline, command, prompt: `${input.prompt}\nThe admitted remote workspace is ${f.remoteCwd}.${command ? `\nThe exact supplied command is:\n${command.command}\nDo not inspect or modify fixture code, fabricate its marker, or launch a substitute command.` : ""}` };
}
+1 -1
View File
@@ -18,7 +18,7 @@ export function assertRemoteNativeEvidencePrerequisites(
environment: NodeJS.ProcessEnv,
): void {
if (!executions.some(execution => execution.environment.id === "daytona"
&& ["cursor_native", "native_active_stop"].includes(execution.task.flow))) return;
&& ["cursor_native", "native_active_stop", "native_provider_loss"].includes(execution.task.flow))) return;
const names = ["PAPERCLIP_E2E_DAYTONA_NODE_SHA256", "PAPERCLIP_E2E_DAYTONA_RUNNERD_SHA256"];
const invalid = names.filter(name => !/^sha256:[a-f0-9]{64}$/u.test(environment[name] ?? ""));
if (invalid.length) throw new Error(`Native Daytona evidence requires exact image executable digests: ${invalid.join(", ")}`);
+13 -1
View File
@@ -30,7 +30,7 @@ function relative(value: string): string {
}
// Only closed diagnostic enums cross the remote boundary; SDK errors and output
// are never retained. These diagnostics explain failed evidence, not qualification.
const RPC_PHASES = ["runtime-ready", "install", "wait", "close", "arm", "publish", "snapshot", "attached", "read"] as const;
const RPC_PHASES = ["runtime-ready", "install", "wait", "close", "arm", "publish", "snapshot", "attached", "read", "inject-loss"] as const;
const RPC_CODES = ["node_identity", "sentinel_type", "sentinel", "cwd", "runtime_root_identity", "runtime_binary_identity", "proc_bound", "ambiguous_run_root", "runtime_not_ready", "runtime_identity_changed", "invalid_proc_identity", "invalid_proc_fields", "socket_error", "socket_timeout", "output_bound", "rpc_deadline", "remote_unknown", "transport_failure", "invalid_response", "readiness_deadline"] as const;
type RpcPhase = typeof RPC_PHASES[number];
type RpcCode = typeof RPC_CODES[number];
@@ -174,6 +174,9 @@ const server=net.createServer(socket=>{sockets.add(socket);socket.on('close',()=
child=cp.spawn(process.execPath,['-e','setTimeout(()=>process.exit(0),'+r.delayMs+')'],{env:{PATH:'/usr/bin:/bin'},stdio:'ignore'});child.once('error',()=>{attached.failure='child_start';s.destroy()});child.once('exit',(code,signal)=>{attached.commandExit={code:code??-1,observedAtMs:Date.now(),observedMonotonicNs:process.hrtime.bigint().toString()};if(code!==0||signal){attached.failure='child_failed';s.destroy();return}try{fs.writeFileSync(path.join(config.binding.remoteCwd,r.marker),r.markerText,{flag:'wx'});attached.markerWrittenAtMs=Date.now();attached.markerWrittenMonotonicNs=process.hrtime.bigint().toString();s.end(JSON.stringify({code:0})+'\n')}catch{attached.failure='marker_failed';s.destroy()}});
}catch{attached.failure='client_rejected';s.destroy()}})});srv.listen(clientSocket);attached.server=srv;result={clientScript,clientSocket};
}
else if(r.op==='inject-loss'){const p=sample();if(sealed||!complete||!publishedHash||!root||!p.live.includes(root.pid))throw Error('loss_owner_missing');const original=journal.get(root.pid);if(!original||original.startTicks!==root.startTicks||original.bootId!==root.bootId)throw Error('loss_owner_changed');
const program="import os,signal,sys\npid=int(sys.argv[1]);fd=os.pidfd_open(pid)\ntry:\n s=open('/proc/'+str(pid)+'/stat').read();ticks=s[s.rfind(') ')+2:].split()[19];boot=open('/proc/sys/kernel/random/boot_id').read().strip()\n if ticks!=sys.argv[2] or boot!=sys.argv[3]:raise RuntimeError('identity changed')\n signal.pidfd_send_signal(fd,signal.SIGKILL)\nfinally:os.close(fd)";
const killed=cp.spawnSync('/usr/bin/python3',['-c',program,String(root.pid),root.startTicks,root.bootId],{env:{PATH:'/usr/bin:/bin'},stdio:'pipe',timeout:3000});if(killed.status!==0)throw Error('loss_not_injected');result={root:original,signal:'SIGKILL',observedMonotonicNs:process.hrtime.bigint().toString()};}
else if(r.op==='finish'){result=snapshot();if(!result.complete||!result.processes.captured||result.processes.live.length)throw Error('retirement_unproven');sealed=true;workspaceWatch.close();for(const t of targets.values())t.watch.close();}
else if(r.op==='close'){shutdown(socket);return;}
else throw Error('unknown_operation');
@@ -212,6 +215,7 @@ export interface RemoteNativeFixture {
/** Watchers and exact run-root identity are already armed at return from bind. */
readonly baseline: RemoteNativeSnapshot;
snapshot(label: string): Promise<RemoteNativeSnapshot>;
injectOwnedRunLoss?(): Promise<unknown>;
readFile(path: string): Promise<Buffer>;
publishAction(path: string, text: string): Promise<void>;
setupAttachedCommand(input: { marker: string; markerText: string; delayMs: number }): Promise<{ command: string; commandSha256: string }>;
@@ -442,6 +446,14 @@ export async function bindRemoteNativeFixture(options: RemoteNativeFixtureOption
fail(bytes.length <= 65536 && `sha256:${createHash("sha256").update(bytes).digest("hex")}` === data.sha256, "read_digest");
return bytes;
},
async injectOwnedRunLoss() {
fail(!closed && published && !finished, "loss_fixture_unavailable");
const value = record(await rpc({ op: "inject-loss" }));
const observed = record(value.root), root = baseline.processes.root;
fail(root && observed.pid === root.pid && observed.startTicks === root.startTicks && observed.bootId === root.bootId
&& value.signal === "SIGKILL" && typeof value.observedMonotonicNs === "string" && /^\d+$/u.test(value.observedMonotonicNs), "loss_owner_acknowledgement");
return value;
},
async publishAction(path, text) {
fail(!closed && !published && path === actionFile && typeof text === "string" && Buffer.byteLength(text) <= 16384, "publish_bound");
// Set before RPC: uncertain delivery must never cause a second publish.
+12 -2
View File
@@ -6,6 +6,7 @@ import { warmManagedFileEvidence } from "./warm-managed-files.js";
import { gitFinalizationEvidence, gitStreamingEvidence, setupGitStreamingWorkspace } from "./daytona-git-streaming.js";
import { runsCompletionUpdateProbe, completionQualityControls, completionQualityStatus, judgeCompletionQuality, reserveCompletionQuality, type CompletionQualityRecord } from "./completion-quality.js";
import { runNativeActiveStopFlow } from "./native-active-stop-flow.js";
import { runNativeProviderLossFlow } from "./native-provider-loss-flow.js";
import { runCursorNativeFlow } from "./cursor-native-flow.js";
import { createRemoteNativeBootstrap, createRemoteFixtureClient } from "./remote-native-bootstrap.js";
import { runCleanupWithObservers, verifyCleanupAssertions, type CleanupAssertion } from "./cleanup-verification.js";
@@ -569,7 +570,7 @@ for (const execution of executions) {
const credentials = credentialValues();
const secrets = normalizedSecrets(Object.values(credentials));
const api = new RunnerApi(request);
const companyRunFlow = execution.suite.id === "task-titles" || ["blocker_guidance", "continuation_accounting", "continuation", "context_integrity", "agent_chat", "everyday_workflow", "first_task", "instruction_persistence", "cursor_native", "native_active_stop"].includes(execution.task.flow);
const companyRunFlow = execution.suite.id === "task-titles" || ["blocker_guidance", "continuation_accounting", "continuation", "context_integrity", "agent_chat", "everyday_workflow", "first_task", "instruction_persistence", "cursor_native", "native_active_stop", "native_provider_loss"].includes(execution.task.flow);
const consoleDiagnostics: Array<Record<string, unknown>> = [];
const networkDiagnostics: Array<Record<string, unknown>> = [];
const pageLifecycleDiagnostics: Array<Record<string, unknown>> = [];
@@ -885,7 +886,7 @@ for (const execution of executions) {
nativeInitial = { issueIds: issues.map(value => value.id), agentIds: agents.map(value => value.id), workspaceDigest };
}
const remoteBootstrap = execution.environment.id === "daytona"
&& ["cursor_native", "native_active_stop"].includes(execution.task.flow)
&& ["cursor_native", "native_active_stop", "native_provider_loss"].includes(execution.task.flow)
? createRemoteNativeBootstrap({
api, daytona: await createRemoteFixtureClient(credentials.DAYTONA_API_KEY ?? ""),
companyId: fixtures.company.id, environmentId: fixtures.environment.id, agentId: fixtures.agent.id,
@@ -975,6 +976,15 @@ for (const execution of executions) {
evidence: (name, data) => writeSanitizedJson(snapshotsDir, name, data, secrets),
});
issue = accounting.issue as IssueRecord; selectedRuns = accounting.runs as RunRecord[];
} else if (execution.task.flow === "native_provider_loss") {
const story = await runNativeProviderLossFlow({
page, api, fixtures, execution, nonce, workspacePath, deadlineAt: startedAtMs + deadlineMs,
observe: (currentIssue, currentRuns) => { issue = currentIssue as IssueRecord; selectedRuns = currentRuns as RunRecord[]; },
capture: captureScreenshot, evidence: (name, data) => writeSanitizedJson(snapshotsDir, name, data, secrets),
remoteBootstrap, registerCleanupAssertion, registerBeforeEnvironmentTeardownAssertion,
});
issue = story.issue as IssueRecord; selectedRuns = story.runs as RunRecord[];
matcherResults = story.checks.map(check => ({ matcher: { kind: "json_path" as const, path: `nativeProviderLoss.${check.id}`, expected: true }, passed: check.passed, detail: check.detail }));
} else if (execution.task.flow === "native_active_stop") {
const story = await runNativeActiveStopFlow({
page, api, fixtures, execution, nonce, workspacePath, deadlineAt: startedAtMs + deadlineMs,
+1
View File
@@ -31,6 +31,7 @@ export type RunnerTaskFlow =
| "warm_three_turn"
| "instruction_persistence"
| "native_active_stop"
| "native_provider_loss"
| "cursor_native";
export interface SecretReference {