Reconcile Pi prerequisites with the integrated runtime while holding admission

This commit is contained in:
Dotta committed 2026-10-08 15:01:33 -05:00
1 parent 20ce062701
commit f620e7157b
313 files changed
+13086 -2187

No files matched your search

@@ -6,6 +6,7 @@ import { createRequire } from "node:module";
import childProcess, { execFileSync } from "node:child_process";
import { prepareAdapterExecutionTargetRuntime } from "@paperclipai/adapter-utils/execution-target";
import { captureDirectorySnapshot } from "@paperclipai/adapter-utils/workspace-restore-merge";
import { prepareSandboxManagedRuntime } from "@paperclipai/adapter-utils/sandbox-managed-runtime";
import { agentDirectoryBaselineDigest, agentDirectoryProbeProgram, observeLocalAgentDirectory, probeAgentDirectory, retireAgentDirectoryTransferScratch, agentDirectoryTransferCleanupProgram } from "../services/agent-directory-probe.js";
describe("stable live agent directory observation", () => {
@@ -98,6 +99,51 @@ describe("stable live agent directory observation", () => {
expect(probeAgentDirectory(remote).digest).toBe(agentDirectoryBaselineDigest(baseline));
} finally { await runtime?.cleanupWorkspaceSnapshot?.(); fs.rmSync(remote, { recursive: true, force: true }); }
});
it("keeps restart fallback transfer scratch outside the native memory directory", async () => {
const remoteCwd = `${root}-remote`;
const agentHome = join(remoteCwd, ".paperclip-runtime", "agent-files", "agent", "original-run");
const scratch = join(remoteCwd, ".paperclip-runtime", "paperclip-runner", "agent-file-transfers", "agent", "original-run");
fs.mkdirSync(agentHome, { recursive: true });
fs.cpSync(root, agentHome, { recursive: true });
const target = { kind: "remote" as const, transport: "sandbox" as const, remoteCwd,
// A newly bound controller target may have no native sync hooks. Exercise
// the production command/base64 fallback rather than a transport mock.
runner: { execute: async (input: Parameters<import("@paperclipai/adapter-utils/command-managed-runtime").CommandManagedRuntimeRunner["execute"]>[0]) => ({
stdout: execFileSync(input.command, input.args ?? [], { cwd: input.cwd, input: input.stdin, encoding: "utf8",
env: { PATH: process.env.PATH, ...input.env }, timeout: input.timeoutMs, maxBuffer: 32 * 1024 * 1024 }),
stderr: "", exitCode: 0, signal: null, timedOut: false, pid: null, startedAt: new Date().toISOString(),
}) } };
let runtime: Awaited<ReturnType<typeof prepareAdapterExecutionTargetRuntime>> | undefined;
try {
const baseline = await captureDirectorySnapshot(root);
runtime = await prepareAdapterExecutionTargetRuntime({ target, runId: "restart-transfer", adapterKey: "agent-files",
workspaceLocalDir: root, workspaceRemoteDir: agentHome, runtimeRootDir: scratch,
syncWorkspace: true, workspaceInboundMode: "adopt_remote", workspaceBaseline: baseline,
workspaceGitSnapshot: null, workspaceFileMode: "all", workspaceExclude: [".paperclip-runtime", ".paperclip-runtime/**"] });
const memory = "0123456789abcdef0123456789abcdef\n";
fs.writeFileSync(join(agentHome, "notes", "memory.txt"), memory);
await runtime.restoreWorkspace();
expect(fs.readFileSync(join(root, "notes", "memory.txt"), "utf8")).toBe(memory);
// A transfer must not introduce a reserved path that the product's own
// full agent-directory probe rejects, including after controller recovery.
expect(() => probeAgentDirectory(agentHome)).not.toThrow();
expect(fs.existsSync(join(agentHome, ".paperclip-runtime"))).toBe(false);
expect(runtime.runtimeRootDir).toBe(scratch);
expect(fs.readdirSync(scratch)).toEqual([]);
} finally { await runtime?.cleanupWorkspaceSnapshot?.(); fs.rmSync(remoteCwd, { recursive: true, force: true }); }
});
it.each(["outside", "reserved-root", "relative", "traversal", "trailing-slash", "nul"])("rejects %s transfer scratch before filesystem work", async kind => {
const reserved = join(root, ".paperclip-runtime");
const invalid = {
outside: `${root}-foreign/scratch`, "reserved-root": reserved, relative: ".paperclip-runtime/scratch",
traversal: `${reserved}/../../scratch`, "trailing-slash": `${reserved}/scratch/`, nul: `${reserved}/scratch\0`,
}[kind]!;
await expect(prepareSandboxManagedRuntime({
spec: { transport: "sandbox", provider: "fixture", sandboxId: "owned", remoteCwd: root, apiKey: null },
adapterKey: "agent-files", client: {} as never, workspaceLocalDir: root, runtimeRootDir: invalid,
})).rejects.toThrow("Transfer scratch must remain within the lease runtime tree");
expect(fs.existsSync(reserved)).toBe(false);
});
it.each(["file", "extra-directory", "symlink", "race"])("fails closed on %s in transfer-owned scratch without deleting contents", kind => {
const parent = join(root, ".paperclip-runtime"), scratch = join(parent, "agent-files");
fs.mkdirSync(scratch, { recursive: true });
@@ -225,6 +225,8 @@ describe("persistent agent directories", () => {
await copies.recoverStopped();
expect((await copies.get(companyId, secondId))?.processStoppedAt).toBeNull();
await stop(secondId);
// Make the deferred maintenance retry due after the new owner stop proof.
await db.update(agentInstructionWorkingCopies).set({ nextAttemptAt: null }).where(eq(agentInstructionWorkingCopies.runId, secondId));
await copies.recoverStopped();
}
expect(await copies.get(companyId, secondId)).toMatchObject({ state: "saved", receipt: { cleanupPending: false } });
@@ -556,7 +558,8 @@ describe("persistent agent directories", () => {
}
expect(execute).toHaveBeenCalledTimes(2);
expect(execute).toHaveBeenLastCalledWith(expect.objectContaining({ lease: expect.objectContaining({ id: leaseId, providerLeaseId: "original-sandbox" }),
command: "rm", args: ["-rf", "--", executionRoot], bypassSession: true }));
command: "rm", args: ["-rf", "--", executionRoot,
path.posix.join(remoteCwd, ".paperclip-runtime", "paperclip-runner", "agent-file-transfers", agentId, copy.runId)], bypassSession: true }));
await copies.recoverCaptured();
expect(execute).toHaveBeenCalledTimes(2);
await copies.release(companyId, copy.runId);
@@ -856,6 +859,9 @@ describe("persistent agent directories", () => {
await fs.mkdir(path.join(root, "build"));
await fs.writeFile(path.join(root, "build", "personal.txt"), "cache-like names are still agent files");
const first = await prepare();
const firstScratch = path.join(remoteCwd, ".paperclip-runtime", "paperclip-runner", "agent-file-transfers", agentId, first.runId);
expect(await fs.readdir(firstScratch)).toEqual([]);
await expect(fs.stat(path.join(first.executionRoot, ".paperclip-runtime"))).rejects.toMatchObject({ code: "ENOENT" });
expect(await fs.readFile(path.join(first.executionRoot, "build", "personal.txt"), "utf8")).toBe("cache-like names are still agent files");
await fs.mkdir(path.join(first.executionRoot, "notes"));
await fs.writeFile(path.join(first.executionRoot, "notes", "bytes.bin"), Buffer.from([0, 128, 255]));
@@ -865,6 +871,7 @@ describe("persistent agent directories", () => {
expect((await execFile("git", ["-C", remoteCwd, "status", "--porcelain", "--untracked-files=all"])).stdout).toBe("?? task-only.txt\n");
expect((await copies.collectStopped({ companyId, runId: first.runId, target: executionTarget }))?.state).toBe("saved");
await copies.release(companyId, first.runId);
await expect(fs.stat(firstScratch)).rejects.toMatchObject({ code: "ENOENT" });
expect((await copies.get(companyId, first.runId))?.receipt?.baseline).toBeUndefined();
await expect(fs.stat(first.localRoot)).rejects.toMatchObject({ code: "ENOENT" });
await fs.rm(remoteCwd, { recursive: true });
@@ -1188,8 +1195,8 @@ describe("persistent agent directories", () => {
const execute = vi.fn(), restoreWorkspace = vi.fn(), cleanupWorkspaceSnapshot = vi.fn().mockResolvedValue(undefined);
const executionTarget = { kind: "remote" as const, transport: "sandbox" as const, environmentId, leaseId, remoteCwd, runner: { execute } };
const shell = vi.spyOn(executionTargetTools, "runAdapterExecutionTargetShellCommand").mockResolvedValue({ exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "" });
const stage = vi.spyOn(executionTargetTools, "prepareAdapterExecutionTargetRuntime").mockImplementation(async () => ({ target: executionTarget,
workspaceRemoteDir: path.posix.join(remoteCwd, ".paperclip-runtime", "agent-files", agentId, runId), runtimeRootDir: null,
const stage = vi.spyOn(executionTargetTools, "prepareAdapterExecutionTargetRuntime").mockImplementation(async options => ({ target: executionTarget,
workspaceRemoteDir: path.posix.join(remoteCwd, ".paperclip-runtime", "agent-files", agentId, runId), runtimeRootDir: options.runtimeRootDir ?? null,
assetDirs: {}, additionalSourceDirs: {}, additionalSourceFailures: [], workspaceSyncSnapshot: null, restoreWorkspace, cleanupWorkspaceSnapshot }));
try {
const copy = (await copies.prepare({ ...target(), runId, cwd: home, target: executionTarget }))!;
@@ -300,6 +300,10 @@ describe("registered run instruction copies", () => {
expect((await revisions.readCurrent(target(), board()))?.content).toBe(initial);
expect((await copies.list(companyId, agentId, board()))[0]).toMatchObject({ state: "pending_collection", content: null });
await appendHeartbeatRunEvent(db, { ...target(), runId: copy.runId, eventType: "native.local_process_stopped", stream: "system" });
// Recovery scheduled a retry while stop authority was missing. Make that
// retry due before asking the restarted controller to collect the copy.
await db.update(agentInstructionWorkingCopies).set({ nextAttemptAt: new Date(0) })
.where(eq(agentInstructionWorkingCopies.runId, copy.runId));
await copies.recoverStopped();
expect((await revisions.readCurrent(target(), board()))?.content).toBe("edit before controller restart");
});
@@ -1091,12 +1091,23 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
const companyId = randomUUID();
const assignedAgentId = randomUUID();
const replacementAgentId = randomUUID();
await db.insert(companies).values({
id: companyId,
name: `Chat Test ${companyId.slice(0, 8)}`,
issuePrefix: `C${companyId.replace(/-/g, "").toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
let inserted = false;
// Retired fixtures retain company rows. Retry only the short-prefix unique
// conflict; every other database error must still fail the test immediately.
for (let attempt = 0; attempt < companyPrefixAttempts; attempt += 1) {
const [company] = await db.insert(companies).values({
id: companyId,
name: `Chat Test ${companyId.slice(0, 8)}`,
issuePrefix: nextIssuePrefix(),
requireBoardApprovalForNewAgents: false,
}).onConflictDoNothing({ target: companies.issuePrefix }).returning({ id: companies.id });
if (company) {
inserted = true;
fixtureCompanies.add(companyId);
break;
}
}
if (!inserted) throw new Error(`Could not allocate a chat fixture company prefix after ${companyPrefixAttempts} attempts`);
const now = new Date();
await db
.insert(authUsers)
@@ -1614,6 +1625,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
await service.processPendingPublications();
const providerRuntime = fakeRuntime.endpoints.get(endpointId);
if (providerRuntime) providerRuntime.posts.length = 0;
return setupFollowUpMessageId;
}
async function configuredSlackEndpoint(
@@ -28229,8 +28241,18 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
}),
trigger: "direct_message",
});
await qualifySetupRoundTrip(service, endpoint.id, userId);
const setupMessageId = await qualifySetupRoundTrip(service, endpoint.id, userId);
await service.test(endpoint.id, "owner-user");
if (provider === "telegram") {
// Setup dispatches receipt cleanup asynchronously. Finish it before
// measuring reaction removals owned by this fixture's working run.
await service.processPendingReceiptReactions();
await waitForProcessedReceiptRemoval(endpoint.id, {
threadId: thread.thread.id,
messageId: setupMessageId,
emoji: "eyes",
});
}
const [conversation] = await service.listConversations(endpoint.id);
const runId = randomUUID();
await db.insert(heartbeatRuns).values({
@@ -62562,7 +62584,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
},
);
it("orders reversed GitHub edit and delete callbacks behind their durable root", async () => {
it.each([false, true])("orders reversed GitHub edit and delete callbacks behind their durable root (root processed before replay: %s)", async (rootProcessedFirst) => {
const fixture = await seedCompany();
const deferred: Array<() => void | Promise<void>> = [];
const { callbacks, endpoint, service, wakeup, webhookSecret } =
@@ -62665,8 +62687,28 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
// One root-conversation drain plus one durable-ingress callback per HTTP
// request is queued. The duplicate delivery callback becomes a no-op.
expect(deferred).toHaveLength(4);
if (rootProcessedFirst) {
await deferred.shift()?.();
await vi.waitFor(async () => {
const [root] = await db
.select({ state: chatDeliveries.state })
.from(chatDeliveries)
.where(eq(chatDeliveries.endpointId, endpoint.id));
expect(root.state).toBe("processed");
});
}
await drainDeferred();
// The scheduler callbacks start background promises. Wait for durable
// replay admission before draining the conversation work it schedules.
await vi.waitFor(async () => {
const deliveries = await db
.select({ id: chatDeliveries.id })
.from(chatDeliveries)
.where(eq(chatDeliveries.endpointId, endpoint.id));
expect(deliveries).toHaveLength(3);
});
await vi.waitFor(async () => {
await drainDeferred();
const deliveries = await db
.select({
eventKind: chatDeliveries.eventKind,
@@ -12,6 +12,7 @@ const mockUpdateExecutionWorkspace = vi.hoisted(() => vi.fn());
const mockLogActivity = vi.hoisted(() => vi.fn());
const mockLoggerInfo = vi.hoisted(() => vi.fn());
const mockGetEnvironment = vi.hoisted(() => vi.fn());
const mockGetLease = vi.hoisted(() => vi.fn());
vi.mock("../services/environment-execution-target.js", () => ({
resolveEnvironmentExecutionTarget: mockResolveEnvironmentExecutionTarget,
@@ -30,6 +31,7 @@ vi.mock("../services/environments.js", () => ({
environmentService: vi.fn(() => ({
ensureLocalEnvironment: vi.fn(),
getById: mockGetEnvironment,
getLeaseById: mockGetLease,
acquireLease: vi.fn(),
releaseLease: vi.fn(),
updateLeaseMetadata: mockUpdateLeaseMetadata,
@@ -815,3 +817,56 @@ describe("admitted native lifecycle recovery", () => {
expect(environment.config.reuseLease).toBe(false);
});
});
describe("live remote runner lease reattachment", () => {
const input = {
companyId: "company-1", selectedEnvironmentId: "env-1", localEnvironmentId: "local",
adapterType: "paperclip_runner", admittedLifecycleMode: "per_turn" as const,
issueId: "task-1", heartbeatRunId: "run-1", agentId: "agent-1",
persistedExecutionWorkspace: { id: "ew-1", mode: "shared_workspace" as const },
executionWorkspaceSettings: null,
reattachRemoteLease: { leaseId: "lease-1", providerLeaseId: "original-sandbox", remoteCwd: "/remote/workspace" },
};
const originalLease = () => makeLease({
provider: "daytona", providerLeaseId: "original-sandbox", issueId: "task-1", executionWorkspaceId: "ew-1",
metadata: { agentId: "agent-1", remoteCwd: "/remote/workspace",
sandboxLeaseAcquisition: { outcome: "created", providerLeaseId: "original-sandbox" } },
});
beforeEach(() => {
vi.clearAllMocks();
mockGetEnvironment.mockResolvedValue({ ...makeEnvironment("sandbox"), config: { provider: "daytona", reuseLease: false } });
mockGetLease.mockResolvedValue(originalLease());
});
it("reattaches the original active ephemeral lease without acquiring or resuming a sandbox", async () => {
const savedLease = originalLease();
mockGetLease.mockResolvedValue(savedLease);
const runtime = makeMockRuntime();
const result = await environmentRunOrchestrator({} as never, { environmentRuntime: runtime }).acquireForRun(input);
expect(mockGetLease).toHaveBeenCalledWith("lease-1");
expect(runtime.acquireRunLease).not.toHaveBeenCalled();
expect(result.lease).toEqual(savedLease);
expect(result.leaseContext.executionWorkspaceId).toBe("ew-1");
expect(result.environment.config.reuseLease).toBe(false);
});
it.each([
{ companyId: "foreign" }, { environmentId: "foreign" }, { heartbeatRunId: "another-run" },
{ issueId: "another-task" }, { executionWorkspaceId: "another-workspace" },
{ providerLeaseId: "replacement" }, { provider: "another-provider" },
{ status: "released" }, { expiresAt: new Date(0) }, { releasedAt: new Date() }, { cleanupStatus: "pending" },
{ metadata: { agentId: "another-agent", remoteCwd: "/remote/workspace" } },
{ metadata: { agentId: "agent-1", remoteCwd: "/other/workspace" } },
] as Partial<EnvironmentLease>[])("fails closed before provider acquisition for a mismatched lease %j", async (override) => {
mockGetLease.mockResolvedValue({ ...originalLease(), ...override });
const runtime = makeMockRuntime();
await expect(environmentRunOrchestrator({} as never, { environmentRuntime: runtime }).acquireForRun(input))
.rejects.toThrow("native_remote_recovery_lease_mismatch");
expect(runtime.acquireRunLease).not.toHaveBeenCalled();
});
it("never replaces a missing original lease", async () => {
mockGetLease.mockResolvedValue(null);
const runtime = makeMockRuntime();
await expect(environmentRunOrchestrator({} as never, { environmentRuntime: runtime }).acquireForRun(input))
.rejects.toThrow("native_remote_recovery_lease_mismatch");
expect(runtime.acquireRunLease).not.toHaveBeenCalled();
});
});
@@ -6679,13 +6679,24 @@ describeEmbeddedPostgres("environmentRuntimeService", () => {
});
});
it("destroys scoped reusable plugin-backed sandbox leases", async () => {
it.each([
{ status: "active", scope: "workspace" },
{ status: "failed", scope: "workspace" },
{ status: "failed", scope: "issue" },
] as const)("destroys $status reusable plugin-backed sandbox leases scoped to an $scope", async ({ status, scope }) => {
const { pluginId, companyId, runId, executionWorkspaceId, reusableLease } =
await seedReusablePluginSandboxLease();
await db
.update(heartbeatRuns)
.set({ status: "succeeded" })
.set({ status: status === "failed" ? "failed" : "succeeded" })
.where(eq(heartbeatRuns.id, runId));
if (status === "failed") await environmentService(db).releaseLease(reusableLease.id, "failed");
const issueId = randomUUID();
if (scope === "issue") {
await db.insert(issues).values({ id: issueId, companyId, title: "Failed reusable lease cleanup", status: "done", priority: "medium" });
await db.update(environmentLeases).set({ issueId }).where(eq(environmentLeases.id, reusableLease.id));
}
const selection = scope === "issue" ? { issueId } : { executionWorkspaceId };
const workerManager = {
isRunning: vi.fn((id: string) => id === pluginId),
@@ -6699,9 +6710,12 @@ describeEmbeddedPostgres("environmentRuntimeService", () => {
} as unknown as PluginWorkerManager;
const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager });
// A known issue/workspace id does not authorize another company's teardown.
expect(await runtimeWithPlugin.destroyReusableSandboxLeases({ companyId: randomUUID(), ...selection })).toEqual([]);
expect(workerManager.call).not.toHaveBeenCalled();
const destroyed = await runtimeWithPlugin.destroyReusableSandboxLeases({
companyId,
executionWorkspaceId,
...selection,
failureReason: "execution_workspace_closed",
});
@@ -6724,9 +6738,10 @@ describeEmbeddedPostgres("environmentRuntimeService", () => {
});
});
it("does not destroy a scoped reusable lease while its run is active", async () => {
it.each(["active", "failed"] as const)("does not destroy a %s scoped reusable lease while its run is active", async (status) => {
const { pluginId, companyId, executionWorkspaceId, reusableLease } =
await seedReusablePluginSandboxLease();
if (status === "failed") await environmentService(db).releaseLease(reusableLease.id, "failed");
const workerManager = {
isRunning: vi.fn((id: string) => id === pluginId),
call: vi.fn(async () => undefined),
@@ -6752,9 +6767,106 @@ describeEmbeddedPostgres("environmentRuntimeService", () => {
expect(workerManager.call).not.toHaveBeenCalled();
await expect(
environmentService(db).getLeaseById(reusableLease.id),
).resolves.toMatchObject({ status: "active" });
).resolves.toMatchObject({ status });
});
it.each(["issue", "workspace", "environment"] as const)(
"durably claims failed reusable cleanup before %s teardown and recovers a provider failure",
async (scope) => {
const seeded = await seedReusablePluginSandboxLease();
await db.update(heartbeatRuns).set({ status: "failed" }).where(eq(heartbeatRuns.id, seeded.runId));
await environmentService(db).releaseLease(seeded.reusableLease.id, "failed");
const issueId = randomUUID();
if (scope === "issue") {
await db.insert(issues).values({ id: issueId, companyId: seeded.companyId, title: "Cleanup crash window", status: "done" });
await db.update(environmentLeases).set({ issueId }).where(eq(environmentLeases.id, seeded.reusableLease.id));
}
let enterProvider!: () => void;
let finishProvider!: () => void;
const entered = new Promise<void>((resolve) => { enterProvider = resolve; });
const finish = new Promise<void>((resolve) => { finishProvider = resolve; });
let unavailable = true;
const call = vi.fn(async (_id: string, method: string) => {
if (method !== "environmentDestroyLease") throw new Error(`Unexpected method ${method}`);
if (unavailable) {
enterProvider();
await finish;
throw new Error("Provider response lost");
}
return { providerLeaseId: seeded.reusableLease.providerLeaseId, state: "destroyed" };
});
const makeRuntime = () => environmentRuntimeService(db, { pluginWorkerManager: {
isRunning: () => true, call,
getWorker: () => ({ supportedMethods: ["environmentDestroyLease"] }),
} as unknown as PluginWorkerManager });
const runtime = makeRuntime();
const close = (service: ReturnType<typeof makeRuntime>) => scope === "environment"
? service.destroyReusableSandboxLeasesForEnvironment({ environmentId: seeded.environment.id })
: service.destroyReusableSandboxLeases({ companyId: seeded.companyId,
...(scope === "issue" ? { issueId } : { executionWorkspaceId: seeded.executionWorkspaceId }) });
const closing = close(runtime);
try {
await entered;
// This independently persisted row is what survives controller loss
// during the provider call. No provider receipt has arrived yet.
expect(await environmentService(db).getLeaseById(seeded.reusableLease.id)).toMatchObject({
status: "pending_cleanup", cleanupStatus: "failed", companyId: seeded.companyId,
providerLeaseId: seeded.reusableLease.providerLeaseId,
metadata: { pendingCleanupAttemptId: expect.any(String), pendingCleanupInFlight: true,
pendingCleanupLeaseExpiresAtMs: expect.any(Number) },
});
await close(makeRuntime());
expect(await heartbeatService(db, { environmentRuntime: makeRuntime() }).sweepPendingCleanupLeases({ backoffMs: 0 }))
.toEqual({ swept: 0, destroyed: 0, capped: 0 });
expect(call).toHaveBeenCalledOnce();
} finally {
finishProvider();
await closing;
}
expect(await environmentService(db).getLeaseById(seeded.reusableLease.id)).toMatchObject({
status: "pending_cleanup", metadata: { pendingCleanupInFlight: false },
});
unavailable = false;
expect(await heartbeatService(db, { environmentRuntime: makeRuntime() }).sweepPendingCleanupLeases({ backoffMs: 0 }))
.toEqual({ swept: 1, destroyed: 1, capped: 0 });
expect(call).toHaveBeenCalledTimes(2);
expect(await environmentService(db).getLeaseById(seeded.reusableLease.id)).toMatchObject({
status: "expired", cleanupStatus: "success", metadata: { remoteExecutionTermination: { state: "destroyed" } },
});
},
);
it.each(["queued", "scheduled_retry", "running"] as const)(
"fences scoped cleanup when its holding run becomes %s after selection", async (status) => {
const seeded = await seedReusablePluginSandboxLease();
await db.update(heartbeatRuns).set({ status: "failed" }).where(eq(heartbeatRuns.id, seeded.runId));
await environmentService(db).releaseLease(seeded.reusableLease.id, "failed");
const originalService = environmentsModule.environmentService;
const serviceSpy = vi.spyOn(environmentsModule, "environmentService").mockImplementation((client) => {
const service = originalService(client);
return { ...service, getById: async (id: string) => {
const environment = await service.getById(id);
if (id === seeded.environment.id) {
await db.update(heartbeatRuns).set({ status }).where(eq(heartbeatRuns.id, seeded.runId));
}
return environment;
} };
});
const call = vi.fn(async () => undefined);
const runtime = environmentRuntimeService(db, { pluginWorkerManager: {
isRunning: () => true, call, getWorker: () => ({ supportedMethods: ["environmentDestroyLease"] }),
} as unknown as PluginWorkerManager });
try {
expect(await runtime.destroyReusableSandboxLeases({ companyId: seeded.companyId,
executionWorkspaceId: seeded.executionWorkspaceId })).toEqual([]);
expect(call).not.toHaveBeenCalled();
expect(await originalService(db).getLeaseById(seeded.reusableLease.id)).toMatchObject({ status: "failed" });
} finally {
serviceSpy.mockRestore();
}
},
);
it("destroys reusable plugin-backed sandbox leases scoped to an environment", async () => {
const { pluginId, runId, reusableLease } = await seedReusablePluginSandboxLease();
// The holding run is finished, so the reservation is stale and destroyable.
@@ -6796,9 +6908,51 @@ describeEmbeddedPostgres("environmentRuntimeService", () => {
});
});
it("keeps a reusable lease held by an in-flight run out of the environment-scoped destroy", async () => {
it("destroys a failed reusable sandbox after successful release before deleting its environment", async () => {
const { pluginId, companyId, runId, environment, reusableLease } = await seedReusablePluginSandboxLease();
const workerManager = {
isRunning: vi.fn((id: string) => id === pluginId),
call: vi.fn(async (_pluginId: string, method: string) => {
if (method === "environmentReleaseLease") {
return { providerLeaseId: reusableLease.providerLeaseId, state: "stopped" };
}
if (method === "environmentDestroyLease") {
// The provider call must already have a durable cleanup reference.
expect(await environmentService(db).getLeaseById(reusableLease.id)).toMatchObject({
status: "pending_cleanup", environmentId: environment.id,
});
return { providerLeaseId: reusableLease.providerLeaseId, state: "destroyed" };
}
throw new Error(`Unexpected plugin method: ${method}`);
}),
getWorker: vi.fn(() => ({ supportedMethods: ["environmentReleaseLease", "environmentDestroyLease"] })),
} as unknown as PluginWorkerManager;
const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager });
await db.update(heartbeatRuns).set({ status: "failed" }).where(eq(heartbeatRuns.id, runId));
const released = await runtimeWithPlugin.releaseRunLeases(runId, "failed", undefined, undefined, true);
expect(released[0]?.lease).toMatchObject({
id: reusableLease.id, status: "failed", cleanupStatus: "success",
metadata: { remoteExecutionTermination: { state: "stopped" } },
});
const result = await runtimeWithPlugin.destroyReusableSandboxLeasesForEnvironment({
environmentId: environment.id, failureReason: "environment_deleted",
});
expect(result).toEqual({ destroyed: 1, failed: 0, skippedLiveRun: 0 });
expect(workerManager.call).toHaveBeenCalledWith(pluginId, "environmentDestroyLease",
expect.objectContaining({ companyId, environmentId: environment.id, providerLeaseId: reusableLease.providerLeaseId }),
31234);
await expect(environmentService(db).getLeaseById(reusableLease.id)).resolves.toMatchObject({
status: "expired", cleanupStatus: "success",
metadata: { remoteExecutionTermination: { state: "destroyed" } },
});
expect((await environmentService(db).removeIfDeletable(environment.id))?.id).toBe(environment.id);
});
it.each(["active", "failed"] as const)("keeps a %s reusable lease held by an in-flight run out of the environment-scoped destroy", async (status) => {
const { pluginId, reusableLease } = await seedReusablePluginSandboxLease();
// seedEnvironment leaves the holding run in `running` status.
if (status === "failed") await environmentService(db).releaseLease(reusableLease.id, "failed");
const workerManager = {
isRunning: vi.fn((id: string) => id === pluginId),
@@ -6816,7 +6970,7 @@ describeEmbeddedPostgres("environmentRuntimeService", () => {
expect(workerManager.call).not.toHaveBeenCalled();
// The lease keeps its reusable status, so the delete guard still blocks.
await expect(environmentService(db).getLeaseById(reusableLease.id)).resolves.toMatchObject({
status: "active",
status,
leasePolicy: "reuse_by_environment",
});
});
@@ -636,6 +636,18 @@ describeEmbeddedPostgres("environmentService leases", () => {
expect(releasedImpact?.reusableSandboxLeaseCount).toBe(1);
expect(await svc.removeIfDeletable(environmentId)).toBeNull();
// A failed run can release its reusable sandbox successfully without
// destroying it. The provider handle still needs environment-scoped cleanup.
await svc.releaseLease(lease.id, "failed", {
failureReason: "adapter_or_run_failure",
cleanupStatus: "success",
});
const failedImpact = await svc.getDeleteBlastRadius(environmentId);
expect.soft(failedImpact?.canDelete).toBe(false);
expect.soft(failedImpact?.deleteBlockedReasons).toContain("reusable_sandbox_lease");
expect.soft(failedImpact?.reusableSandboxLeaseCount).toBe(1);
expect(await svc.removeIfDeletable(environmentId)).toBeNull();
const rows = await db.select().from(environments).where(eq(environments.id, environmentId));
expect(rows).toHaveLength(1);
const storedLease = await svc.getLeaseById(lease.id);
@@ -134,50 +134,65 @@ describe("workspace Git scan route load regression", () => {
...unavailableMethods(),
};
const app = createLoadApp(db, companyId, service);
const pendingResponses = Array.from({ length: 500 }, (_, index) => request(app)
// Model one busy API server, not 500 independent ephemeral listeners whose
// accept/close races can reset clients before they reach the scheduler.
const server = app.listen(0, "127.0.0.1");
await new Promise<void>(resolve => server.once("listening", resolve));
const pendingResponses = Array.from({ length: 500 }, (_, index) => request(server)
.get(`/api/issues/issue-${index}/file-resources/list`)
.set("x-test-actor", `actor-${index % 73}`)
.query({ mode: "changed" })
.then((response) => response));
// Observe every client rejection immediately, including on assertion failure.
const settledResponses = Promise.allSettled(pendingResponses);
try {
await vi.waitFor(
() => expect(scheduler.snapshot().totals.singleFlightJoins).toBe(498),
{ timeout: 15_000, interval: 20 },
);
const loadedSnapshot = scheduler.snapshot();
expect(loadedSnapshot).toMatchObject({ activeCount: 2, queuedCount: 0, inFlightCount: 2 });
await vi.waitFor(
() => expect(scheduler.snapshot().totals.singleFlightJoins).toBe(498),
{ timeout: 15_000, interval: 20 },
);
const loadedSnapshot = scheduler.snapshot();
expect(loadedSnapshot).toMatchObject({ activeCount: 2, queuedCount: 0, inFlightCount: 2 });
const healthLatencies: number[] = [];
for (let index = 0; index < 25; index += 1) {
const startedAt = performance.now();
const response = await request(server).get("/api/health");
healthLatencies.push(performance.now() - startedAt);
expect(response.status).toBe(200);
}
healthLatencies.sort((left, right) => left - right);
const healthP99Ms = healthLatencies[Math.ceil(healthLatencies.length * 0.99) - 1]!;
expect(healthP99Ms).toBeLessThan(250);
const healthLatencies: number[] = [];
for (let index = 0; index < 25; index += 1) {
const startedAt = performance.now();
const response = await request(app).get("/api/health");
healthLatencies.push(performance.now() - startedAt);
expect(response.status).toBe(200);
releaseScans();
const responses = (await settledResponses).map(result => {
if (result.status === "rejected") throw result.reason;
return result.value;
});
const outcomeCounts = responses.reduce<Record<number, number>>((counts, response) => {
counts[response.status] = (counts[response.status] ?? 0) + 1;
return counts;
}, {});
expect(outcomeCounts).toEqual({ 200: 500 });
expect({ runnerCalls, peakActive }).toEqual({ runnerCalls: 2, peakActive: 2 });
expect(scheduler.snapshot()).toMatchObject({ activeCount: 0, queuedCount: 0, inFlightCount: 0 });
console.info("workspace Git scan regression metrics", {
requests: responses.length,
repositories: roots.length,
underlyingScans: runnerCalls,
peakActive,
peakQueued: loadedSnapshot.queuedCount,
outcomes: outcomeCounts,
healthP99Ms: Math.round(healthP99Ms * 100) / 100,
unreapedChildCount: 0,
});
} finally {
releaseScans();
server.closeAllConnections();
await settledResponses;
await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
}
healthLatencies.sort((left, right) => left - right);
const healthP99Ms = healthLatencies[Math.ceil(healthLatencies.length * 0.99) - 1]!;
expect(healthP99Ms).toBeLessThan(250);
releaseScans();
const responses = await Promise.all(pendingResponses);
const outcomeCounts = responses.reduce<Record<number, number>>((counts, response) => {
counts[response.status] = (counts[response.status] ?? 0) + 1;
return counts;
}, {});
expect(outcomeCounts).toEqual({ 200: 500 });
expect({ runnerCalls, peakActive }).toEqual({ runnerCalls: 2, peakActive: 2 });
expect(scheduler.snapshot()).toMatchObject({ activeCount: 0, queuedCount: 0, inFlightCount: 0 });
console.info("workspace Git scan regression metrics", {
requests: responses.length,
repositories: roots.length,
underlyingScans: runnerCalls,
peakActive,
peakQueued: loadedSnapshot.queuedCount,
outcomes: outcomeCounts,
healthP99Ms: Math.round(healthP99Ms * 100) / 100,
unreapedChildCount: 0,
});
}, 60_000);
it.each([
@@ -1,5 +1,5 @@
import { ensureNativeCompletionContract } from "../services/native-runtime/completion-contracts.js";
import { readNativeCursorPlanWait, hasCommittedNativeCursorPlanWait } from "../services/native-runtime/native-cursor-plan-wait.js";
import { readNativePlanWait, hasCommittedNativePlanWait } from "../services/native-runtime/native-plan-wait.js";
import { nativeSha256 } from "../services/native-runtime/canonical.js";
import { buildQuestionResponseDeliveryEnvelope } from "../services/question-response-delivery.js";
import * as executionContinuation from "../services/execution-continuation.js";
@@ -7930,8 +7930,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
});
expect(next?.contextSnapshot?.wakeCommentIds).toEqual([pending!.id, go!.id]);
expect((await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.id, deferred!.id)))[0]).toMatchObject({ status: "coalesced", runId: next!.id });
await heartbeat.drainActiveRunExecutions();
expect((await heartbeat.getRun(next!.id))?.status).not.toBe("running");
await vi.waitFor(async () => expect((await heartbeat.getRun(next!.id))?.status).not.toBe("running"));
});
it.each(["dedicated deferred donor", "non-coalescing recipient", "persistent agent conversation"] as const)(
@@ -14537,226 +14536,6 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
expect(recoveryAction.ownerType).toBe("board");
});
describe("accepted Cursor planning boundaries", () => {
const nativeImplementation = mockExecutePaperclipNativeSession.getMockImplementation();
beforeEach(() => {
mockExecutePaperclipNativeSession.mockImplementation(async () => { throw new Error("Accepted-plan tests must never execute a provider"); });
});
afterEach(() => { mockExecutePaperclipNativeSession.mockImplementation(nativeImplementation!); });
async function seedAcceptedCursorPlanWait(semanticFinish = false, sourceSequenceOffset = 0) {
const f = await seedStrandedIssueFixture({ status: "in_progress", runStatus: "succeeded", livenessState: "advanced" });
const instance = randomUUID(), interactionId = randomUUID();
const contractInput = { db, companyId: f.companyId,
issue: { id: f.issueId, title: "Review the plan before further work", description: "Explicit completion required" }, actorId: "test" };
const { row: persistedContract, contract } = await ensureNativeCompletionContract(contractInput);
const reused = await ensureNativeCompletionContract(contractInput);
expect(reused.row.id).toBe(persistedContract.id);
expect(reused.contract).toEqual(contract);
const contractId = persistedContract.id, contractSha = persistedContract.canonicalSha256;
// Production binds policy/schema as well as the body; a body-only hash is not a valid contract receipt.
expect(contractSha).not.toBe(nativeSha256(contract));
const model = "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]";
await db.update(agents).set({ adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "cursor", model, acpxSessionMode: "plan", acpxPermissionMode: "approve-all" } }).where(eq(agents.id, f.agentId));
await db.update(agentWakeupRequests).set({ status: "completed" }).where(eq(agentWakeupRequests.id, f.wakeupRequestId));
await db.update(heartbeatRuns).set({ runtimeMode: "native", nativeIssueId: f.issueId, nativeSessionId: f.runId,
completionContractId: contractId, completionContractSha256: contractSha, runnerInstanceId: instance,
runnerProfileJson: { nativeExecutionInput: { binding: { companyId: f.companyId, issueId: f.issueId, runId: f.runId, agentId: f.agentId }, provider: { kind: "acpx", agent: "cursor", model, cursorMode: "plan", permissionMode: "approve-all",
profile: paperclipRunner.resolveQualifiedAcpxProfile("cursor", model) }, session: { normalizedSessionId: f.runId },
completionContract: { id: contractId, sha256: contractSha, contract } } } }).where(eq(heartbeatRuns.id, f.runId));
const planId = `plan-${"a".repeat(64)}`;
const questionSet = { schema: "paperclip.question_set.v1", title: "Native plan", description: "Exact accepted revision", questions: [{ id: planId, prompt: "Proceed?", required: true,
answerMode: "single_select", options: [{ id: "accept", label: "Accept" }, { id: "reject", label: "Reject" }, { id: "cancel", label: "Cancel" }] }] };
const [interaction] = await db.insert(issueThreadInteractions).values({ id: interactionId, companyId: f.companyId, issueId: f.issueId, sourceRunId: f.runId,
createdByAgentId: f.agentId, kind: "ask_user_questions", status: "answered", continuationPolicy: "none",
idempotencyKey: `paperclip-runner-question:${f.runId}:request`, resolvedByUserId: "responsible-user", resolvedAt: new Date(Date.now() - 1000),
payload: { version: 1, questions: [{ id: planId, prompt: "Proceed?", selectionMode: "single", required: true, allowOther: false, options: [{ id: "accept", label: "Accept" }, { id: "reject", label: "Reject" }, { id: "cancel", label: "Cancel" }] }], runtimeRequestId: "request", questionSet: questionSet as never }, result: { version: 1, answers: [{ questionId: planId, optionIds: ["accept"] }] } }).returning();
const answer = buildQuestionResponseDeliveryEnvelope(interaction as never);
const [delivery] = await db.insert(issueQuestionResponseDeliveries).values({ companyId: f.companyId, issueId: f.issueId, interactionId,
sourceRunId: f.runId, targetRunId: f.runId, correlationId: randomUUID(), status: "delivered", deliveryMode: "steered", acknowledgedAt: new Date(), payloadSha256: nativeSha256(answer) }).returning();
const port = new PaperclipControlPlanePort(db, { companyId: f.companyId, issueId: f.issueId, runId: f.runId, agentId: f.agentId,
sessionId: f.runId, completionContractId: contractId, completionContractSha256: contractSha, sourceInstanceId: instance, controlPlaneSourceInstanceId: `control-${f.runId}` });
const events = [
{ eventType: "runtime_request.created", payload: { request: { schema: "paperclip.runtime_request.v2", requestKind: "runtime", requestId: "request", type: "input", status: "pending", turnId: "turn", itemId: "native-plan-tool", prompt: "Review plan",
origin: { adapter: "acpx-runtime-sidecar", provider: "cursor", method: "cursor/create_plan" }, input: questionSet } } },
{ eventType: "tool.execution.started", payload: { schema: "paperclip.tool.execution.v1", executionId: "native-plan-tool", transport: "builtin", operation: "execute", status: "running", readOnly: false, namespace: null, name: "Create Plan", target: null, inputUpdated: true, output: null, outputBytes: 0, outputTruncated: false, outputDigest: null, progress: null, exitCode: null, durationMs: null } },
{ eventType: "runtime_request.resolved", payload: { requestId: "request", turnId: "turn", action: "submit", response: answer.response } },
{ eventType: "tool.execution.completed", payload: { schema: "paperclip.tool.execution.v1", executionId: "native-plan-tool", transport: "builtin", operation: "execute", status: "completed", readOnly: false, namespace: null, name: "Create Plan", target: null, inputUpdated: false, output: null, outputBytes: 0, outputTruncated: false, outputDigest: null, progress: null, exitCode: null, durationMs: null } },
{ eventType: "turn.completed", payload: { status: "completed", error: null } },
];
for (const [index, event] of events.entries()) await port.appendEvent({ schema: "paperclip.prp.event.v1", sourceEventId: `${instance}:${sourceSequenceOffset + index + 1}`, sourceSeq: sourceSequenceOffset + index + 1,
sourceInstanceId: instance, sourceKind: "runner", runId: f.runId, normalizedSessionId: f.runId, turnId: "turn", schemaVersion: 1, priority: 0,
emittedAt: new Date().toISOString(), ...event } as paperclipRunner.PrpEvent);
const proof = await readNativeCursorPlanWait(db, f);
expect(proof).not.toBeNull();
const result = semanticFinish ? { ...proof!.result, reportedWorkDisposition: "done" as const, summary: "Explicit semantic finish wins", continuation: undefined,
completionClaim: { contractRevision: "1", objectiveSatisfied: true, criteria: [{ criterionId: "objective", status: "satisfied" as const, evidenceRefs: [] }], remainingWork: [] } } : proof!.result;
await port.completeRun({ result, turnId: "turn", terminal: { schema: "paperclip.prp.terminal.v1", runTerminalState: "succeeded", turnTerminalState: "completed", reportedWorkDisposition: result.reportedWorkDisposition } });
return { ...f, interactionId, deliveryId: delivery!.id, proof: proof! };
}
it("keeps an accepted Cursor plan passive across restart, future configuration changes, and paused recovery", async () => {
const f = await seedAcceptedCursorPlanWait();
await finalizeNativeRun({ db, runId: f.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
await finalizeNativeRun({ db, runId: f.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
expect(await hasCommittedNativeCursorPlanWait(db, f)).toBe(true);
expect(await db.select().from(statusDecisions).where(eq(statusDecisions.runId, f.runId))).toEqual([expect.objectContaining({ reasonCode: "native_plan_accepted_waiting_for_continuation", toStatus: "in_progress" })]);
expect(await db.select().from(statusDecisionEffects).where(eq(statusDecisionEffects.issueId, f.issueId))).toEqual([expect.objectContaining({ effectKind: "issue_status_projection", targetType: "issue", deliveryState: "delivered" })]);
expect(await db.select().from(issueComments).where(eq(issueComments.createdByRunId, f.runId))).toEqual([expect.objectContaining({ body: expect.stringContaining("next message") })]);
const current = paperclipRunner.resolveQualifiedAcpxProfile("cursor", "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]");
const resolver = vi.spyOn(paperclipRunner, "resolveQualifiedAcpxProfile").mockReturnValue({ ...current,
agentProfileVersion: current.agentProfileVersion + 1, commandDigest: `sha256:${"b".repeat(64)}` });
try {
expect(await readNativeCursorPlanWait(db, f)).toBeNull(); // Old profile cannot create a new wait.
for (const status of ["idle", "paused"] as const) {
await db.update(agents).set({ status, adapterConfig: { provider: "acpx", acpxAgent: "cursor",
model: "future-model", acpxSessionMode: "agent", acpxPermissionMode: "approve-reads" } }).where(eq(agents.id, f.agentId));
expect(await hasCommittedNativeCursorPlanWait(db, f)).toBe(true);
const recovered = await heartbeatService(db).reconcileStrandedAssignedIssues();
expect(recovered.continuationRequeued).toBe(0); expect(recovered.escalated).toBe(0);
}
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
const changed = structuredClone(run!.runnerProfileJson!);
(changed.nativeExecutionInput as { provider: { profile: { commandDigest: string } } }).provider.profile.commandDigest = "tampered-original-profile";
await db.update(heartbeatRuns).set({ runnerProfileJson: changed }).where(eq(heartbeatRuns.id, f.runId));
expect(await hasCommittedNativeCursorPlanWait(db, f)).toBe(false);
await db.update(heartbeatRuns).set({ runnerProfileJson: run!.runnerProfileJson }).where(eq(heartbeatRuns.id, f.runId));
expect(await hasCommittedNativeCursorPlanWait(db, f)).toBe(true);
const [tool] = await db.select().from(heartbeatRunEvents).where(and(eq(heartbeatRunEvents.runId, f.runId), eq(heartbeatRunEvents.eventType, "tool.execution.completed")));
const changedTool = structuredClone(tool!.payload!) as { prpEvent: { payload: { name: string } } };
changedTool.prpEvent.payload.name = "mutated committed tool evidence";
await db.update(heartbeatRunEvents).set({ payload: changedTool, sourcePayloadSha256: nativeSha256(changedTool.prpEvent) }).where(eq(heartbeatRunEvents.id, tool!.id));
expect(await hasCommittedNativeCursorPlanWait(db, f)).toBe(false);
await db.update(heartbeatRunEvents).set({ payload: tool!.payload, sourcePayloadSha256: tool!.sourcePayloadSha256 }).where(eq(heartbeatRunEvents.id, tool!.id));
expect(await hasCommittedNativeCursorPlanWait(db, f)).toBe(true);
} finally { resolver.mockRestore(); }
expect(await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.companyId, f.companyId))).toHaveLength(1);
expect(mockAdapterExecute).not.toHaveBeenCalled(); expect(mockExecutePaperclipNativeSession).not.toHaveBeenCalled();
});
it("retains an exact historical Cursor6 committed wait across more than 1000 ignored progress rows without allowing new unbound admission", async () => {
const progressCount = 1002;
const f = await seedAcceptedCursorPlanWait(false, progressCount);
await finalizeNativeRun({ db, runId: f.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
const profile = structuredClone(run!.runnerProfileJson!) as any;
Object.assign(profile.nativeExecutionInput.provider.profile, { agentProfileVersion: 6, commandDigest: "sha256:377dcea64a727ce799cc112458d4b40ba4bc6574cd6c6f7233b6efd5917a6c4b" });
await db.update(heartbeatRuns).set({ runnerProfileJson: profile }).where(eq(heartbeatRuns.id, f.runId));
await db.delete(heartbeatRunEvents).where(and(eq(heartbeatRunEvents.runId, f.runId), inArray(heartbeatRunEvents.eventType, ["tool.execution.started", "tool.execution.completed"])));
// Reconstruct the persisted Cursor6 receipt format using its unchanged
// source facts. This historical format had no tool lifecycle binding.
const rows = await db.select().from(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, f.runId));
const event = (kind: string) => (rows.find(r => r.eventType === kind)!.payload as any).prpEvent;
const [contract] = await db.select().from(completionContracts).where(eq(completionContracts.id, run!.completionContractId!));
const [interaction] = await db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, f.interactionId));
const [delivery] = await db.select().from(issueQuestionResponseDeliveries).where(eq(issueQuestionResponseDeliveries.id, f.deliveryId));
const legacy = { ...f.proof.source }; delete legacy.toolExecutionId; delete legacy.toolLifecycleSha256;
legacy.authoritySha256 = nativeSha256({ admission: profile.nativeExecutionInput, contract, created: event("runtime_request.created"), resolved: event("runtime_request.resolved"), terminal: event("turn.completed"), interaction, delivery, resolvedAt: interaction!.resolvedAt!.toISOString(), acknowledgedAt: delivery!.acknowledgedAt!.toISOString() });
const [decision] = await db.select().from(statusDecisions).where(eq(statusDecisions.runId, f.runId));
await db.update(statusDecisions).set({ decisionJson: { ...decision!.decisionJson, cursorPlanWait: legacy } }).where(eq(statusDecisions.id, decision!.id));
expect(await readNativeCursorPlanWait(db, f)).toBeNull();
expect(await hasCommittedNativeCursorPlanWait(db, f)).toBe(true);
// Fill the reserved source prefix with genuine durable progress rows. Shift
// only DB ordering keys; the original PRP facts and committed receipt stay
// byte-identical. Cursor6 never queried these rows, even above its budget.
await db.update(heartbeatRunEvents).set({ seq: sql`${heartbeatRunEvents.seq} + ${progressCount}` }).where(eq(heartbeatRunEvents.runId, f.runId));
const progress = Array.from({ length: progressCount }, (_, index) => {
const seq = index + 1;
const prpEvent = { ...event("runtime_request.created"), sourceEventId: `${run!.runnerInstanceId}:${seq}`, sourceSeq: seq, eventType: "tool.execution.progressed",
payload: { schema: "paperclip.tool.execution.v1", executionId: "earlier-tool", transport: "builtin", operation: "read", status: "running" } };
return { companyId: f.companyId, runId: f.runId, agentId: f.agentId, seq, eventType: prpEvent.eventType, payload: { prpEvent }, sourceInstanceId: run!.runnerInstanceId,
sourceEventId: prpEvent.sourceEventId, sourceSeq: seq, sourcePayloadSha256: nativeSha256(prpEvent), protocolSchemaVersion: 1 };
});
await db.insert(heartbeatRunEvents).values(progress);
expect(await hasCommittedNativeCursorPlanWait(db, f)).toBe(true);
const [unchangedDecision] = await db.select().from(statusDecisions).where(eq(statusDecisions.id, decision!.id));
expect(unchangedDecision!.decisionJson!.cursorPlanWait).toEqual(legacy);
// Completed rows belonged to the old query. A later completion must still
// invalidate the normal-terminal boundary rather than being filtered away.
const completion = { ...event("turn.completed"), sourceEventId: `${run!.runnerInstanceId}:99999`, sourceSeq: 99999, eventType: "tool.execution.completed", payload: { ...progress[0]!.payload.prpEvent.payload, status: "completed" } };
const [extra] = await db.insert(heartbeatRunEvents).values({ ...progress[0]!, seq: 99999, eventType: completion.eventType, payload: { prpEvent: completion }, sourceEventId: completion.sourceEventId, sourceSeq: 99999, sourcePayloadSha256: nativeSha256(completion) }).returning();
expect(await hasCommittedNativeCursorPlanWait(db, f)).toBe(false);
await db.delete(heartbeatRunEvents).where(eq(heartbeatRunEvents.id, extra!.id));
expect(await hasCommittedNativeCursorPlanWait(db, f)).toBe(true);
profile.nativeExecutionInput.provider.profile.commandDigest = "tampered-history";
await db.update(heartbeatRuns).set({ runnerProfileJson: profile }).where(eq(heartbeatRuns.id, f.runId));
expect(await hasCommittedNativeCursorPlanWait(db, f)).toBe(false);
});
it("admits a later ordinary user message after an accepted Cursor plan without changing Plan mode or replaying its run", async () => {
const f = await seedAcceptedCursorPlanWait();
await finalizeNativeRun({ db, runId: f.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
// Occupy the single dispatch slot: this checks actual user-wake admission
// without executing any provider or fabricating a second semantic result.
const occupied = randomUUID();
await db.update(agents).set({ runtimeConfig: { heartbeat: { wakeOnDemand: true, maxConcurrentRuns: 1 } } }).where(eq(agents.id, f.agentId));
await db.insert(heartbeatRuns).values({ id: occupied, companyId: f.companyId, agentId: f.agentId, status: "running", startedAt: new Date(), contextSnapshot: {} });
const [comment] = await db.insert(issueComments).values({ companyId: f.companyId, issueId: f.issueId, authorType: "user", authorUserId: "responsible-user", body: "Continue reviewing the accepted plan in Plan mode." }).returning();
try {
const heartbeat = heartbeatService(db);
const next = await heartbeat.wakeup(f.agentId, { source: "automation", triggerDetail: "system", reason: "issue_commented", requestedByActorType: "user", requestedByActorId: "responsible-user",
payload: { issueId: f.issueId, commentId: comment!.id }, contextSnapshot: { issueId: f.issueId, taskId: f.issueId, wakeCommentId: comment!.id, wakeCommentIds: [comment!.id] } });
expect(next).not.toBeNull(); expect(next!.id).not.toBe(f.runId);
const [admitted] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, next!.id));
expect(admitted).toMatchObject({ status: "queued", retryOfRunId: null });
expect(admitted!.contextSnapshot?.wakeCommentIds).toContain(comment!.id);
const [agent] = await db.select().from(agents).where(eq(agents.id, f.agentId));
expect(agent!.adapterConfig.acpxSessionMode).toBe("plan");
expect(await hasCommittedNativeCursorPlanWait(db, f)).toBe(false);
expect(mockExecutePaperclipNativeSession).not.toHaveBeenCalled();
} finally {
await db.update(heartbeatRuns).set({ status: "cancelled", finishedAt: new Date() }).where(and(eq(heartbeatRuns.companyId, f.companyId), inArray(heartbeatRuns.status, ["running", "queued"])));
await db.update(agentWakeupRequests).set({ status: "cancelled", finishedAt: new Date() }).where(and(eq(agentWakeupRequests.companyId, f.companyId), inArray(agentWakeupRequests.status, ["queued", "claimed"])));
}
});
it.each(["delivery", "assignment", "admission", "user_request", "result_or_decision"] as const)("revokes an accepted Cursor plan passive wait after %s changes", async change => {
const f = await seedAcceptedCursorPlanWait();
await finalizeNativeRun({ db, runId: f.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
if (change === "delivery") await db.update(issueQuestionResponseDeliveries).set({ acknowledgedAt: null }).where(eq(issueQuestionResponseDeliveries.id, f.deliveryId));
if (change === "assignment") await db.update(issues).set({ assigneeAgentId: null }).where(eq(issues.id, f.issueId));
if (change === "admission") {
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
const profile = structuredClone(run!.runnerProfileJson!);
(profile.nativeExecutionInput as { provider: { cursorMode: string } }).provider.cursorMode = "agent";
await db.update(heartbeatRuns).set({ runnerProfileJson: profile }).where(eq(heartbeatRuns.id, f.runId));
}
if (change === "user_request") await db.insert(issueComments).values({ companyId: f.companyId, issueId: f.issueId, authorType: "user", authorUserId: "responsible-user", body: "Continue reviewing this plan in Plan mode." });
if (change === "result_or_decision") {
const [accepted] = await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, f.runId));
await db.update(nativeRunResults).set({ canonicalSha256: "changed" }).where(eq(nativeRunResults.id, accepted!.id));
expect(await hasCommittedNativeCursorPlanWait(db, f)).toBe(false);
await db.update(nativeRunResults).set({ canonicalSha256: accepted!.canonicalSha256 }).where(eq(nativeRunResults.id, accepted!.id));
expect(await hasCommittedNativeCursorPlanWait(db, f)).toBe(true);
await db.update(issues).set({ lastStatusDecisionId: null }).where(eq(issues.id, f.issueId));
}
expect(await hasCommittedNativeCursorPlanWait(db, f)).toBe(false);
});
it("rechecks accepted Cursor plan delivery under the status transaction before presentation or effects", async () => {
const f = await seedAcceptedCursorPlanWait();
await finalizeNativeRun({ db, runId: f.runId, workspaceFinalizeStatus: "succeeded", failpoint: "status_projection" });
const [coordinator] = await db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, f.runId));
const [issue] = await db.select().from(issues).where(eq(issues.id, f.issueId));
await db.update(issueQuestionResponseDeliveries).set({ payloadSha256: "changed" }).where(eq(issueQuestionResponseDeliveries.id, f.deliveryId));
await expect(commitNativeStatusDecision({ db, companyId: f.companyId, issueId: f.issueId, runId: f.runId, assessmentId: coordinator!.assessmentId!,
priorStatus: issue!.status, priorStatusVersion: issue!.statusVersion, priorDecisionId: issue!.lastStatusDecisionId,
decision: { policyVersion: "phase6-v7", statusAction: "in_progress", toStatus: "in_progress", reasonCode: "native_plan_accepted_waiting_for_continuation", unblockDescriptor: null, effects: [] },
requireCursorPlanWaitSource: f.proof.source })).rejects.toBeInstanceOf(NativeStatusRaceError);
expect(await db.select().from(statusDecisions).where(eq(statusDecisions.runId, f.runId))).toHaveLength(0);
expect(await db.select().from(issueComments).where(eq(issueComments.createdByRunId, f.runId))).toHaveLength(0);
});
it("preserves explicit semantic finish priority over accepted Cursor plan evidence", async () => {
const f = await seedAcceptedCursorPlanWait(true);
await finalizeNativeRun({ db, runId: f.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
expect(await hasCommittedNativeCursorPlanWait(db, f)).toBe(false);
const decisions = await db.select().from(statusDecisions).where(eq(statusDecisions.runId, f.runId));
expect(decisions).toHaveLength(1); expect(decisions[0]!.reasonCode).not.toBe("native_plan_accepted_waiting_for_continuation");
});
});
async function seedNativePassiveBoardResponse(
continuationKind:
"response_wake" | "same_agent" | "retry" = "response_wake",
@@ -16627,4 +16406,237 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
.where(eq(heartbeatRuns.id, runId));
expect(runs).toHaveLength(1);
});
describe("accepted Cursor planning boundaries", () => {
const nativeImplementation = mockExecutePaperclipNativeSession.getMockImplementation();
beforeEach(() => {
mockExecutePaperclipNativeSession.mockImplementation(async () => { throw new Error("Accepted-plan tests must never execute a provider"); });
});
afterEach(() => { mockExecutePaperclipNativeSession.mockImplementation(nativeImplementation!); });
async function seedAcceptedCursorPlanWait(semanticFinish = false, sourceSequenceOffset = 0) {
const f = await seedStrandedIssueFixture({ status: "in_progress", runStatus: "succeeded", livenessState: "advanced" });
const instance = randomUUID(), interactionId = randomUUID();
const contractInput = { db, companyId: f.companyId,
issue: { id: f.issueId, title: "Review the plan before further work", description: "Explicit completion required" }, actorId: "test" };
const { row: persistedContract, contract } = await ensureNativeCompletionContract(contractInput);
const reused = await ensureNativeCompletionContract(contractInput);
expect(reused.row.id).toBe(persistedContract.id);
expect(reused.contract).toEqual(contract);
const contractId = persistedContract.id, contractSha = persistedContract.canonicalSha256;
// Production binds policy/schema as well as the body; a body-only hash is not a valid contract receipt.
expect(contractSha).not.toBe(nativeSha256(contract));
const model = "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]";
await db.update(agents).set({ adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "cursor", model, acpxSessionMode: "plan", acpxPermissionMode: "approve-all" } }).where(eq(agents.id, f.agentId));
await db.update(agentWakeupRequests).set({ status: "completed" }).where(eq(agentWakeupRequests.id, f.wakeupRequestId));
await db.update(heartbeatRuns).set({ runtimeMode: "native", nativeIssueId: f.issueId, nativeSessionId: f.runId,
completionContractId: contractId, completionContractSha256: contractSha, runnerInstanceId: instance,
runnerProfileJson: { nativeExecutionInput: { binding: { companyId: f.companyId, issueId: f.issueId, runId: f.runId, agentId: f.agentId }, provider: { kind: "acpx", agent: "cursor", model, mode: "plan", permissionMode: "approve-all",
profile: paperclipRunner.resolveQualifiedAcpxProfile("cursor", model) }, session: { normalizedSessionId: f.runId },
completionContract: { id: contractId, sha256: contractSha, contract } } } }).where(eq(heartbeatRuns.id, f.runId));
const planId = `plan-${"a".repeat(64)}`;
const questionSet = { schema: "paperclip.question_set.v1", title: "Native plan", description: "Exact accepted revision", questions: [{ id: planId, prompt: "Proceed?", required: true,
answerMode: "single_select", options: [{ id: "accept", label: "Accept" }, { id: "reject", label: "Reject" }, { id: "cancel", label: "Cancel" }] }] };
const [interaction] = await db.insert(issueThreadInteractions).values({ id: interactionId, companyId: f.companyId, issueId: f.issueId, sourceRunId: f.runId,
createdByAgentId: f.agentId, kind: "ask_user_questions", status: "answered", continuationPolicy: "none",
idempotencyKey: `paperclip-runner-question:${f.runId}:request`, resolvedByUserId: "responsible-user", resolvedAt: new Date(Date.now() - 1000),
payload: { version: 1, questions: [{ id: planId, prompt: "Proceed?", selectionMode: "single", required: true, allowOther: false, options: [{ id: "accept", label: "Accept" }, { id: "reject", label: "Reject" }, { id: "cancel", label: "Cancel" }] }], runtimeRequestId: "request", questionSet: questionSet as never }, result: { version: 1, answers: [{ questionId: planId, optionIds: ["accept"] }] } }).returning();
const answer = buildQuestionResponseDeliveryEnvelope(interaction as never);
const [delivery] = await db.insert(issueQuestionResponseDeliveries).values({ companyId: f.companyId, issueId: f.issueId, interactionId,
sourceRunId: f.runId, targetRunId: f.runId, correlationId: randomUUID(), status: "delivered", deliveryMode: "steered", acknowledgedAt: new Date(), payloadSha256: nativeSha256(answer) }).returning();
const port = new PaperclipControlPlanePort(db, { companyId: f.companyId, issueId: f.issueId, runId: f.runId, agentId: f.agentId,
sessionId: f.runId, completionContractId: contractId, completionContractSha256: contractSha, sourceInstanceId: instance, controlPlaneSourceInstanceId: `control-${f.runId}` });
const events = [
{ eventType: "runtime_request.created", payload: { request: { schema: "paperclip.runtime_request.v2", requestKind: "runtime", requestId: "request", type: "input", status: "pending", turnId: "turn", itemId: "native-plan-tool", prompt: "Review plan",
origin: { adapter: "acpx-runtime-sidecar", provider: "cursor", method: "cursor/create_plan" }, input: questionSet } } },
{ eventType: "tool.execution.started", payload: { schema: "paperclip.tool.execution.v1", executionId: "native-plan-tool", transport: "builtin", operation: "execute", status: "running", readOnly: false, namespace: null, name: "Create Plan", target: null, inputUpdated: true, output: null, outputBytes: 0, outputTruncated: false, outputDigest: null, progress: null, exitCode: null, durationMs: null } },
{ eventType: "runtime_request.resolved", payload: { requestId: "request", turnId: "turn", action: "submit", response: answer.response } },
{ eventType: "tool.execution.completed", payload: { schema: "paperclip.tool.execution.v1", executionId: "native-plan-tool", transport: "builtin", operation: "execute", status: "completed", readOnly: false, namespace: null, name: "Create Plan", target: null, inputUpdated: false, output: null, outputBytes: 0, outputTruncated: false, outputDigest: null, progress: null, exitCode: null, durationMs: null } },
{ eventType: "turn.completed", payload: { status: "completed", error: null } },
];
for (const [index, event] of events.entries()) await port.appendEvent({ schema: "paperclip.prp.event.v1", sourceEventId: `${instance}:${sourceSequenceOffset + index + 1}`, sourceSeq: sourceSequenceOffset + index + 1,
sourceInstanceId: instance, sourceKind: "runner", runId: f.runId, normalizedSessionId: f.runId, turnId: "turn", schemaVersion: 1, priority: 0,
emittedAt: new Date().toISOString(), ...event } as paperclipRunner.PrpEvent);
const proof = await readNativePlanWait(db, f);
expect(proof).not.toBeNull();
const result = semanticFinish ? { ...proof!.result, reportedWorkDisposition: "done" as const, summary: "Explicit semantic finish wins", continuation: undefined,
completionClaim: { contractRevision: "1", objectiveSatisfied: true, criteria: [{ criterionId: "objective", status: "satisfied" as const, evidenceRefs: [] }], remainingWork: [] } } : proof!.result;
await port.completeRun({ result, turnId: "turn", terminal: { schema: "paperclip.prp.terminal.v1", runTerminalState: "succeeded", turnTerminalState: "completed", reportedWorkDisposition: result.reportedWorkDisposition } });
return { ...f, interactionId, deliveryId: delivery!.id, proof: proof! };
}
it("keeps an accepted Cursor plan passive across restart, future configuration changes, and paused recovery", async () => {
const f = await seedAcceptedCursorPlanWait();
await finalizeNativeRun({ db, runId: f.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
await finalizeNativeRun({ db, runId: f.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
expect(await hasCommittedNativePlanWait(db, f)).toBe(true);
expect(await db.select().from(statusDecisions).where(eq(statusDecisions.runId, f.runId))).toEqual([expect.objectContaining({ reasonCode: "native_plan_accepted_waiting_for_continuation", toStatus: "in_progress" })]);
expect(await db.select().from(statusDecisionEffects).where(eq(statusDecisionEffects.issueId, f.issueId))).toEqual([expect.objectContaining({ effectKind: "issue_status_projection", targetType: "issue", deliveryState: "delivered" })]);
expect(await db.select().from(issueComments).where(eq(issueComments.createdByRunId, f.runId))).toEqual([expect.objectContaining({ body: expect.stringContaining("next message") })]);
const current = paperclipRunner.resolveQualifiedAcpxProfile("cursor", "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]");
const resolver = vi.spyOn(paperclipRunner, "resolveQualifiedAcpxProfile").mockReturnValue({ ...current,
agentProfileVersion: current.agentProfileVersion + 1, commandDigest: `sha256:${"b".repeat(64)}` });
try {
expect(await readNativePlanWait(db, f)).toBeNull(); // Old profile cannot create a new wait.
for (const status of ["idle", "paused"] as const) {
await db.update(agents).set({ status, adapterConfig: { provider: "acpx", acpxAgent: "cursor",
model: "future-model", acpxSessionMode: "agent", acpxPermissionMode: "approve-reads" } }).where(eq(agents.id, f.agentId));
expect(await hasCommittedNativePlanWait(db, f)).toBe(true);
const recovered = await heartbeatService(db).reconcileStrandedAssignedIssues();
expect(recovered.continuationRequeued).toBe(0); expect(recovered.escalated).toBe(0);
}
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
const changed = structuredClone(run!.runnerProfileJson!);
(changed.nativeExecutionInput as { provider: { profile: { commandDigest: string } } }).provider.profile.commandDigest = "tampered-original-profile";
await db.update(heartbeatRuns).set({ runnerProfileJson: changed }).where(eq(heartbeatRuns.id, f.runId));
expect(await hasCommittedNativePlanWait(db, f)).toBe(false);
await db.update(heartbeatRuns).set({ runnerProfileJson: run!.runnerProfileJson }).where(eq(heartbeatRuns.id, f.runId));
expect(await hasCommittedNativePlanWait(db, f)).toBe(true);
const [tool] = await db.select().from(heartbeatRunEvents).where(and(eq(heartbeatRunEvents.runId, f.runId), eq(heartbeatRunEvents.eventType, "tool.execution.completed")));
const changedTool = structuredClone(tool!.payload!) as { prpEvent: { payload: { name: string } } };
changedTool.prpEvent.payload.name = "mutated committed tool evidence";
await db.update(heartbeatRunEvents).set({ payload: changedTool, sourcePayloadSha256: nativeSha256(changedTool.prpEvent) }).where(eq(heartbeatRunEvents.id, tool!.id));
expect(await hasCommittedNativePlanWait(db, f)).toBe(false);
await db.update(heartbeatRunEvents).set({ payload: tool!.payload, sourcePayloadSha256: tool!.sourcePayloadSha256 }).where(eq(heartbeatRunEvents.id, tool!.id));
expect(await hasCommittedNativePlanWait(db, f)).toBe(true);
} finally { resolver.mockRestore(); }
expect(await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.companyId, f.companyId))).toHaveLength(1);
expect(mockAdapterExecute).not.toHaveBeenCalled(); expect(mockExecutePaperclipNativeSession).not.toHaveBeenCalled();
});
it("retains an exact historical Cursor6 committed wait across more than 1000 ignored progress rows without allowing new unbound admission", async () => {
const progressCount = 1002;
const f = await seedAcceptedCursorPlanWait(false, progressCount);
await finalizeNativeRun({ db, runId: f.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
const profile = structuredClone(run!.runnerProfileJson!) as any;
Object.assign(profile.nativeExecutionInput.provider.profile, { agentProfileVersion: 6, commandDigest: "sha256:377dcea64a727ce799cc112458d4b40ba4bc6574cd6c6f7233b6efd5917a6c4b" });
await db.update(heartbeatRuns).set({ runnerProfileJson: profile }).where(eq(heartbeatRuns.id, f.runId));
await db.delete(heartbeatRunEvents).where(and(eq(heartbeatRunEvents.runId, f.runId), inArray(heartbeatRunEvents.eventType, ["tool.execution.started", "tool.execution.completed"])));
// Reconstruct the persisted Cursor6 receipt format using its unchanged
// source facts. This historical format had no tool lifecycle binding.
const rows = await db.select().from(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, f.runId));
const event = (kind: string) => (rows.find(r => r.eventType === kind)!.payload as any).prpEvent;
const [contract] = await db.select().from(completionContracts).where(eq(completionContracts.id, run!.completionContractId!));
const [interaction] = await db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, f.interactionId));
const [delivery] = await db.select().from(issueQuestionResponseDeliveries).where(eq(issueQuestionResponseDeliveries.id, f.deliveryId));
profile.nativeExecutionInput.provider.cursorMode = profile.nativeExecutionInput.provider.mode;
delete profile.nativeExecutionInput.provider.mode;
await db.update(heartbeatRuns).set({ runnerProfileJson: profile }).where(eq(heartbeatRuns.id, f.runId));
const legacy = { ...f.proof.source, schema: "paperclip.native_cursor_plan_wait.v1" }; delete legacy.toolExecutionId; delete legacy.toolLifecycleSha256;
legacy.authoritySha256 = nativeSha256({ admission: profile.nativeExecutionInput, contract, created: event("runtime_request.created"), resolved: event("runtime_request.resolved"), terminal: event("turn.completed"), interaction, delivery, resolvedAt: interaction!.resolvedAt!.toISOString(), acknowledgedAt: delivery!.acknowledgedAt!.toISOString() });
const [decision] = await db.select().from(statusDecisions).where(eq(statusDecisions.runId, f.runId));
const [savedResult] = await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, f.runId));
const resultJson = structuredClone(savedResult!.resultJson!) as any;
resultJson.result.continuation.idempotencyKey = resultJson.result.continuation.idempotencyKey.replace("native-plan-wait:", "cursor-plan-wait:");
const canonicalSha256 = nativeSha256(resultJson);
await db.update(nativeRunResults).set({ resultJson, canonicalSha256 }).where(eq(nativeRunResults.id, savedResult!.id));
const decisionJson = { ...decision!.decisionJson } as any;
delete decisionJson.planWait; delete decisionJson.planWaitResult;
decisionJson.cursorPlanWait = legacy;
decisionJson.cursorPlanWaitResult = { resultId: savedResult!.id, resultSha256: canonicalSha256 };
await db.update(statusDecisions).set({ decisionJson }).where(eq(statusDecisions.id, decision!.id));
expect(await readNativePlanWait(db, f)).toBeNull();
expect(await hasCommittedNativePlanWait(db, f)).toBe(true);
// Fill the reserved source prefix with genuine durable progress rows. Shift
// only DB ordering keys; the original PRP facts and committed receipt stay
// byte-identical. Cursor6 never queried these rows, even above its budget.
await db.update(heartbeatRunEvents).set({ seq: sql`${heartbeatRunEvents.seq} + ${progressCount}` }).where(eq(heartbeatRunEvents.runId, f.runId));
const progress = Array.from({ length: progressCount }, (_, index) => {
const seq = index + 1;
const prpEvent = { ...event("runtime_request.created"), sourceEventId: `${run!.runnerInstanceId}:${seq}`, sourceSeq: seq, eventType: "tool.execution.progressed",
payload: { schema: "paperclip.tool.execution.v1", executionId: "earlier-tool", transport: "builtin", operation: "read", status: "running" } };
return { companyId: f.companyId, runId: f.runId, agentId: f.agentId, seq, eventType: prpEvent.eventType, payload: { prpEvent }, sourceInstanceId: run!.runnerInstanceId,
sourceEventId: prpEvent.sourceEventId, sourceSeq: seq, sourcePayloadSha256: nativeSha256(prpEvent), protocolSchemaVersion: 1 };
});
await db.insert(heartbeatRunEvents).values(progress);
expect(await hasCommittedNativePlanWait(db, f)).toBe(true);
const [unchangedDecision] = await db.select().from(statusDecisions).where(eq(statusDecisions.id, decision!.id));
expect(unchangedDecision!.decisionJson!.cursorPlanWait).toEqual(legacy);
// Completed rows belonged to the old query. A later completion must still
// invalidate the normal-terminal boundary rather than being filtered away.
const completion = { ...event("turn.completed"), sourceEventId: `${run!.runnerInstanceId}:99999`, sourceSeq: 99999, eventType: "tool.execution.completed", payload: { ...progress[0]!.payload.prpEvent.payload, status: "completed" } };
const [extra] = await db.insert(heartbeatRunEvents).values({ ...progress[0]!, seq: 99999, eventType: completion.eventType, payload: { prpEvent: completion }, sourceEventId: completion.sourceEventId, sourceSeq: 99999, sourcePayloadSha256: nativeSha256(completion) }).returning();
expect(await hasCommittedNativePlanWait(db, f)).toBe(false);
await db.delete(heartbeatRunEvents).where(eq(heartbeatRunEvents.id, extra!.id));
expect(await hasCommittedNativePlanWait(db, f)).toBe(true);
profile.nativeExecutionInput.provider.profile.commandDigest = "tampered-history";
await db.update(heartbeatRuns).set({ runnerProfileJson: profile }).where(eq(heartbeatRuns.id, f.runId));
expect(await hasCommittedNativePlanWait(db, f)).toBe(false);
});
it("admits a later ordinary user message after an accepted Cursor plan without changing Plan mode or replaying its run", async () => {
const f = await seedAcceptedCursorPlanWait();
await finalizeNativeRun({ db, runId: f.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
// Occupy the single dispatch slot: this checks actual user-wake admission
// without executing any provider or fabricating a second semantic result.
const occupied = randomUUID();
await db.update(agents).set({ runtimeConfig: { heartbeat: { wakeOnDemand: true, maxConcurrentRuns: 1 } } }).where(eq(agents.id, f.agentId));
await db.insert(heartbeatRuns).values({ id: occupied, companyId: f.companyId, agentId: f.agentId, status: "running", startedAt: new Date(), contextSnapshot: {} });
const [comment] = await db.insert(issueComments).values({ companyId: f.companyId, issueId: f.issueId, authorType: "user", authorUserId: "responsible-user", body: "Continue reviewing the accepted plan in Plan mode." }).returning();
try {
const heartbeat = heartbeatService(db);
const next = await heartbeat.wakeup(f.agentId, { source: "automation", triggerDetail: "system", reason: "issue_commented", requestedByActorType: "user", requestedByActorId: "responsible-user",
payload: { issueId: f.issueId, commentId: comment!.id }, contextSnapshot: { issueId: f.issueId, taskId: f.issueId, wakeCommentId: comment!.id, wakeCommentIds: [comment!.id] } });
expect(next).not.toBeNull(); expect(next!.id).not.toBe(f.runId);
const [admitted] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, next!.id));
expect(admitted).toMatchObject({ status: "queued", retryOfRunId: null });
expect(admitted!.contextSnapshot?.wakeCommentIds).toContain(comment!.id);
const [agent] = await db.select().from(agents).where(eq(agents.id, f.agentId));
expect(agent!.adapterConfig.acpxSessionMode).toBe("plan");
expect(await hasCommittedNativePlanWait(db, f)).toBe(false);
expect(mockExecutePaperclipNativeSession).not.toHaveBeenCalled();
} finally {
await db.update(heartbeatRuns).set({ status: "cancelled", finishedAt: new Date() }).where(and(eq(heartbeatRuns.companyId, f.companyId), inArray(heartbeatRuns.status, ["running", "queued"])));
await db.update(agentWakeupRequests).set({ status: "cancelled", finishedAt: new Date() }).where(and(eq(agentWakeupRequests.companyId, f.companyId), inArray(agentWakeupRequests.status, ["queued", "claimed"])));
}
});
it.each(["delivery", "assignment", "admission", "user_request", "result_or_decision"] as const)("revokes an accepted Cursor plan passive wait after %s changes", async change => {
const f = await seedAcceptedCursorPlanWait();
await finalizeNativeRun({ db, runId: f.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
if (change === "delivery") await db.update(issueQuestionResponseDeliveries).set({ acknowledgedAt: null }).where(eq(issueQuestionResponseDeliveries.id, f.deliveryId));
if (change === "assignment") await db.update(issues).set({ assigneeAgentId: null }).where(eq(issues.id, f.issueId));
if (change === "admission") {
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
const profile = structuredClone(run!.runnerProfileJson!);
(profile.nativeExecutionInput as { provider: { cursorMode: string } }).provider.cursorMode = "agent";
await db.update(heartbeatRuns).set({ runnerProfileJson: profile }).where(eq(heartbeatRuns.id, f.runId));
}
if (change === "user_request") await db.insert(issueComments).values({ companyId: f.companyId, issueId: f.issueId, authorType: "user", authorUserId: "responsible-user", body: "Continue reviewing this plan in Plan mode." });
if (change === "result_or_decision") {
const [accepted] = await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, f.runId));
await db.update(nativeRunResults).set({ canonicalSha256: "changed" }).where(eq(nativeRunResults.id, accepted!.id));
expect(await hasCommittedNativePlanWait(db, f)).toBe(false);
await db.update(nativeRunResults).set({ canonicalSha256: accepted!.canonicalSha256 }).where(eq(nativeRunResults.id, accepted!.id));
expect(await hasCommittedNativePlanWait(db, f)).toBe(true);
await db.update(issues).set({ lastStatusDecisionId: null }).where(eq(issues.id, f.issueId));
}
expect(await hasCommittedNativePlanWait(db, f)).toBe(false);
});
it("rechecks accepted Cursor plan delivery under the status transaction before presentation or effects", async () => {
const f = await seedAcceptedCursorPlanWait();
await finalizeNativeRun({ db, runId: f.runId, workspaceFinalizeStatus: "succeeded", failpoint: "status_projection" });
const [coordinator] = await db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, f.runId));
const [issue] = await db.select().from(issues).where(eq(issues.id, f.issueId));
await db.update(issueQuestionResponseDeliveries).set({ payloadSha256: "changed" }).where(eq(issueQuestionResponseDeliveries.id, f.deliveryId));
await expect(commitNativeStatusDecision({ db, companyId: f.companyId, issueId: f.issueId, runId: f.runId, assessmentId: coordinator!.assessmentId!,
priorStatus: issue!.status, priorStatusVersion: issue!.statusVersion, priorDecisionId: issue!.lastStatusDecisionId,
decision: { policyVersion: "phase6-v7", statusAction: "in_progress", toStatus: "in_progress", reasonCode: "native_plan_accepted_waiting_for_continuation", unblockDescriptor: null, effects: [] },
requirePlanWaitSource: f.proof.source })).rejects.toBeInstanceOf(NativeStatusRaceError);
expect(await db.select().from(statusDecisions).where(eq(statusDecisions.runId, f.runId))).toHaveLength(0);
expect(await db.select().from(issueComments).where(eq(issueComments.createdByRunId, f.runId))).toHaveLength(0);
});
it("preserves explicit semantic finish priority over accepted Cursor plan evidence", async () => {
const f = await seedAcceptedCursorPlanWait(true);
await finalizeNativeRun({ db, runId: f.runId, workspaceFinalizeStatus: "succeeded", projectRunStatus: true });
expect(await hasCommittedNativePlanWait(db, f)).toBe(false);
const decisions = await db.select().from(statusDecisions).where(eq(statusDecisions.runId, f.runId));
expect(decisions).toHaveLength(1); expect(decisions[0]!.reasonCode).not.toBe("native_plan_accepted_waiting_for_continuation");
});
});
});
@@ -78,7 +78,12 @@ async function createApp(
return app;
}
describe("GET /invites/:token/test-resolution", () => {
describe("GET /invites/:token/test-resolution", { sequential: true }, () => {
beforeAll(async () => {
// Route transformation is fixture setup, not part of the network assertions.
await loadAppModules();
});
beforeEach(() => {
vi.clearAllMocks();
});
+42 -14
View File
@@ -41,20 +41,48 @@ function receiptNotice(version: 1 | 2 = 1): Record<string, any> {
const receiptSchemaValue = (notice: Record<string, any>) => notice.details.find((detail: any) => detail.name === "schema").value;
describe("redaction", () => {
it("preserves credential-related prose, metadata, and dotted filenames", () => {
const input = {
body: "Keep the private key in a secret manager. Document credential handling and token permissions. Use bearer tokens for authentication. Prefer bearer authentication.",
tokenPolicy: "least privilege",
secretStorage: "vault",
credentialHandling: "harness",
authorizationRequired: true,
path: "deployment.credentials.example.md",
};
expect(sanitizeRecord(input)).toEqual(input);
expect(redactSensitiveText(input.body)).toBe(input.body);
expect(sanitizeRecord({ credentials: { provider: "opaque-value" }, passwordValue: "opaque-value", authorization_code: "opaque-value" }))
.toEqual({ credentials: REDACTED_EVENT_VALUE, passwordValue: REDACTED_EVENT_VALUE, authorization_code: REDACTED_EVENT_VALUE });
expect(redactSensitiveText("--token-budget 4000 SECRET_STORAGE=vault")).toBe("--token-budget 4000 SECRET_STORAGE=vault");
it("preserves the actual Copilot receipt producer discriminator through nested durable redaction", () => {
const events: Array<Record<string, any>> = [];
const projector = createCopilotToolEvidence({ sessionId: "session", turnId: "turn", workingDirectory: "/workspace",
active: () => true, emit: event => events.push(event) });
const receipt = appendSemanticToolReceipt({ tool: "finish_task", callId: "call", arguments: {} },
{ content: [{ type: "text", text: "accepted" }] }).receipt;
expect(receipt.schema).toBe("paperclip.semantic_tool_receipt.v2");
projector.captureSemanticReceipt()!(receipt);
expect(events).toHaveLength(1);
expect(events[0]!.payload.category).toBe("paperclip_semantic_tool_receipt_v2");
expect(events[0]!.payload.details).toHaveLength(8);
const input = { prpEvent: { schema: "paperclip.prp.event.v1", schemaVersion: 1,
eventType: "provider.notice.recorded", payload: events[0]!.payload } };
expect(redactEventPayload(input)).toEqual(input);
expect(redactEventPayload(redactEventPayload(input))).toEqual(input);
});
it.each([1, 2] as const)("preserves public v%s receipt identifiers as text", version => {
const notice = receiptNotice(version);
expect(redactEventPayload(notice)).toEqual(notice);
// Master's JWT detector recognizes encoded headers, so a dotted public
// identifier needs no receipt-shaped exemption to survive redaction.
expect(redactEventPayload({ value: `paperclip.semantic_tool_receipt.v${version}` }))
.toEqual({ value: `paperclip.semantic_tool_receipt.v${version}` });
});
it("redacts credentials in receipt-shaped data and every adjacent field", () => {
const jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature12345678";
const notice = receiptNotice();
notice.details.find((d: any) => d.name === "operationId").value = jwt;
notice.provenance.sessionId = jwt;
const redacted = redactEventPayload(notice)! as Record<string, any>;
expect(receiptSchemaValue(redacted)).toBe("paperclip.semantic_tool_receipt.v1");
expect(redacted.details.find((d: any) => d.name === "operationId").value).toBe(REDACTED_EVENT_VALUE);
expect(redacted.provenance.sessionId).toBe(REDACTED_EVENT_VALUE);
expect(redactEventPayload({ notice, password: "canary", arbitrary: jwt }))
.toMatchObject({ password: REDACTED_EVENT_VALUE, arbitrary: REDACTED_EVENT_VALUE });
notice.details.find((d: any) => d.name === "schema").value = jwt;
notice.summary = "Authorization: Bearer canary-token";
const hostile = redactEventPayload(notice)!;
expect(receiptSchemaValue(hostile)).toBe(REDACTED_EVENT_VALUE);
expect(JSON.stringify(hostile)).not.toContain("canary-token");
});
it("keeps the discriminator allowlist in exact PRP v1 schema parity", () => {
@@ -0,0 +1,163 @@
import { createHash } from "node:crypto";
import { chmodSync, linkSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, expect, it, vi } from "vitest";
const hooks = vi.hoisted(() => ({ mkdir: undefined as undefined | ((path: string) => void), rename: undefined as undefined | ((source: string, destination: string) => void), open: undefined as undefined | ((path: string) => void), read: undefined as undefined | ((path: string, bytes: number) => void) }));
vi.mock("node:fs/promises", async (original) => {
const fs = await original<typeof import("node:fs/promises")>();
return { ...fs,
mkdir: async (...args: Parameters<typeof fs.mkdir>) => { hooks.mkdir?.(String(args[0])); return fs.mkdir(...args); },
rename: async (...args: Parameters<typeof fs.rename>) => { hooks.rename?.(String(args[0]), String(args[1])); return fs.rename(...args); },
open: async (...args: Parameters<typeof fs.open>) => { hooks.open?.(String(args[0])); const file = await fs.open(...args); const read = file.read.bind(file); file.read = (async (...readArgs: any[]) => { const result = await (read as any)(...readArgs); hooks.read?.(String(args[0]), result.bytesRead); return result; }) as typeof file.read; return file; },
};
});
vi.mock("../vendor/paperclip-runner/index.js", async () => await import("../../../packages/paperclip-runner/src/drivers/acpx/qualified-profiles.js"));
import { QUALIFIED_ACPX_PROFILES } from "../../../packages/paperclip-runner/src/drivers/acpx/qualified-profiles.js";
import { createRemotePiCompanionManifest, importRemotePiCompanion, inventoryRemoteCompanion, resolveRemotePiCompanion, selectRemotePiCompanion } from "../services/native-runtime/remote-pi-companion.js";
const roots: string[] = [];
afterEach(() => { hooks.mkdir = undefined; hooks.rename = undefined; hooks.open = undefined; hooks.read = undefined; for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); vi.restoreAllMocks(); });
const hash = (bytes: string | Buffer) => createHash("sha256").update(bytes).digest("hex");
const canonical = (v: any): string => Array.isArray(v) ? `[${v.map(canonical).join(",")}]` : v && typeof v === "object" ? `{${Object.keys(v).sort().map(k => `${JSON.stringify(k)}:${canonical(v[k])}`).join(",")}}` : JSON.stringify(v);
async function fixture() {
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperclip-companion-"))); roots.push(root);
const source = "a".repeat(40); const serverRoot = join(root, "server"); const directory = join(root, "release");
mkdirSync(join(serverRoot, "dist"), { recursive: true }); writeFileSync(join(serverRoot, "package.json"), '{"name":"@paperclipai/server"}'); writeFileSync(join(serverRoot, "dist/build-info.json"), JSON.stringify({ commit: source }));
mkdirSync(join(directory, "bin"), { recursive: true }); mkdirSync(join(directory, "provider-pack"));
// Synthetic ELF header; these tests never launch it or claim native qualification.
const elf = Buffer.alloc(128); Buffer.from([127, 69, 76, 70, 2, 1]).copy(elf); elf.writeUInt16LE(62, 18); writeFileSync(join(directory, "bin/paperclip-runnerd"), elf, { mode: 0o755 });
const payload = { runnerSourceRevision: source, target: { platform: "linux", architecture: "x64" }, candidateProviders: { pi: { qualification: "qualified", profileDigest: QUALIFIED_ACPX_PROFILES.pi.commandDigest, path: "provider-assets/pi/linux-x64" } } };
writeFileSync(join(directory, "provider-pack/provider-pack.json"), JSON.stringify({ schema: "paperclip-runner/remote-provider-pack/v1", digest: `sha256:${hash(canonical(payload))}`, payload }));
const manifest = await createRemotePiCompanionManifest(directory, source); const bytes = JSON.stringify(manifest); writeFileSync(join(directory, "companion.json"), bytes); return { root, directory, serverRoot, sha256: hash(bytes), manifest, source };
}
it("imports the release-generated closure and resolves target bytes without environment overrides", async () => {
const f = await fixture(); const result = await importRemotePiCompanion(f); expect(result.status).toBe("imported_verified");
expect(result.runnerBinary).toBe(join(f.serverRoot, "remote-companions/linux-x64/bin/paperclip-runnerd"));
expect(readFileSync(result.runnerBinary!)).toEqual(readFileSync(join(f.directory, "bin/paperclip-runnerd")));
expect((await importRemotePiCompanion(f)).status).toBe("verified_existing"); expect((await resolveRemotePiCompanion({ serverRoot: f.serverRoot }))?.manifestSha256).toBe(f.sha256);
expect(readdirSync(join(f.serverRoot, "remote-companions"))).toEqual(["linux-x64"]);
});
it.each(["sha", "source", "profile", "target", "daemon", "extra", "mode", "outside-link", "outside-hardlink", "pack"])("rejects %s before publishing", async kind => {
const f = await fixture();
if (kind === "sha") f.sha256 = "b".repeat(64);
if (kind === "source") writeFileSync(join(f.serverRoot, "dist/build-info.json"), JSON.stringify({ commit: "b".repeat(40) }));
if (kind === "profile" || kind === "target") { Object.assign(f.manifest, kind === "profile" ? { profileDigest: "sha256:" + "b".repeat(64) } : { target: "darwin-arm64" }); const bytes = JSON.stringify(f.manifest); writeFileSync(join(f.directory, "companion.json"), bytes); f.sha256 = hash(bytes); }
if (kind === "daemon") writeFileSync(join(f.directory, "bin/paperclip-runnerd"), "changed");
if (kind === "extra") writeFileSync(join(f.directory, "extra"), "unlisted");
if (kind === "mode") chmodSync(join(f.directory, "bin/paperclip-runnerd"), 0o777);
if (kind === "outside-link") symlinkSync("../../server/package.json", join(f.directory, "provider-pack/escape"));
if (kind === "outside-hardlink") linkSync(join(f.directory, "bin/paperclip-runnerd"), join(f.root, "alias"));
if (kind === "pack") writeFileSync(join(f.directory, "provider-pack/provider-pack.json"), "{}");
await expect(importRemotePiCompanion(f)).rejects.toThrow(); expect(() => readdirSync(join(f.serverRoot, "remote-companions/linux-x64"))).toThrow();
});
it("copies contained symlinks and breaks only fully owned internal hardlinks", async () => {
const f = await fixture(); linkSync(join(f.directory, "bin/paperclip-runnerd"), join(f.directory, "bin/alias")); symlinkSync("paperclip-runnerd", join(f.directory, "bin/link"));
f.manifest = await createRemotePiCompanionManifest(f.directory, f.source); const bytes = JSON.stringify(f.manifest); writeFileSync(join(f.directory, "companion.json"), bytes); f.sha256 = hash(bytes);
expect((await importRemotePiCompanion(f)).status).toBe("imported_verified");
});
it("fails closed on cache corruption, foreign authority and workspace-contained cache", async () => {
const f = await fixture(); const result = await importRemotePiCompanion(f);
await expect(resolveRemotePiCompanion({ serverRoot: f.serverRoot, workspaceRoot: f.root })).rejects.toThrow("workspace");
await expect(resolveRemotePiCompanion({ serverRoot: f.serverRoot, workspaceRoot: join(result.root!, "provider-pack") })).rejects.toThrow("workspace");
writeFileSync(result.runnerBinary!, "corrupted"); await expect(resolveRemotePiCompanion({ serverRoot: f.serverRoot })).rejects.toThrow("inventory");
await expect(importRemotePiCompanion(f)).rejects.toThrow();
});
it("preserves an existing empty destination and releases only its import lock", async () => {
const f = await fixture(); mkdirSync(join(f.serverRoot, "remote-companions/linux-x64"), { recursive: true, mode: 0o700 }); chmodSync(join(f.serverRoot, "remote-companions"), 0o700);
await expect(importRemotePiCompanion(f)).rejects.toThrow(); expect(readdirSync(join(f.serverRoot, "remote-companions/linux-x64"))).toEqual([]); expect(readdirSync(join(f.serverRoot, "remote-companions"))).toEqual(["linux-x64"]);
});
it("rejects an unsafe cache parent and leaves it intact", async () => {
const f = await fixture(); const outside = join(f.root, "outside"); mkdirSync(outside); symlinkSync(outside, join(f.serverRoot, "remote-companions"));
await expect(importRemotePiCompanion(f)).rejects.toThrow(); expect(readdirSync(outside)).toEqual([]);
});
it("reports missing installation without manufacturing a companion", async () => { const f = await fixture(); expect(await resolveRemotePiCompanion({ serverRoot: f.serverRoot })).toBeNull(); });
it("selects only normal remote Pi, preserving explicit overrides and C/C admission", async () => {
const f = await fixture(); await importRemotePiCompanion(f); const workspaceRoot = join(f.root, "workspace"); mkdirSync(workspaceRoot);
const input = { serverRoot: f.serverRoot, workspaceRoot, remote: true, provider: { kind: "acpx", agent: "pi" } };
expect((await selectRemotePiCompanion(input))?.target).toBe("linux-x64");
for (const patch of [{ remote: false }, { provider: { kind: "acpx", agent: "cursor" } }, { provider: { kind: "acpx", agent: "copilot" } }, { provider: { kind: "codex" } }, { binaryOverride: "/operator/runnerd" }, { packOverride: "/operator/pack" }]) expect(await selectRemotePiCompanion({ ...input, ...patch })).toBeNull();
});
it("rejects an appeared empty destination without replacing it", async () => {
const f = await fixture(); const output = join(f.serverRoot, "remote-companions/linux-x64");
hooks.mkdir = path => { if (path !== output) return; hooks.mkdir = undefined; mkdirSync(output, { mode: 0o700 }); };
await expect(importRemotePiCompanion(f)).rejects.toThrow(); expect(readdirSync(output)).toEqual([]);
expect(readdirSync(join(f.serverRoot, "remote-companions"))).toEqual(["linux-x64"]);
});
it("drains owned output, staging and lock cleanup after publication failure", async () => {
const f = await fixture(); hooks.rename = () => { hooks.rename = undefined; throw new Error("injected publication failure"); };
await expect(importRemotePiCompanion(f)).rejects.toThrow("injected publication failure");
expect(readdirSync(join(f.serverRoot, "remote-companions"))).toEqual([]);
});
it("preserves a replaced staging root while draining the other owned cleanup", async () => {
const f = await fixture(); let replaced = "";
hooks.rename = source => {
hooks.rename = undefined; replaced = source.slice(0, source.lastIndexOf("/"));
renameSync(replaced, replaced + "-original"); mkdirSync(replaced, { mode: 0o700 }); writeFileSync(join(replaced, "foreign"), "preserve");
throw new Error("replaced staging");
};
await expect(importRemotePiCompanion(f)).rejects.toThrow("cleanup incomplete"); expect(readFileSync(join(replaced, "foreign"), "utf8")).toBe("preserve");
expect(readdirSync(join(f.serverRoot, "remote-companions"))).not.toContain(".import-linux-x64.lock");
expect(readdirSync(join(f.serverRoot, "remote-companions"))).not.toContain("linux-x64");
});
it.each(["new", "existing"])("defers actual SIGTERM on the %s import path and drains owned cleanup", async (mode) => {
const f = await fixture(); if (mode === "existing") await importRemotePiCompanion(f);
const { build } = await import("esbuild");
const { execFile } = await import("node:child_process");
const { promisify } = await import("node:util");
const bundle = join(f.root, "companion.mjs");
await build({ entryPoints: [new URL("../services/native-runtime/remote-pi-companion.ts", import.meta.url).pathname], outfile: bundle,
platform: "node", format: "esm", target: "node24", bundle: true, logLevel: "silent",
plugins: [{ name: "source-owned-profile-only", setup(builder) {
builder.onResolve({ filter: /vendor\/paperclip-runner\/index\.js$/ }, () => ({ path: new URL("../../../packages/paperclip-runner/src/drivers/acpx/qualified-profiles.ts", import.meta.url).pathname }));
} }],
});
const script = join(f.root, "cancel.mjs");
writeFileSync(script, `import {importRemotePiCompanion} from './companion.mjs';
import {readdirSync,existsSync} from 'node:fs';
let cancelled=false, checkpoints=0,sent=false; const cancel=()=>{cancelled=true}; process.on('SIGTERM',cancel);
try { await importRemotePiCompanion({...JSON.parse(process.argv[2]),checkCancelled(){ checkpoints++; if(!sent&&existsSync(process.argv[3]+'/.import-linux-x64.lock')){sent=true;process.kill(process.pid,'SIGTERM');} if(cancelled)throw Error('owned cancellation'); }}); throw Error('unexpected success'); }
catch(error){ if(error.message!=='owned cancellation')throw error; console.log(JSON.stringify({cancelled,checkpoints,remaining:readdirSync(process.argv[3])})); }
finally {process.off('SIGTERM',cancel);}
`);
const { stdout } = await promisify(execFile)(process.execPath, [script, JSON.stringify({ directory: f.directory, sha256: f.sha256, serverRoot: f.serverRoot }), join(f.serverRoot, "remote-companions")], { env: { PATH: "/usr/bin:/bin", LANG: "C.UTF-8" }, timeout: 10_000, maxBuffer: 65536 });
expect(JSON.parse(stdout)).toMatchObject({ cancelled: true, remaining: mode === "existing" ? ["linux-x64"] : [] });
});
it("honors deadline expiry after re-verifying an existing cache without deleting it", async () => {
const f = await fixture(); await importRemotePiCompanion(f); let expired = false;
hooks.open = path => { if(path === join(f.serverRoot, "remote-companions/linux-x64/companion.json")) expired = true; };
vi.spyOn(Date, "now").mockImplementation(() => expired ? 600_001 : 0);
await expect(importRemotePiCompanion(f)).rejects.toThrow("deadline exceeded");
expect(readdirSync(join(f.serverRoot, "remote-companions"))).toEqual(["linux-x64"]);
});
async function largeFixture() {
const f = await fixture(); writeFileSync(join(f.directory, "provider-pack/large"), Buffer.alloc(8 * 1024 ** 2, 0x61));
f.manifest = await createRemotePiCompanionManifest(f.directory, f.source);
const bytes = JSON.stringify(f.manifest); writeFileSync(join(f.directory, "companion.json"), bytes); f.sha256 = hash(bytes); return f;
}
it("yields to unrelated event-loop work between chunks of a real cache file", async () => {
const f = await largeFixture(); await importRemotePiCompanion(f);
const file = join(f.serverRoot, "remote-companions/linux-x64/provider-pack/large");
let reads = 0; let serviced = false; let servicedBeforeNextRead = false;
hooks.read = (path, bytes) => { if(path !== file || !bytes) return; reads++; if(reads === 1) setImmediate(() => { serviced = true; }); else servicedBeforeNextRead ||= serviced; };
expect((await resolveRemotePiCompanion({serverRoot:f.serverRoot}))?.manifestSha256).toBe(f.sha256);
expect(reads).toBeGreaterThanOrEqual(8); expect(servicedBeforeNextRead).toBe(true);
});
it("cancels during a large existing-cache file before reading the whole file and preserves it", async () => {
const f = await largeFixture(); await importRemotePiCompanion(f);
const file = join(f.serverRoot, "remote-companions/linux-x64/provider-pack/large"); let readBytes = 0;
hooks.read = (path, bytes) => { if(path === file) readBytes += bytes; };
await expect(importRemotePiCompanion({...f, checkCancelled(){ if(readBytes) throw Error("cancel during cache bytes"); }})).rejects.toThrow("cancel during cache bytes");
expect(readBytes).toBeGreaterThan(0); expect(readBytes).toBeLessThan(8 * 1024 ** 2);
expect(readdirSync(join(f.serverRoot, "remote-companions"))).toEqual(["linux-x64"]);
});
it("rejects read-only directory modes before claiming a staging or output path", async () => {
const f = await fixture(); const dir = join(f.directory, "provider-pack"); chmodSync(dir, 0o500);
try {
await expect(importRemotePiCompanion({...f, checkCancelled(){}})).rejects.toThrow("owner read/write/search");
expect(() => readdirSync(join(f.serverRoot, "remote-companions"))).toThrow();
} finally { chmodSync(dir, 0o755); }
});
+5 -5
View File
@@ -410,7 +410,7 @@ const paperclipRunnerAdapter: ServerAdapterModule = {
checks: [{ code: "dot_event_test_required", level: "warn" as const, message: "Dot manages its model and billing. Validate the dedicated agent binding and event round trip in Paperclip; this read-only check does not wake the Dot." }] };
}
if (profile.provider === "acpx") {
if (["copilot", "pi"].includes(profile.acpxAgent)) {
if (["copilot"].includes(profile.acpxAgent)) {
// The profile resolver already validated the isolated host's exact
// qualification pair. Do not report a production readiness pass.
return {
@@ -420,7 +420,7 @@ const paperclipRunnerAdapter: ServerAdapterModule = {
};
}
try {
if (profile.acpxAgent !== "claude" && profile.acpxAgent !== "grok" && profile.acpxAgent !== "cursor") throw new Error("Select Codex to use the native Codex runner.");
if (profile.acpxAgent !== "claude" && profile.acpxAgent !== "grok" && profile.acpxAgent !== "cursor" && profile.acpxAgent !== "pi") throw new Error("Select Codex to use the native Codex runner.");
const target = context.executionTarget;
if (target?.kind === "remote") {
const probe = await runAdapterExecutionTargetShellCommand(
@@ -438,8 +438,8 @@ const paperclipRunnerAdapter: ServerAdapterModule = {
message: "The remote platform is supported. Runtime package integrity and readiness must still be verified by the remote runner before launch." }],
};
}
const { probeAcpxClaudeInstallation, probeAcpxGrokInstallation, probeAcpxCursorInstallation } = await import("../vendor/paperclip-runner/live/index.js");
await (profile.acpxAgent === "grok" ? probeAcpxGrokInstallation : profile.acpxAgent === "cursor" ? probeAcpxCursorInstallation : probeAcpxClaudeInstallation)(profile.model);
const { probeAcpxClaudeInstallation, probeAcpxGrokInstallation, probeAcpxCursorInstallation, probeAcpxPiInstallation } = await import("../vendor/paperclip-runner/live/index.js");
await (profile.acpxAgent === "pi" ? probeAcpxPiInstallation : profile.acpxAgent === "grok" ? probeAcpxGrokInstallation : profile.acpxAgent === "cursor" ? probeAcpxCursorInstallation : probeAcpxClaudeInstallation)(profile.model);
return {
adapterType: "paperclip_runner", status: "pass" as const, testedAt: new Date().toISOString(),
checks: [{ code: "acpx_runtime_ready", level: "info" as const, message: `ACPX ${profile.acpxAgent} runtime is installed and verified. Model access is checked when it runs.` }],
@@ -525,7 +525,7 @@ const paperclipRunnerAdapter: ServerAdapterModule = {
)
: buildNpmRuntimeCommandSpec(config, "codex", "@openai/codex@0.160.0"),
agentConfigurationDoc:
"# Paperclip Runner\n\nAdapter: paperclip_runner\n\nRuns Codex, OpenCode, Claude Managed, AWS AgentCore, or ACPX Claude/Grok Build/Cursor through the Rust Paperclip runner and authenticated PRP transport. GitHub Copilot and Pi are awaiting local and Daytona qualification and are not enabled for production runs. Managed providers use company-scoped qualified profiles, explicit retention acknowledgement, and spend limits.\n",
"# Paperclip Runner\n\nAdapter: paperclip_runner\n\nRuns Codex, OpenCode, Claude Managed, AWS AgentCore, or ACPX Claude/Grok Build/Cursor/Pi through the Rust Paperclip runner and authenticated PRP transport. Pi accepts an explicit provider/model ID and uses the company credentials bound to the agent environment. GitHub Copilot awaits local and Daytona qualification. Managed providers use company-scoped qualified profiles, explicit retention acknowledgement, and spend limits.\n",
getConfigSchema: () => ({
fields: [
{
-45
View File
@@ -878,45 +878,6 @@ function isPaperclipSchemaDiscriminator(
);
}
/** Only restore the public literal in this closed receipt-notice context.
* Details use {name,value}, so the ordinary schema-key exemption cannot apply.
* Every other value still passes the generic secret/JWT redactor below. */
function semanticReceiptSchemaDetail(record: Record<string, unknown>): number | null {
if (Object.keys(record).sort().join(",") !== "category,details,noticeId,provenance,recoverable,schema,scope,severity,summary,userActionable"
|| record.schema !== "paperclip.provider.notice.v1" || record.scope !== "turn"
|| record.severity !== "info" || record.recoverable !== true || record.userActionable !== false
|| record.summary !== "Paperclip returned a semantic tool result."
|| typeof record.noticeId !== "string" || !/^copilot-evidence-[a-f0-9]{24}-[1-9][0-9]{0,3}$/.test(record.noticeId)
|| Number(record.noticeId.slice(record.noticeId.lastIndexOf("-") + 1)) > 2048
|| !isPlainObject(record.provenance) || !Array.isArray(record.details)) return null;
const provenance = record.provenance;
if (Object.keys(provenance).sort().join(",") !== "eventType,method,sessionId,turnId"
|| provenance.method !== "paperclip/semantic_tool_result" || provenance.eventType !== "semantic_result"
|| ![provenance.sessionId, provenance.turnId].every(value => typeof value === "string"
&& value.length > 0 && value.length <= 240 && !/[\u0000-\u001f\u007f]/u.test(value))) return null;
const version = record.category === "paperclip_semantic_tool_receipt_v1" ? 1
: record.category === "paperclip_semantic_tool_receipt_v2" ? 2 : null;
if (version === null || record.details.length !== (version === 1 ? 7 : 8)) return null;
const details = new Map<string, string>();
let schemaIndex: number | null = null;
for (const [index, detail] of record.details.entries()) {
if (!isPlainObject(detail) || Object.keys(detail).sort().join(",") !== "name,value"
|| typeof detail.name !== "string" || typeof detail.value !== "string" || details.has(detail.name)) return null;
details.set(detail.name, detail.value);
if (detail.name === "schema") schemaIndex = index;
}
const names = ["stage", "schema", "operationId", "callIdentitySha256", "inputSha256", "resultSha256", "outcome",
...(version === 2 ? ["normalizedInputSha256"] : [])];
const hex = (value: string | undefined) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
if (!names.every(name => details.has(name)) || details.get("stage") !== "semantic_result"
|| details.get("schema") !== `paperclip.semantic_tool_receipt.v${version}`
|| !/^[A-Za-z0-9_.:-]{1,256}$/.test(details.get("operationId") ?? "")
|| !["callIdentitySha256", "inputSha256", "resultSha256"].every(name => hex(details.get(name)))
|| !["returned", "error"].includes(details.get("outcome") ?? "")
|| (version === 2 && details.get("normalizedInputSha256") !== "null" && !hex(details.get("normalizedInputSha256")))) return null;
return schemaIndex;
}
export function sanitizeRecord(
record: Record<string, unknown>,
): Record<string, unknown> {
@@ -974,12 +935,6 @@ export function sanitizeRecord(
}
redacted[key] = sanitizeValue(value);
}
const schemaIndex = semanticReceiptSchemaDetail(record);
if (schemaIndex !== null && Array.isArray(redacted.details)) {
// Restore only the exact checked discriminator, never sibling data.
(redacted.details[schemaIndex] as Record<string, unknown>).value =
(record.details as Array<Record<string, unknown>>)[schemaIndex]!.value;
}
return redacted;
}
+1 -1
View File
@@ -1,3 +1,4 @@
import { cancellationRequestId } from "../services/native-runtime/native-cancellation-request.js";
import { aiRoutingHarness } from "@paperclipai/shared";
import { agentIdentityService } from "../services/agent-identity.js";
import { aiConnectionRouterService, poolMemberRuntimeConfig } from "../services/ai-connection-router.js";
@@ -6,7 +7,6 @@ import { dotRunnerBroker } from "../services/dot-runner-broker.js";
import { publicMcpConfig } from "../services/public-mcp/oauth.js";
import { connectionIntentDeliveryService } from "../services/connection-intent-delivery.js";
import { completeConnectionIntentSchema } from "@paperclipai/shared";
import { cancellationRequestId } from "../services/native-runtime/native-cancellation-request.js";
import { agentFileStore, agentFileTokenFromHash } from "../services/agent-file-store.js";
import { pipeline } from "node:stream/promises";
import { resolveAgentAppearance, agentAvatarUrl } from "@paperclipai/shared";
@@ -13,7 +13,7 @@ import { HttpError, conflict, notFound } from "../errors.js";
import type { AuthorizationActor } from "./authorization.js";
import type { EnvironmentRuntimeService } from "./environment-runtime.js";
import type { Environment, EnvironmentLease } from "@paperclipai/shared";
import { agentDirectoryBaselineDigest, agentDirectoryProbeProgram, agentDirectoryTransferCleanupProgram, observeLocalAgentDirectory } from "./agent-directory-probe.js";
import { agentDirectoryBaselineDigest, agentDirectoryProbeProgram, observeLocalAgentDirectory } from "./agent-directory-probe.js";
import { hasRemoteTerminationReceipt } from "./remote-execution-termination.js";
import { cachedAgentFileManifest, captureAgentFileCheckpoint, checkpointBaseline, checkpointSnapshot, type AgentFileManifest } from "./agent-file-checkpoints.js";
import { logger } from "../middleware/logger.js";
@@ -23,6 +23,10 @@ export class AgentDirectoryReuseInvalidatedError extends Error {}
const completed = new Set(["saved", "unchanged", "resolved", "unavailable"]);
const transports = new Map<string, PreparedAdapterExecutionTargetRuntime>();
const key = (row: Pick<Copy, "companyId" | "runId">) => `${row.companyId}:${row.runId}`;
function sandboxTransferRoot(row: Copy, remoteCwd: string): string {
const origin = typeof row.receipt?.materializationRunId === "string" ? row.receipt.materializationRunId : String(row.receipt?.directoryRunId ?? row.runId);
return path.posix.join(remoteCwd, ".paperclip-runtime", "paperclip-runner", "agent-file-transfers", row.agentId, origin);
}
export function isAgentDirectoryCopy(row: Pick<Copy, "receipt"> | null): boolean {
return row?.receipt?.schema === AGENT_FILES_CONTRACT;
}
@@ -62,10 +66,30 @@ export function agentDirectoryWorkingCopyService(db: Db, get: (companyId: string
}
return prepareAdapterExecutionTargetRuntime({ target, runId: row.runId, adapterKey: "agent-files",
workspaceLocalDir: row.localRoot, workspaceRemoteDir: row.executionRoot,
// Agent files are an independently observed native directory. Transfer
// scratch belongs to the host runtime, including the fallback selected
// while plugin capability discovery is cold after controller restart.
runtimeRootDir: sandboxTransferRoot(row, target.remoteCwd),
syncWorkspace: true, workspaceInboundMode: recovering ? "adopt_remote" : undefined,
workspaceBaseline: baseline(row), workspaceGitSnapshot: null, workspaceFileMode: "all",
workspaceExclude: [".paperclip-runtime", ".paperclip-runtime/**"] });
}
async function observe(row: Copy, target?: AdapterExecutionTarget | null) {
if (row.location === "local") {
// The same bounded program validates aliases on its actual host.
return observeLocalAgentDirectory(row.localRoot);
}
if (!target || target.kind !== "remote" || row.location !== `remote:${target.environmentId ?? ""}`) throw new Error("Agent directory environment changed");
const origin = typeof row.receipt?.materializationRunId === "string" ? row.receipt.materializationRunId : row.runId;
if (row.executionRoot !== path.posix.join(target.remoteCwd, ".paperclip-runtime", "agent-files", row.agentId, origin)) throw new Error("Agent directory root changed");
const quote = (value: string) => `'${value.replaceAll("'", `'"'"'`)}'`;
const result = await runAdapterExecutionTargetShellCommand(row.runId, target,
`node -e ${quote(agentDirectoryProbeProgram)} ${quote(row.executionRoot)}`, { cwd: target.remoteCwd, env: {}, timeoutSec: 10 });
if (result.exitCode !== 0 || result.timedOut || result.stdout.length > 1024) throw new Error("Agent directory observation unavailable");
const value = JSON.parse(result.stdout);
if (!value || typeof value.digest !== "string" || !/^[a-f0-9]{64}$/.test(value.digest) || typeof value.identity !== "string") throw new Error("Invalid agent directory observation");
return value as { digest: string; identity: string };
}
async function prepareCopy(input: { companyId: string; agentId: string; runId: string; target?: AdapterExecutionTarget | null; cwd: string; warm?: boolean; reuseRunId?: string; onWarmHandoff?: (copy: Copy) => void }, serialHeld = false): Promise<Copy | null> {
const [agent] = await db.select().from(agents).where(and(eq(agents.id, input.agentId), eq(agents.companyId, input.companyId)));
if (!agent) throw notFound("Agent not found");
@@ -189,11 +213,7 @@ export function agentDirectoryWorkingCopyService(db: Db, get: (companyId: string
const runtime = await transport(row, input.target, false);
transports.set(key(row), runtime);
if (input.target.transport === "sandbox") {
if (runtime.runtimeRootDir !== path.posix.join(row.executionRoot, ".paperclip-runtime", "agent-files")) throw new Error("Agent directory transfer scratch path changed");
const retired = await runAdapterExecutionTargetShellCommand(input.runId, input.target,
`node -e ${quote(agentDirectoryTransferCleanupProgram)} ${quote(row.executionRoot)}`,
{ cwd: input.target.remoteCwd, env: {}, timeoutSec: 10 });
if (retired.exitCode !== 0 || retired.timedOut) throw new Error("Agent directory transfer scratch could not be safely retired");
if (runtime.runtimeRootDir !== sandboxTransferRoot(row, input.target.remoteCwd)) throw new Error("Agent directory transfer scratch path changed");
}
}
const observed = await observe(row, input.target).catch(() => null);
@@ -394,22 +414,36 @@ export function agentDirectoryWorkingCopyService(db: Db, get: (companyId: string
async function release(row: Copy, target?: AdapterExecutionTarget | null) {
if (releaseIsNoop(row) || !await owns(row)) return;
const destroyedOnly = row.receipt?.cleanupDestroyedOnly === true;
if (row.receipt?.retainedByRunId || row.state === "superseded" || !await owns(row) || !row.processStoppedAt) return;
// Collection and environment teardown can both release the same copy.
// A compact successful cleanup receipt is final, even after restart.
if (completed.has(row.state) && row.processStoppedAt && row.receipt?.cleanupPending === false) return;
const runtime = transports.get(key(row));
transports.delete(key(row));
let cleanupPending = false;
await runtime?.cleanupWorkspaceSnapshot?.().catch(() => { cleanupPending = true; });
const cleanupTarget = target ?? runtime?.target;
if (!destroyedOnly && row.processStoppedAt && completed.has(row.state) && cleanupTarget?.kind === "remote") {
const expected = path.posix.join(cleanupTarget.remoteCwd, ".paperclip-runtime", "agent-files", row.agentId, String(row.receipt?.directoryRunId ?? row.runId));
const lease = row.location !== "local" ? await ownedRemoteLease(row) : null;
const terminated = lease && hasRemoteTerminationReceipt(lease);
const destroyed = destroyedOnly || terminated && (lease.metadata?.remoteExecutionTermination as { state?: string } | undefined)?.state === "destroyed";
const cleanupLeaseId = (row.receipt?.cleanup as { leaseId?: string } | undefined)?.leaseId;
// Maintenance may run while the collector is still cached. A cached target
// cannot override the current lease's stopped/destroyed/uncertain state.
const remoteUnavailable = row.location !== "local" && !destroyed && (!lease || terminated
|| Boolean(lease && (lease.releasedAt || lease.status !== "active"))
|| Boolean(cleanupLeaseId && (!lease || cleanupTarget?.kind === "remote" && cleanupTarget.leaseId !== lease.id)));
if (remoteUnavailable) cleanupPending = true;
else if (!destroyed && row.processStoppedAt && completed.has(row.state) && cleanupTarget?.kind === "remote") {
const expected = path.posix.join(cleanupTarget.remoteCwd, ".paperclip-runtime", "agent-files", row.agentId, typeof row.receipt?.materializationRunId === "string" ? row.receipt.materializationRunId : String(row.receipt?.directoryRunId ?? row.runId));
if (row.executionRoot !== expected) throw new Error("Agent directory cleanup path changed");
const quoted = `'${expected.replaceAll("'", `'"'"'`)}'`;
const paths = cleanupTarget.transport === "sandbox" ? [expected, sandboxTransferRoot(row, cleanupTarget.remoteCwd)] : [expected];
const quoted = paths.map(p => `'${p.replaceAll("'", `'"'"'`)}'`).join(" ");
const remoteCleanupFailed = await runAdapterExecutionTargetShellCommand(row.runId, cleanupTarget, `rm -rf -- ${quoted}`,
{ cwd: cleanupTarget.remoteCwd, env: {}, timeoutSec: 15 }).then(result => result.exitCode !== 0 || result.timedOut, () => true);
cleanupPending ||= remoteCleanupFailed;
} else if (row.processStoppedAt && completed.has(row.state) && row.location !== "local") {
// Rebind only the original host-owned lease. Never acquire a replacement
// sandbox or persist transport credentials in a working-copy receipt.
cleanupPending ||= !await cleanupRemoteAfterRestart(row, destroyedOnly).catch(() => false);
} else if (!destroyed && row.processStoppedAt && completed.has(row.state) && row.location !== "local") {
// Rebind only the current host-owned lease. Never acquire a replacement.
cleanupPending ||= !await cleanupRemoteAfterRestart(row).catch(() => false);
}
if (completed.has(row.state) && row.processStoppedAt) {
try { await fs.rm(path.dirname(row.localRoot), { recursive: true, force: true }); }
@@ -437,7 +471,7 @@ export function agentDirectoryWorkingCopyService(db: Db, get: (companyId: string
if (lease.environmentId !== null && row.location !== `remote:${lease.environmentId}`) return false;
const remoteCwd = cleanup?.remoteCwd ?? lease.metadata?.remoteCwd;
return typeof remoteCwd === "string" &&
row.executionRoot === path.posix.join(remoteCwd, ".paperclip-runtime", "agent-files", row.agentId, String(row.receipt?.directoryRunId ?? row.runId)) &&
row.executionRoot === path.posix.join(remoteCwd, ".paperclip-runtime", "agent-files", row.agentId, String(row.receipt?.materializationRunId ?? row.receipt?.directoryRunId ?? row.runId)) &&
hasRemoteTerminationReceipt(lease) && (lease.metadata?.remoteExecutionTermination as { state?: string }).state === "destroyed";
}
async function recoverUnavailable(row: Copy) {
@@ -455,10 +489,36 @@ export function agentDirectoryWorkingCopyService(db: Db, get: (companyId: string
if (confirmed.state === "unavailable" && confirmed.processStoppedAt) await release(confirmed);
}, "agent_directory_release");
}
async function cleanupRemoteAfterRestart(row: Copy, destroyedOnly = false): Promise<boolean> {
// Newly recovered loss receipts permit no command that could wake a stopped
// provider. Recheck the exact destruction binding even after lock acquisition.
if (destroyedOnly) return destroyedRemoteCopy(row);
async function ownedRemoteLease(row: Copy) {
const cleanup = row.receipt?.cleanup as { leaseId?: string } | undefined;
const leases = await db.select().from(environmentLeases).where(and(
eq(environmentLeases.companyId, row.companyId), eq(environmentLeases.heartbeatRunId, row.runId),
cleanup?.leaseId ? eq(environmentLeases.id, cleanup.leaseId) : eq(environmentLeases.environmentId, row.location.slice("remote:".length)),
));
if (leases.length !== 1) return null;
const lease = leases[0]!;
const destroyedWithoutEnvironment = lease.environmentId === null && hasRemoteTerminationReceipt(lease)
&& (lease.metadata?.remoteExecutionTermination as { state?: string } | undefined)?.state === "destroyed";
if (row.location !== `remote:${lease.environmentId}` && !destroyedWithoutEnvironment) return null;
return lease;
}
async function recoverStoppedRemote(row: Copy) {
const lease = await ownedRemoteLease(row);
const destroyed = lease && hasRemoteTerminationReceipt(lease)
&& (lease.metadata?.remoteExecutionTermination as { state?: string } | undefined)?.state === "destroyed";
if (!destroyed) {
// execute (including bypassSession) may restart a stopped sandbox. Keep
// the only remaining bytes; a stop receipt is not a destruction receipt.
return patch(row, { state: "pending_collection", errorCode: lease ? "INSTRUCTION_STOPPED_REMOTE_COLLECTION_PENDING" : "INSTRUCTION_REMOTE_LEASE_UNVERIFIED",
errorMessage: lease ? "The remote provider stopped with its agent directory retained. Safe stopped-file retrieval is unavailable; no save or discard is claimed."
: "The current remote lease could not be verified. Its agent directory is preserved; no retrieval, save or discard is claimed.", nextAttemptAt: new Date(Date.now() + 30_000) });
}
row = await patch(row, { state: "unavailable", processStoppedAt: row.processStoppedAt ?? new Date(),
errorCode: "INSTRUCTION_COLLECTION_UNAVAILABLE", errorMessage: "The exact remote sandbox was destroyed before its agent directory was retrieved. No instruction save is claimed.", nextAttemptAt: null });
await release(row);
return (await get(row.companyId, row.runId))!;
}
async function cleanupRemoteAfterRestart(row: Copy): Promise<boolean> {
const cleanup = row.receipt?.cleanup as { leaseId?: string; remoteCwd?: string } | undefined;
const lease = await ownedRemoteLease(row);
if (!lease) return false;
@@ -471,7 +531,8 @@ export function agentDirectoryWorkingCopyService(db: Db, get: (companyId: string
if (!environment) return false;
const remoteCwd = cleanup?.remoteCwd ?? lease.metadata?.remoteCwd;
if (typeof remoteCwd !== "string" || row.executionRoot !== path.posix.join(remoteCwd, ".paperclip-runtime", "agent-files", row.agentId, typeof row.receipt?.materializationRunId === "string" ? row.receipt.materializationRunId : String(row.receipt?.directoryRunId ?? row.runId))) return false;
const result = await environmentRuntime.execute({ environment: environment as Environment, lease: lease as EnvironmentLease, command: "rm", args: ["-rf", "--", row.executionRoot],
const paths = environment.driver === "sandbox" ? [row.executionRoot, sandboxTransferRoot(row, remoteCwd)] : [row.executionRoot];
const result = await environmentRuntime.execute({ environment: environment as Environment, lease: lease as EnvironmentLease, command: "rm", args: ["-rf", "--", ...paths],
cwd: remoteCwd, env: {}, timeoutMs: 15_000, bypassSession: true });
return result.exitCode === 0 && !result.timedOut;
}
@@ -484,7 +545,8 @@ export function agentDirectoryWorkingCopyService(db: Db, get: (companyId: string
return fn(current);
}, process.env, operation);
}
return { prepare: (input: Parameters<typeof prepareCopy>[0]) => prepareCopy(input), hasChanges, canReuse, recoverUnavailable,
return { prepare: (input: Parameters<typeof prepareCopy>[0]) => prepareCopy(input), hasChanges, adopt, canReuse, recoverUnavailable,
recoverStoppedRemote: (row: Copy) => serial(row, current => current.receipt?.retainedByRunId ? Promise.resolve(current) : recoverStoppedRemote(current), "agent_directory_collect"),
checkpointWarm: (row: Copy, target?: AdapterExecutionTarget | null) => serial(row, current => current.receipt?.warm === true ? checkpoint(current, target) : Promise.resolve(current), "agent_directory_checkpoint"),
collectStopped: (row: Copy, target?: AdapterExecutionTarget | null) => serial(row, current => collectStopped(current, target), "agent_directory_collect"),
release: async (row: Copy) => {
@@ -328,12 +328,13 @@ export function agentInstructionWorkingCopyService(db: Db, options: { environmen
async function recoverStopped() {
const pending = await db.select({ copy: copies, runtimeMode: heartbeatRuns.runtimeMode }).from(copies)
.innerJoin(heartbeatRuns, and(eq(heartbeatRuns.companyId, copies.companyId), eq(heartbeatRuns.id, copies.runId)))
.where(and(or(and(or(inArray(copies.state, ["prepared", "pending_collection", "warm_saved"]),
.where(and(or(and(or(inArray(copies.state, ["prepared", "pending_collection", "unchanged_turn", "warm_saved"]),
and(eq(copies.state, "preparing"), sql`${copies.receipt}->>'schema' = 'paperclip.agent-files.v1'`)),
lte(copies.attempts, MAX_COLLECTION_ATTEMPTS - 1)),
and(eq(copies.state, "unavailable"), isNull(copies.processStoppedAt),
sql`${copies.location} like 'remote:%'`,
sql`${copies.receipt}->>'schema' = 'paperclip.agent-files.v1'`)),
sql`NOT (coalesce(${copies.receipt}, '{}'::jsonb) ? 'retainedByRunId')`,
inArray(heartbeatRuns.status, ["succeeded", "failed", "cancelled", "timed_out", "interrupted"]),
or(isNull(copies.nextAttemptAt), lte(copies.nextAttemptAt, new Date())))).orderBy(asc(copies.updatedAt)).limit(20);
for (const { copy: row, runtimeMode } of pending) {
@@ -363,8 +364,8 @@ export function agentInstructionWorkingCopyService(db: Db, options: { environmen
}
} else if (row.state === "prepared") {
await patch(row, { state: "pending_collection", errorCode: "INSTRUCTION_STOP_UNCONFIRMED",
errorMessage: "The provider's stop has not been confirmed. Instruction collection is pending; no save is claimed.", nextAttemptAt: null });
} else if (row.state === "warm_saved") {
errorMessage: "The provider's stop has not been confirmed. Instruction collection is pending; no save is claimed.", nextAttemptAt: new Date(Date.now() + 30_000) });
} else if (["warm_saved", "pending_collection", "unchanged_turn"].includes(row.state)) {
// Live retained sessions must not occupy every batch and starve stopped
// copies from other agents. This does not permit reads without stop proof.
await patch(row, { nextAttemptAt: new Date(Date.now() + 30_000) });
@@ -273,6 +273,8 @@ export function environmentRunOrchestrator(
agentId: string;
persistedExecutionWorkspace: Pick<ExecutionWorkspace, "id" | "mode"> | null;
executionWorkspaceSettings: IssueExecutionWorkspaceSettings | null;
/** Existing live runner recovery must use its original active lease, including ephemeral leases. */
reattachRemoteLease?: { leaseId: string; providerLeaseId: string; remoteCwd: string };
}): Promise<EnvironmentAcquisitionResult> {
// Step 1: Resolve environment
const selectedEnvironment = await resolveEnvironment({
@@ -286,7 +288,7 @@ export function environmentRunOrchestrator(
);
// Step 2: Acquire lease
const leaseRecord = await acquireLease({
const acquisitionInput = {
companyId: input.companyId,
environment,
issueId: input.issueId,
@@ -295,7 +297,29 @@ export function environmentRunOrchestrator(
persistedExecutionWorkspace: input.persistedExecutionWorkspace,
executionWorkspaceSettings: input.executionWorkspaceSettings,
adapterType: input.adapterType ?? null,
});
};
let leaseRecord: EnvironmentRuntimeLeaseRecord;
if (input.reattachRemoteLease) {
// Reattachment is inspection of an already running sandbox, never a new
// acquisition or a lifecycle resume. Those paths can create a replacement
// when the admitted per-turn policy deliberately disables reusable leases.
const expected = input.reattachRemoteLease;
const lease = await environmentsSvc.getLeaseById(expected.leaseId);
if (!lease || environment.driver !== "sandbox" ||
lease.companyId !== input.companyId || lease.environmentId !== environment.id ||
lease.heartbeatRunId !== input.heartbeatRunId || lease.issueId !== input.issueId ||
lease.executionWorkspaceId !== (input.persistedExecutionWorkspace?.id ?? null) ||
lease.metadata?.agentId !== input.agentId || lease.status !== "active" ||
lease.releasedAt !== null || lease.cleanupStatus !== null ||
(lease.expiresAt !== null && new Date(lease.expiresAt).getTime() <= Date.now()) ||
lease.provider !== environment.config.provider || lease.providerLeaseId !== expected.providerLeaseId ||
lease.metadata?.remoteCwd !== expected.remoteCwd) {
throw new Error("native_remote_recovery_lease_mismatch");
}
leaseRecord = { environment, lease, leaseContext: buildEnvironmentLeaseContext(input) };
} else {
leaseRecord = await acquireLease(acquisitionInput);
}
// Step 3: Log lease acquisition activity
await logActivity(db, {
+94 -71
View File
@@ -3875,6 +3875,86 @@ export function environmentRuntimeService(
return driver;
}
async function destroyScopedReusableSandboxLease(input: {
environment: Environment;
leaseRow: typeof environmentLeases.$inferSelect;
scopeCondition?: ReturnType<typeof and>;
failureReason: string;
}): Promise<EnvironmentLease | null> {
const now = new Date();
const attemptId = randomUUID();
// Persist recovery ownership before any provider work. Re-check scope and
// run liveness in this write: the earlier selection cannot fence a resume.
// The in-flight lease also excludes concurrent closures and cleanup sweeps.
const [claimed] = await db.update(environmentLeases).set({
status: "pending_cleanup",
failureReason: input.failureReason,
cleanupStatus: "failed",
releasedAt: now,
lastUsedAt: now,
updatedAt: now,
metadata: sql`(case when jsonb_typeof(${environmentLeases.metadata}) = 'object'
then ${environmentLeases.metadata} else '{}'::jsonb end - 'remoteExecutionTermination')
|| ${JSON.stringify({ pendingCleanupAttemptId: attemptId, pendingCleanupInFlight: true,
pendingCleanupLeaseExpiresAtMs: Date.now() + 15 * 60_000 })}::jsonb`,
}).where(and(
eq(environmentLeases.id, input.leaseRow.id),
eq(environmentLeases.companyId, input.leaseRow.companyId),
eq(environmentLeases.environmentId, input.environment.id),
eq(environmentLeases.leasePolicy, "reuse_by_environment"),
eq(environmentLeases.status, input.leaseRow.status),
input.scopeCondition,
sql`coalesce(${environmentLeases.metadata}->>'pendingCleanupInFlight', 'false') != 'true'`,
sql`NOT EXISTS (
SELECT 1 FROM ${heartbeatRuns}
WHERE ${heartbeatRuns.id} = ${environmentLeases.heartbeatRunId}
AND ${heartbeatRuns.status} IN ('queued', 'scheduled_retry', 'running')
)`,
)).returning();
if (!claimed) return null;
const lease = toEnvironmentLeaseSnapshot(claimed);
let renewalInFlight = false;
const renewal = setInterval(() => {
if (renewalInFlight) return;
renewalInFlight = true;
void db.update(environmentLeases).set({
metadata: sql`${environmentLeases.metadata} || ${JSON.stringify({
pendingCleanupLeaseExpiresAtMs: Date.now() + 15 * 60_000,
})}::jsonb`,
}).where(and(eq(environmentLeases.id, lease.id), eq(environmentLeases.status, "pending_cleanup"),
sql`${environmentLeases.metadata}->>'pendingCleanupAttemptId' = ${attemptId}`,
sql`${environmentLeases.metadata}->>'pendingCleanupInFlight' = 'true'`,
)).then(() => {}).catch(() => {
logger.warn({ leaseId: lease.id }, "scoped cleanup ownership renewal failed");
}).finally(() => { renewalInFlight = false; });
}, 30_000);
renewal.unref();
try {
const driver = getDriver(getLeaseDriverKey(lease, input.environment));
if (!driver?.destroyRunLease) return lease;
return await driver.destroyRunLease({
environment: input.environment,
lease,
failureReason: input.failureReason,
}) ?? lease;
} catch {
// Even a failed settlement write leaves the durable provider handle for
// the sweep. Continue with later leases without logging provider errors.
return lease;
} finally {
clearInterval(renewal);
// A crash skips this write; the bounded ownership lease lets the sweep
// recover. A completed failure becomes eligible for a later explicit retry.
await db.update(environmentLeases).set({
metadata: sql`${environmentLeases.metadata} || '{"pendingCleanupInFlight":false}'::jsonb`,
}).where(and(eq(environmentLeases.id, lease.id),
sql`${environmentLeases.metadata}->>'pendingCleanupAttemptId' = ${attemptId}`,
)).catch(() => {
logger.warn({ leaseId: lease.id }, "scoped cleanup ownership settlement failed");
});
}
}
return {
getDriver,
@@ -4180,7 +4260,7 @@ export function environmentRuntimeService(
and(
eq(environmentLeases.companyId, input.companyId),
eq(environmentLeases.leasePolicy, "reuse_by_environment"),
inArray(environmentLeases.status, ["active", "released", "retained", "pending_cleanup"]),
inArray(environmentLeases.status, ["active", "released", "retained", "failed", "pending_cleanup"]),
...scopeConditions,
),
);
@@ -4222,18 +4302,12 @@ export function environmentRuntimeService(
? await environmentsSvc.getById(leaseRow.environmentId)
: null;
if (!environment) continue;
const leaseSnapshot = toEnvironmentLeaseSnapshot(leaseRow);
const driver = getDriver(getLeaseDriverKey(leaseSnapshot, environment));
const lease = driver?.destroyRunLease
? await driver.destroyRunLease({
environment,
lease: leaseSnapshot,
failureReason: input.failureReason ?? "reusable_lease_destroyed",
})
: await environmentsSvc.releaseLease(leaseSnapshot.id, "pending_cleanup", {
failureReason: input.failureReason ?? "reusable_lease_destroyed",
cleanupStatus: "failed",
});
const lease = await destroyScopedReusableSandboxLease({
environment,
leaseRow,
scopeCondition: and(...scopeConditions),
failureReason: input.failureReason ?? "reusable_lease_destroyed",
});
if (!lease) continue;
destroyed.push({
environment,
@@ -4271,7 +4345,9 @@ export function environmentRuntimeService(
and(
eq(environmentLeases.environmentId, input.environmentId),
eq(environmentLeases.leasePolicy, "reuse_by_environment"),
inArray(environmentLeases.status, ["active", "released", "retained"]),
// A failed run may have stopped its reusable sandbox without
// destroying it; that provider handle still needs scoped teardown.
inArray(environmentLeases.status, ["active", "released", "retained", "failed"]),
),
);
@@ -4302,73 +4378,20 @@ export function environmentRuntimeService(
let failed = 0;
let skippedLiveRun = 0;
const failureReason = input.failureReason ?? "environment_delete_requested";
const now = new Date();
for (const leaseRow of leaseRows) {
if (leaseRow.heartbeatRunId && liveRunIds.has(leaseRow.heartbeatRunId)) {
skippedLiveRun += 1;
continue;
}
// Claim the row BEFORE the provider call, mirroring the inline-teardown
// invariant used elsewhere in this file: no provider destroy without a
// durable `pending_cleanup` reference already on disk. The claim is one
// conditional UPDATE, so it is the fence against a racing resume: a
// resume that re-activates the lease first makes the status predicate
// (or the run-liveness predicate) fail and the claim loses — the live
// run keeps its sandbox. A claim that wins parks the lease where the
// cleanup sweep owns it, so a crash or thrown destroy after this point
// is recovered by the sweep's idempotent teardown, and a double write
// failure cannot strand the lease in a reusable status.
const claimedRow = await db
.update(environmentLeases)
.set({
status: "pending_cleanup",
failureReason,
cleanupStatus: "failed",
releasedAt: now,
lastUsedAt: now,
updatedAt: now,
})
.where(
and(
eq(environmentLeases.id, leaseRow.id),
inArray(environmentLeases.status, ["active", "released", "retained"]),
sql`NOT EXISTS (
SELECT 1 FROM ${heartbeatRuns}
WHERE ${heartbeatRuns.id} = ${environmentLeases.heartbeatRunId}
AND ${heartbeatRuns.status} IN ('queued', 'scheduled_retry', 'running')
)`,
),
)
.returning()
.then((rows) => rows[0] ?? null);
if (!claimedRow) {
const lease = await destroyScopedReusableSandboxLease({ environment, leaseRow, failureReason });
if (!lease) {
// Lost to a racing resume or a concurrent terminal transition — the
// lease is no longer ours to destroy.
skippedLiveRun += 1;
continue;
}
const leaseSnapshot = toEnvironmentLeaseSnapshot(claimedRow);
try {
const driver = getDriver(getLeaseDriverKey(leaseSnapshot, environment));
if (!driver?.destroyRunLease) {
// No driver available: the claim already parked the lease for the
// sweep, which retries once the driver's plugin is back.
failed += 1;
continue;
}
const lease = await driver.destroyRunLease({
environment,
lease: leaseSnapshot,
failureReason,
});
if (lease && lease.status !== "pending_cleanup") destroyed += 1;
else failed += 1;
} catch {
// The claim above already parked the lease in `pending_cleanup`, so
// the sweep owns the retry; its teardown is idempotent, so a destroy
// that reached the provider before the throw resolves as success.
failed += 1;
}
if (lease.status !== "pending_cleanup") destroyed += 1;
else failed += 1;
}
return { destroyed, failed, skippedLiveRun };
},
+5 -4
View File
@@ -1179,8 +1179,9 @@ export function environmentService(db: Db) {
where ${environmentLeases.environmentId} = ${environments.id}
and ${environmentLeases.status} = 'pending_cleanup'
)`,
// A reusable lease keeps a live provider sandbox after a run
// releases it. Deleting the environment would set its reference to
// A reusable lease keeps a provider sandbox after a run releases
// it, including when the run failed but release succeeded.
// Deleting the environment would set its reference to
// null, and both the normal release path and scoped reusable cleanup
// require that environment context. Refuse the delete atomically
// until the owning issue/workspace destroys the reusable sandbox.
@@ -1188,7 +1189,7 @@ export function environmentService(db: Db) {
select 1 from ${environmentLeases}
where ${environmentLeases.environmentId} = ${environments.id}
and ${environmentLeases.leasePolicy} = 'reuse_by_environment'
and ${environmentLeases.status} in ('active', 'released', 'retained')
and ${environmentLeases.status} in ('active', 'released', 'retained', 'failed')
)`,
),
)
@@ -1303,7 +1304,7 @@ export function environmentService(db: Db) {
and(
eq(environmentLeases.environmentId, id),
eq(environmentLeases.leasePolicy, "reuse_by_environment"),
inArray(environmentLeases.status, ["active", "released", "retained"]),
inArray(environmentLeases.status, ["active", "released", "retained", "failed"]),
),
),
db
+18 -14
View File
@@ -154,7 +154,7 @@ import {
configuredEnvironmentProjection,
} from "../vendor/paperclip-runner/index.js";
import { decisionModelService } from "./decision-models.js";
import { activeIssueInteractionCondition } from "./issue-question-context.js";
import { activeIssueInteractionCondition, TASK_QUESTION_GUIDANCE } from "./issue-question-context.js";
import { createAgentIdentityRedactor } from "./agent-identity-redaction.js";
import { agentIdentityService, supportsManagedAgentIdentity } from "./agent-identity.js";
import { buildAgentIdentityEnv } from "@paperclipai/adapter-utils/server-utils";
@@ -165,7 +165,7 @@ import { externalObjectService } from "./external-objects.js";
import { resolvePaperclipInstanceRoot } from "../home-paths.js";
import { dotRunnerBroker } from "./dot-runner-broker.js";
import { isAiAuthenticationBlocked } from "./ai-auth-failure.js";
import { nativeRetryCancellationCommitCondition, rethrowNativeCancellationLockConflict, claimCancellationRequest, startupCancellationFence } from "./native-runtime/native-cancellation-request.js";
import { nativeRetryCancellationCommitCondition, rethrowNativeCancellationLockConflict, claimCancellationRequest } from "./native-runtime/native-cancellation-request.js";
import { CHAT_COMPLETION_WAKE_REASON, prepareChatCompletionTurn, chatCompletionInstruction, isCompletedOnboardingHandoffWake } from "./chat-completion-delivery.js";
import { AgentDirectoryReuseInvalidatedError, isAgentDirectoryCopy } from "./agent-directory-working-copies.js";
@@ -261,7 +261,7 @@ import {
startAdapterExecutionTargetPaperclipBridge,
} from "@paperclipai/adapter-utils/execution-target";
import { agentService } from "./agents.js";
import { agentInstructionWorkingCopyService, instructionWorkingCopyGuidance } from "./agent-instruction-working-copies.js";
import { agentInstructionWorkingCopyService, collectStoppedInstructionCopyWithRetries, instructionWorkingCopyGuidance } from "./agent-instruction-working-copies.js";
import { normalizeLegacyRunnerProvider, resolveManagedOpenAiBilling } from "@paperclipai/adapter-utils";
import fs from "node:fs/promises";
import path from "node:path";
@@ -15961,6 +15961,15 @@ export function heartbeatService(
>;
try {
await controllerLease.assertOwned();
const remoteRecovery = runOptions.nativeRestartRecovery?.kind === "reattach_remote_runner"
? runOptions.nativeRestartRecovery : null;
const recoveryWorkspace = remoteRecovery
? readNativeWorkspaceSyncReference(parseObject(run.runnerProfileJson).nativeWorkspaceSync) : null;
if (remoteRecovery && (!recoveryWorkspace || remoteRecovery.runId !== run.id ||
recoveryWorkspace.providerLeaseId !== remoteRecovery.remote.providerLeaseId ||
recoveryWorkspace.remoteCwd !== remoteRecovery.remote.remoteCwd)) {
throw new Error("native_remote_recovery_lease_mismatch");
}
acquiredEnvironment = await envOrchestrator.acquireForRun({
companyId: agent.companyId,
selectedEnvironmentId,
@@ -15973,6 +15982,11 @@ export function heartbeatService(
agentId: agent.id,
persistedExecutionWorkspace,
executionWorkspaceSettings: environmentExecutionWorkspaceSettings,
...(remoteRecovery && recoveryWorkspace ? { reattachRemoteLease: {
leaseId: recoveryWorkspace.leaseId,
providerLeaseId: remoteRecovery.remote.providerLeaseId,
remoteCwd: remoteRecovery.remote.remoteCwd,
} } : {}),
});
await controllerLease.assertOwned();
nativeRunnerPreparationSpans.push({
@@ -18242,17 +18256,7 @@ export function heartbeatService(
},
onLog,
onEvent: onAdapterEvent,
instructionWorkingCopy: instructionCopy ? {
runId: run.id,
root: instructionCopy.executionRoot,
...(instructionCopy.receipt?.warm === true ? { checkpointWarm: async () => {
const saved = await instructionCopies.checkpointWarm({ companyId: agent.companyId, runId: run.id, target: executionTarget });
if (saved) await recordInstructionSave(saved);
return saved?.state === "warm_saved" && saved.errorCode === null;
} } : {}),
hasChanges: () => instructionCopies.hasChanges({ companyId: agent.companyId, runId: run.id, target: executionTarget }),
collectStopped: collectStoppedInstructions,
} : undefined,
instructionWorkingCopy: nativeInstructionWorkingCopy(),
onUsage: async receipt => { await usageRecorder.capture(receipt); },
preparationSpans: nativeRunnerPreparationSpans,
+9 -7
View File
@@ -89,18 +89,20 @@ describe("hot-restart path compatibility", () => {
).not.toBeNull();
});
it("reads Linux process start time from proc metadata", async () => {
it("reads Linux process birth from kernel ticks rather than proc directory metadata", async () => {
await expect(
readProcessStartedAt(123, {
platform: "linux",
stat: async (target) => {
expect(target).toBe("/proc/123");
return {
ctimeMs: Date.parse("2026-08-01T01:00:00.123Z"),
};
linuxProcessStart: {
clockTicksPerSecond: 100,
readFile: (target) => target === "/proc/stat"
? `btime ${Date.parse("2026-08-01T01:00:00.000Z") / 1000}\n`
: target === "/proc/123/stat"
? `123 (runner) S ${Array(18).fill("0").join(" ")} 123\n`
: (() => { throw new Error("Unexpected proc read"); })(),
},
}),
).resolves.toBe("2026-08-01T01:00:00.123Z");
).resolves.toBe("2026-08-01T01:00:01.230Z");
});
it("reads macOS process start time through ps", async () => {
+3 -5
View File
@@ -1,6 +1,7 @@
import { execFile } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import { readLinuxProcessStartedAt, type LinuxProcessStartOptions } from "../vendor/paperclip-runner/index.js";
import {
resolvePaperclipHomeDir,
resolvePaperclipInstanceId,
@@ -14,7 +15,6 @@ const HOT_RESTART_LOCK_STALE_MS = 30_000;
const HOT_RESTART_LOCK_TIMEOUT_MS = 10_000;
type ProcessCommandRunner = (command: string, args: string[]) => Promise<string>;
type ProcessStatReader = (target: string) => Promise<{ ctimeMs: number }>;
export type HotRestartIntentRun = {
runId: string;
@@ -178,17 +178,15 @@ export async function readProcessStartedAt(
pid: number,
options: {
platform?: NodeJS.Platform;
stat?: ProcessStatReader;
linuxProcessStart?: LinuxProcessStartOptions;
runCommand?: ProcessCommandRunner;
} = {},
) {
const platform = options.platform ?? process.platform;
const stat = options.stat ?? fs.stat;
const runCommand = options.runCommand ?? runProcessCommand;
if (platform === "linux") {
const processStat = await stat(`/proc/${pid}`);
return new Date(processStat.ctimeMs).toISOString();
return readLinuxProcessStartedAt(pid, options.linuxProcessStart);
}
if (["darwin", "freebsd", "openbsd", "aix", "sunos"].includes(platform)) {
@@ -1,3 +1,4 @@
import { normalizeEscapedLineBreaks } from "@paperclipai/shared/validators/text";
import { activeIssueInteractionCondition, historicalQuestionCondition } from "./issue-question-context.js";
import {
currentContinuationOrigins,
@@ -47,3 +47,7 @@ describe("native hire runtime inheritance", () => {
.not.toHaveProperty("managedProfileId");
});
});
it("preserves Pi thinking configuration without live identity or credentials", () => {
expect(inheritNativeRunnerAdapterConfig({ provider: "acpx", acpxAgent: "pi", piThinkingLevel: "low", runtimeSessionId: "prior", env: { OPENROUTER_API_KEY: "canary" } })).toEqual({ provider: "acpx", acpxAgent: "pi", piThinkingLevel: "low" });
});
@@ -14,6 +14,7 @@ export const INHERITABLE_NATIVE_RUNNER_CONFIG_KEYS = [
"opencodePermissionMode",
"acpxPermissionMode",
"acpxSessionMode",
"piThinkingLevel",
"lifecycleMode",
"modelReasoningEffort",
"maxIterations",
@@ -1,159 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import * as runner from "../../vendor/paperclip-runner/index.js";
import { resolveQualifiedAcpxProfile, validatePrpStructuredRunResult } from "../../vendor/paperclip-runner/index.js";
import { buildQuestionResponseDeliveryEnvelope } from "../question-response-delivery.js";
import { NATIVE_COMPLETION_CONTRACT_SCHEMA, NATIVE_COMPLETION_POLICY_VERSION } from "./completion-contracts.js";
import { nativeSha256 } from "./canonical.js";
import { nativeCursorPlanWaitFromFacts, type CursorPlanWaitFacts } from "./native-cursor-plan-wait.js";
function fixture(): CursorPlanWaitFacts {
const b = { companyId: "company", issueId: "issue", agentId: "agent", runId: "run" };
const contract = { revision: "revision", criteria: [{ id: "criterion" }] };
const contractMetadata = { schemaVersion: NATIVE_COMPLETION_CONTRACT_SCHEMA, policyVersion: NATIVE_COMPLETION_POLICY_VERSION, risk: "low", completionAuthority: "agent_claim_policy" };
const contractSha = nativeSha256({ ...contractMetadata, contract });
const planId = `plan-${"a".repeat(64)}`;
const input = { schema: "paperclip.question_set.v1", title: "Plan", description: "Exact revised plan text", questions: [{ id: planId, prompt: "Proceed?", required: true, answerMode: "single_select", options: [{ id: "accept", label: "Accept" }, { id: "reject", label: "Reject" }, { id: "cancel", label: "Cancel" }] }] };
const i = { id: "interaction", ...b, sourceRunId: b.runId, createdByAgentId: b.agentId, resolvedByUserId: "board", resolvedByAgentId: null, resolvedAt: new Date(0),
kind: "ask_user_questions", status: "answered", continuationPolicy: "none", idempotencyKey: "paperclip-runner-question:run:request",
payload: { runtimeRequestId: "request", questionSet: input }, result: { version: 1, answers: [{ questionId: planId, optionIds: ["accept"] }] } };
const envelope = buildQuestionResponseDeliveryEnvelope(i as never);
const d = { id: "delivery", ...b, interactionId: i.id, sourceRunId: b.runId, targetRunId: b.runId, targetTurnId: null, status: "delivered", deliveryMode: "steered", acknowledgedAt: new Date(1), payloadSha256: nativeSha256(envelope) };
const event = (sourceSeq: number, eventType: string, payload: Record<string, unknown>) => {
const e = { schema: "paperclip.prp.event.v1", runId: b.runId, normalizedSessionId: "session", turnId: "turn", sourceInstanceId: "instance", sourceEventId: `instance:${sourceSeq}`, sourceSeq, sourceKind: "runner", schemaVersion: 1, eventType, payload };
return { ...b, seq: sourceSeq, eventType, payload: { prpEvent: e }, sourceInstanceId: e.sourceInstanceId, sourceEventId: e.sourceEventId, sourceSeq, sourcePayloadSha256: nativeSha256(e), protocolSchemaVersion: 1 };
};
return {
binding: b,
run: { id: b.runId, companyId: b.companyId, agentId: b.agentId, nativeIssueId: b.issueId, runtimeMode: "native", runnerInstanceId: "instance", status: "running", completionContractId: "contract", completionContractSha256: contractSha, runnerProfileJson: { nativeExecutionInput: { binding: b, provider: { kind: "acpx", agent: "cursor", cursorMode: "plan", model: "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]", profile: resolveQualifiedAcpxProfile("cursor", "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]") }, session: { normalizedSessionId: "session" }, completionContract: { id: "contract", sha256: contractSha, contract } } } },
contract: { id: "contract", ...contractMetadata, canonicalSha256: contractSha, contractJson: contract },
events: [
event(275, "runtime_request.created", { request: { schema: "paperclip.runtime_request.v2", status: "pending", type: "input", requestKind: "runtime", requestId: "request", turnId: "turn", itemId: "native-plan-tool", origin: { provider: "cursor", method: "cursor/create_plan", adapter: "acpx-runtime-sidecar" }, input } }),
event(294, "tool.execution.started", { schema: "paperclip.tool.execution.v1", executionId: "native-plan-tool", transport: "builtin", operation: "execute", status: "running", name: "arbitrary display name" }),
event(300, "runtime_request.resolved", { requestId: "request", turnId: "turn", action: "submit", response: envelope.response }),
event(303, "tool.execution.completed", { schema: "paperclip.tool.execution.v1", executionId: "native-plan-tool", transport: "builtin", operation: "execute", status: "completed" }),
event(304, "turn.completed", { status: "completed", error: null }),
],
interactions: [{ interaction: i, delivery: d }],
} as unknown as CursorPlanWaitFacts;
}
function editEvent(f: CursorPlanWaitFacts, index: number, edit: (e: any) => void) {
const row = f.events.filter(row => !row.eventType.startsWith("tool.execution."))[index]!;
const e = (row.payload as any).prpEvent;
edit(e); row.sourcePayloadSha256 = nativeSha256(e);
}
describe("accepted Cursor plan passive-wait authority", () => {
it("records the accepted revision and explicitly unfinished Plan-mode continuation", () => {
const value = nativeCursorPlanWaitFromFacts(fixture());
expect(value?.source).toMatchObject({ requestId: "request", planRevision: `plan-${"a".repeat(64)}`, terminalEventId: "instance:304", toolExecutionId: "native-plan-tool" });
expect(value?.result).toMatchObject({ reportedWorkDisposition: "yielded", completionClaim: { objectiveSatisfied: false, remainingWork: [{ blocksCompletion: true }] }, continuation: { kind: "response_wake" } });
expect(value?.result.summary).toContain("next message");
const validated = validatePrpStructuredRunResult(value!.result);
expect(validated.ok).toBe(true);
if (validated.ok) expect(validated.result).toEqual(value!.result);
});
it.each(["unrelated same-title tool", "cross turn", "cross session", "duplicate start", "duplicate completion", "missing start", "missing completion", "failed completion", "changed request tool", "uncommitted tool row"])("rejects correlated lifecycle corruption: %s", kind => {
const f = fixture();
const start = f.events.find(row => row.eventType === "tool.execution.started")!;
const end = f.events.find(row => row.eventType === "tool.execution.completed")!;
const event = (start.payload as any).prpEvent;
if (kind === "unrelated same-title tool") event.payload.executionId = "unrelated-tool";
if (kind === "cross turn") event.turnId = "other";
if (kind === "cross session") event.normalizedSessionId = "other";
if (kind === "duplicate start") f.events.splice(2, 0, structuredClone(start));
if (kind === "duplicate completion") f.events.splice(4, 0, structuredClone(end));
if (kind === "missing start") f.events = f.events.filter(row => row !== start);
if (kind === "missing completion") f.events = f.events.filter(row => row !== end);
if (kind === "failed completion") { (end.payload as any).prpEvent.payload.status = "failed"; end.sourcePayloadSha256 = nativeSha256((end.payload as any).prpEvent); }
if (kind === "changed request tool") editEvent(f, 0, e => { e.payload.request.itemId = "unrelated-tool"; });
start.sourcePayloadSha256 = kind === "uncommitted tool row" ? null : nativeSha256(event);
expect(nativeCursorPlanWaitFromFacts(f)).toBeNull();
});
it("rejects an additional same-title tool while the real plan tool remains correlated", () => {
const f = fixture();
const extra = structuredClone(f.events.find(row => row.eventType === "tool.execution.started")!);
extra.seq = 295; extra.sourceSeq = 295; extra.sourceEventId = "instance:295";
const e = (extra.payload as any).prpEvent;
Object.assign(e, { sourceSeq: 295, sourceEventId: extra.sourceEventId });
Object.assign(e.payload, { executionId: "unrelated", name: "Create Plan" });
extra.sourcePayloadSha256 = nativeSha256(e); f.events.splice(2, 0, extra);
expect(nativeCursorPlanWaitFromFacts(f)).toBeNull();
});
it("binds actual callback-before-tool-start order without trusting display names", () => {
const f = fixture(); const original = nativeCursorPlanWaitFromFacts(f)!;
expect(original.source.toolLifecycleSha256).toMatch(/^[a-f0-9]{64}$/);
const start = f.events.find(row => row.eventType === "tool.execution.started")!;
(start.payload as any).prpEvent.payload.name = "changed display text";
start.sourcePayloadSha256 = nativeSha256((start.payload as any).prpEvent);
const changed = nativeCursorPlanWaitFromFacts(f)!;
expect(changed).not.toBeNull();
expect(changed.source.authoritySha256).not.toBe(original.source.authoritySha256);
expect(changed.source.toolLifecycleSha256).not.toBe(original.source.toolLifecycleSha256);
});
it("does not create a new wait from an earlier profile after the catalog advances", () => {
const facts = fixture();
expect(nativeCursorPlanWaitFromFacts(facts)).not.toBeNull();
const current = resolveQualifiedAcpxProfile("cursor", "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]");
// Model a future catalog revision without tying this test to today's version.
const next = { ...current, agentProfileVersion: (current.agentProfileVersion + 1) as typeof current.agentProfileVersion, commandDigest: `sha256:${"b".repeat(64)}` } satisfies typeof current;
expect(next.agentProfileVersion).toBeGreaterThan(current.agentProfileVersion);
const resolver = vi.spyOn(runner, "resolveQualifiedAcpxProfile").mockReturnValue(next);
try { expect(nativeCursorPlanWaitFromFacts(facts)).toBeNull(); }
finally { resolver.mockRestore(); }
});
it("projects only declared scope keys from a wider typed caller binding", () => {
const f = fixture(); Object.assign(f.binding, { wakeupRequestId: "unrelated-caller-metadata" });
const proof = nativeCursorPlanWaitFromFacts(f);
expect(proof).not.toBeNull();
expect(proof!.source).not.toHaveProperty("wakeupRequestId");
});
it.each([
["foreign runner", (f: CursorPlanWaitFacts) => { f.run.runnerInstanceId = "other"; }],
["changed admission binding", (f: CursorPlanWaitFacts) => { (f.run.runnerProfileJson as any).nativeExecutionInput.binding = { ...f.binding, runId: "other" }; }],
["wrong company", (f: CursorPlanWaitFacts) => { f.run.companyId = "other"; }],
["wrong task", (f: CursorPlanWaitFacts) => { f.run.nativeIssueId = "other"; }],
["failed run", (f: CursorPlanWaitFacts) => { f.run.status = "failed"; }],
["cancelled run", (f: CursorPlanWaitFacts) => { f.run.status = "cancelled"; }],
["Agent mode", (f: CursorPlanWaitFacts) => { (f.run.runnerProfileJson as any).nativeExecutionInput.provider.cursorMode = "agent"; }],
["stale profile", (f: CursorPlanWaitFacts) => { (f.run.runnerProfileJson as any).nativeExecutionInput.provider.profile = { ...resolveQualifiedAcpxProfile("cursor", "gpt-5.6-luna[context=272k,reasoning=medium,fast=false]"), commandDigest: "old" }; }],
["changed contract policy", (f: CursorPlanWaitFacts) => { f.contract.policyVersion = "tampered-policy"; }],
["changed completion authority", (f: CursorPlanWaitFacts) => { f.contract.completionAuthority = "server_arbiter"; }],
["changed contract hash", (f: CursorPlanWaitFacts) => { f.contract.canonicalSha256 = "f".repeat(64); }],
["changed contract", (f: CursorPlanWaitFacts) => { f.contract.contractJson.revision = "new"; }],
["unknown origin", (f: CursorPlanWaitFacts) => editEvent(f, 0, e => { e.payload.request.origin.provider = "acpx"; })],
["wrong method", (f: CursorPlanWaitFacts) => editEvent(f, 0, e => { e.payload.request.origin.method = "cursor/ask_question"; })],
["untrusted adapter", (f: CursorPlanWaitFacts) => editEvent(f, 0, e => { e.payload.request.origin.adapter = "other"; })],
["cross turn", (f: CursorPlanWaitFacts) => editEvent(f, 1, e => { e.turnId = "other"; })],
["cross session", (f: CursorPlanWaitFacts) => editEvent(f, 1, e => { e.normalizedSessionId = "other"; })],
["native error", (f: CursorPlanWaitFacts) => editEvent(f, 2, e => { e.payload.error = { message: "failure" }; })],
["changed plan", (f: CursorPlanWaitFacts) => editEvent(f, 0, e => { e.payload.request.input.description = "other"; })],
["rejection", (f: CursorPlanWaitFacts) => editEvent(f, 1, e => { e.payload.response.answers[`plan-${"a".repeat(64)}`].selectedOptionIds = ["reject"]; })],
["cancellation", (f: CursorPlanWaitFacts) => editEvent(f, 1, e => { e.payload.response.answers[`plan-${"a".repeat(64)}`].selectedOptionIds = ["cancel"]; })],
["missing delivery", (f: CursorPlanWaitFacts) => { f.interactions = []; }],
["unacknowledged delivery", (f: CursorPlanWaitFacts) => { f.interactions[0]!.delivery.acknowledgedAt = null; }],
["fallback wake", (f: CursorPlanWaitFacts) => { f.interactions[0]!.delivery.deliveryMode = "wake_fallback"; }],
["wrong target", (f: CursorPlanWaitFacts) => { f.interactions[0]!.delivery.targetRunId = "other"; }],
["changed answer digest", (f: CursorPlanWaitFacts) => { f.interactions[0]!.delivery.payloadSha256 = "other"; }],
["missing resolution time", (f: CursorPlanWaitFacts) => { f.interactions[0]!.interaction.resolvedAt = null; }],
["agent resolved", (f: CursorPlanWaitFacts) => { f.interactions[0]!.interaction.resolvedByAgentId = "agent"; }],
["duplicate receipt", (f: CursorPlanWaitFacts) => { f.events.splice(1, 0, structuredClone(f.events[0]!)); }],
["duplicate delivery", (f: CursorPlanWaitFacts) => { f.interactions.push(structuredClone(f.interactions[0]!)); }],
["uncommitted event", (f: CursorPlanWaitFacts) => { f.events[1]!.sourcePayloadSha256 = null; }],
["changed row identity", (f: CursorPlanWaitFacts) => { f.events[1]!.sourceEventId = "other"; }],
["terminal before answer", (f: CursorPlanWaitFacts) => { f.events.reverse(); }],
] as const)("rejects %s", (_name, mutate) => {
const f = fixture(); mutate(f); expect(nativeCursorPlanWaitFromFacts(f)).toBeNull();
});
it("does not reuse an older acceptance after a new request, later work, or conflicting terminal", () => {
for (const kind of ["runtime_request.created", "tool.execution.started", "turn.failed", "turn.cancelled", "turn.completed"]) {
const f = fixture(); const row = structuredClone(f.events[0]!);
row.seq = 2.5; row.sourceSeq = 3; row.sourceEventId = "instance:later"; row.eventType = kind;
const e = (row.payload as any).prpEvent; Object.assign(e, { sourceSeq: 3, sourceEventId: row.sourceEventId, eventType: kind });
if (kind === "runtime_request.created") e.payload.request.requestId = "new-plan";
row.sourcePayloadSha256 = nativeSha256(e);
editEvent(f, 2, e => { e.sourceSeq = 4; }); f.events[2]!.sourceSeq = 4;
f.events.splice(2, 0, row);
expect(nativeCursorPlanWaitFromFacts(f)).toBeNull();
}
});
});
@@ -1,243 +0,0 @@
import { and, asc, eq, gt, inArray, isNull, ne, sql } from "drizzle-orm";
import {
completionContracts, heartbeatRunEvents, heartbeatRuns, issues, issueComments,
nativeRunFinalizations, nativeRunResults, statusDecisions,
issueQuestionResponseDeliveries, issueThreadInteractions, type Db,
} from "@paperclipai/db";
import type { AskUserQuestionsInteraction } from "@paperclipai/shared";
import {
parsePaperclipQuestionResponse, parsePaperclipQuestionSet, resolveQualifiedAcpxProfile,
type PrpStructuredRunResult,
} from "../../vendor/paperclip-runner/index.js";
import { buildQuestionResponseDeliveryEnvelope } from "../question-response-delivery.js";
import { nativeCompletionContractSha256 } from "./completion-contracts.js";
import { nativeSha256 } from "./canonical.js";
type Binding = { companyId: string; issueId: string; runId: string; agentId: string };
const record = (v: unknown): Record<string, any> => v && typeof v === "object" && !Array.isArray(v) ? v as Record<string, any> : {};
const same = (a: unknown, b: unknown) => nativeSha256(a) === nativeSha256(b);
const PREFIX = "cursor-plan-wait:";
// Preserve the exact Cursor6 proof query: progress rows were never part of its
// 1,000-row budget. Completed tools were included and must stay included.
const LEGACY_EVENT_TYPES = ["runtime_request.created", "runtime_request.resolved", "runtime_request.cancelled", "runtime_request.expired", "turn.started", "turn.completed", "turn.failed", "turn.cancelled", "tool.execution.started", "tool.execution.completed"];
const EVENT_TYPES = [...LEGACY_EVENT_TYPES, "tool.execution.progressed"];
const SUMMARY = "Plan accepted. This task is waiting for your next message. This run used Plan mode; no implementation or task completion is claimed.";
export interface NativeCursorPlanWaitSource extends Binding {
schema: "paperclip.native_cursor_plan_wait.v1";
contractId: string;
contractSha256: string;
interactionId: string;
requestId: string;
planRevision: string;
turnId: string;
normalizedSessionId: string;
sourceInstanceId: string;
requestEventId: string;
resolvedEventId: string;
terminalEventId: string;
deliveryId: string;
authoritySha256: string;
toolExecutionId?: string;
toolLifecycleSha256?: string;
}
export interface CursorPlanWaitFacts {
binding: Binding;
run: Pick<typeof heartbeatRuns.$inferSelect, "id" | "companyId" | "agentId" | "nativeIssueId" | "runtimeMode" | "status" | "runnerProfileJson" | "runnerInstanceId" | "completionContractId" | "completionContractSha256">;
contract: Pick<typeof completionContracts.$inferSelect, "id" | "canonicalSha256" | "contractJson" | "schemaVersion" | "policyVersion" | "risk" | "completionAuthority">;
events: Array<Pick<typeof heartbeatRunEvents.$inferSelect, "companyId" | "agentId" | "runId" | "seq" | "eventType" | "payload" | "sourceInstanceId" | "sourceEventId" | "sourceSeq" | "sourcePayloadSha256" | "protocolSchemaVersion">>;
interactions: Array<{ interaction: typeof issueThreadInteractions.$inferSelect; delivery: typeof issueQuestionResponseDeliveries.$inferSelect }>;
}
function isHistoricalUnboundWait(source?: NativeCursorPlanWaitSource): boolean {
return source !== undefined && source.toolExecutionId === undefined && source.toolLifecycleSha256 === undefined;
}
/** Only committed native request/answer/normal-terminal facts can create this passive wait. */
export function nativeCursorPlanWaitFromFacts(facts: CursorPlanWaitFacts) {
return cursorPlanWaitFromFacts(facts);
}
function cursorPlanWaitFromFacts(facts: CursorPlanWaitFacts, committedSource?: NativeCursorPlanWaitSource): { source: NativeCursorPlanWaitSource; result: PrpStructuredRunResult } | null {
try {
const { run, contract } = facts;
const b: Binding = { companyId: facts.binding.companyId, issueId: facts.binding.issueId, runId: facts.binding.runId, agentId: facts.binding.agentId };
const admission = record(record(run.runnerProfileJson).nativeExecutionInput);
const provider = record(admission.provider), profile = record(provider.profile);
// First admission must match today's qualified runtime. Recovery may retain
// a previously committed wait only if the entire original proof is unchanged.
if (!committedSource) {
const expected = resolveQualifiedAcpxProfile("cursor", typeof provider.model === "string" ? provider.model : "");
if (!["agent", "driverKind", "protocolVersion", "acpxVersion", "commandDigest", "agentProfileVersion", "agentServerPackage", "agentServerVersion", "agentRuntimePackage", "agentRuntimeVersion"].every(key => profile[key] === record(expected)[key])) return null;
}
if (run.id !== b.runId || run.companyId !== b.companyId || run.agentId !== b.agentId || run.nativeIssueId !== b.issueId || run.runtimeMode !== "native" || !["running", "succeeded"].includes(run.status) ||
!Object.entries(b).every(([key, value]) => record(admission.binding)[key] === value) || provider.kind !== "acpx" || provider.agent !== "cursor" || provider.cursorMode !== "plan" || typeof provider.model !== "string" || !provider.model.trim() ||
record(admission.completionContract).id !== contract.id || record(admission.completionContract).sha256 !== contract.canonicalSha256 || nativeCompletionContractSha256(contract) !== contract.canonicalSha256 || run.completionContractId !== contract.id || run.completionContractSha256 !== contract.canonicalSha256 || !same(contract.contractJson, record(admission.completionContract).contract)) return null;
const sessionId = record(admission.session).normalizedSessionId;
if (typeof sessionId !== "string" || !sessionId || facts.events.length === 0 || facts.events.length > 1000) return null;
const events: Array<Record<string, any>> = [];
const ids = new Set<string>(), seqs = new Set<string>();
let lastRowSeq = -1;
const lastSourceSeq = new Map<string, number>();
for (const row of facts.events) {
const event = record(record(row.payload).prpEvent);
if (!event.schema) continue; // Non-PRP aggregate rows are not native evidence.
if (row.companyId !== b.companyId || row.runId !== b.runId || row.agentId !== b.agentId || row.seq <= lastRowSeq ||
event.schema !== "paperclip.prp.event.v1" || event.schemaVersion !== 1 || event.sourceInstanceId !== run.runnerInstanceId || event.sourceKind !== "runner" || event.runId !== b.runId || event.normalizedSessionId !== sessionId ||
row.eventType !== event.eventType || row.sourceEventId !== event.sourceEventId || row.sourceInstanceId !== event.sourceInstanceId || row.sourceSeq !== event.sourceSeq || row.protocolSchemaVersion !== event.schemaVersion ||
typeof event.sourceInstanceId !== "string" || !event.sourceInstanceId || typeof event.sourceEventId !== "string" || !event.sourceEventId || !Number.isSafeInteger(event.sourceSeq) || event.sourceSeq < 1 || row.sourcePayloadSha256 !== nativeSha256(event) ||
(lastSourceSeq.get(event.sourceInstanceId) ?? 0) >= event.sourceSeq || ids.has(event.sourceEventId) || seqs.has(`${event.sourceInstanceId}:${event.sourceSeq}`)) return null;
lastSourceSeq.set(event.sourceInstanceId, event.sourceSeq);
lastRowSeq = row.seq; ids.add(event.sourceEventId); seqs.add(`${event.sourceInstanceId}:${event.sourceSeq}`); events.push(event);
}
const terminal = events.at(-1);
if (!terminal || terminal.eventType !== "turn.completed" || record(terminal.payload).status !== "completed" || record(terminal.payload).error != null || typeof terminal.turnId !== "string") return null;
const turn = events.filter(e => e.turnId === terminal.turnId);
if (turn.some(e => e.sourceInstanceId !== terminal.sourceInstanceId) || turn.filter(e => ["turn.completed", "turn.failed", "turn.cancelled"].includes(e.eventType)).length !== 1) return null;
const requests = turn.filter(e => e.eventType === "runtime_request.created");
const created = requests.at(-1), request = record(record(created?.payload).request), origin = record(request.origin);
if (!created || request.schema !== "paperclip.runtime_request.v2" || request.type !== "input" || request.requestKind !== "runtime" || request.status !== "pending" || request.turnId !== terminal.turnId ||
origin.provider !== "cursor" || origin.method !== "cursor/create_plan" || !["acpx-runtime", "acpx-runtime-sidecar"].includes(origin.adapter) || typeof request.requestId !== "string") return null;
if (new Set(requests.map(e => record(record(e.payload).request).requestId)).size !== requests.length) return null;
// No unresolved earlier input or later work is disguised as an accepted plan boundary.
for (const start of requests) {
const id = record(record(start.payload).request).requestId;
const ends = turn.filter(e => ["runtime_request.resolved", "runtime_request.cancelled", "runtime_request.expired"].includes(e.eventType) && record(e.payload).requestId === id);
if (ends.length !== 1 || ends[0]!.sourceSeq <= start.sourceSeq || ends[0]!.eventType !== "runtime_request.resolved") return null;
}
const resolved = turn.find(e => e.eventType === "runtime_request.resolved" && record(e.payload).requestId === request.requestId)!;
const resolution = record(resolved.payload);
if (resolved.sourceSeq >= terminal.sourceSeq || resolution.action !== "submit" || resolution.turnId !== terminal.turnId) return null;
const historicalUnboundWait = isHistoricalUnboundWait(committedSource);
let toolBinding: { toolExecutionId: string; toolLifecycleSha256: string } | undefined;
if (historicalUnboundWait) {
if (profile.agentProfileVersion !== 6 || profile.commandDigest !== "sha256:377dcea64a727ce799cc112458d4b40ba4bc6574cd6c6f7233b6efd5917a6c4b") return null;
// Reconstruct only the previously committed contract; never create a new
// unbound wait or let a catalog upgrade authorize additional work.
if (turn.some(e => e.sourceSeq > created.sourceSeq && e.eventType === "tool.execution.started")) return null;
} else {
const id = request.itemId;
if (typeof id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/.test(id)) return null;
const tools = turn.filter(e => e.eventType.startsWith("tool.execution."));
const lifecycle = tools.filter(e => record(e.payload).executionId === id);
const starts = lifecycle.filter(e => e.eventType === "tool.execution.started");
const ends = lifecycle.filter(e => e.eventType === "tool.execution.completed");
if (starts.length !== 1 || ends.length !== 1 || starts[0]!.sourceSeq >= resolved.sourceSeq || ends[0]!.sourceSeq <= resolved.sourceSeq || ends[0]!.sourceSeq >= terminal.sourceSeq ||
lifecycle[0] !== starts[0] || lifecycle.at(-1) !== ends[0] ||
lifecycle.some(e => record(e.payload).schema !== "paperclip.tool.execution.v1" || record(e.payload).transport !== "builtin" || record(e.payload).operation !== "execute" || record(e.payload).status !== (e.eventType === "tool.execution.completed" ? "completed" : "running")) ||
tools.some(e => e.sourceSeq > created.sourceSeq && record(e.payload).executionId !== id)) return null;
toolBinding = { toolExecutionId: id, toolLifecycleSha256: nativeSha256(lifecycle) };
}
const questionSet = parsePaperclipQuestionSet(request.input);
const plan = questionSet.questions.filter(q => /^plan-[a-f0-9]{64}$/.test(q.id));
if (plan.length !== 1 || plan[0]!.answerMode !== "single_select" || !plan[0]!.required || !same(plan[0]!.options?.map(o => o.id), ["accept", "reject", "cancel"])) return null;
const response = parsePaperclipQuestionResponse(questionSet, resolution.response);
if (!same(response.answers[plan[0]!.id]?.selectedOptionIds, ["accept"])) return null;
const matches = facts.interactions.filter(({ interaction: i }) => record(i.payload).runtimeRequestId === request.requestId);
if (matches.length !== 1) return null;
const { interaction: i, delivery: d } = matches[0]!;
if (i.companyId !== b.companyId || i.issueId !== b.issueId || i.sourceRunId !== b.runId || i.createdByAgentId !== b.agentId || i.kind !== "ask_user_questions" || i.status !== "answered" || i.continuationPolicy !== "none" || !i.resolvedByUserId || !i.resolvedAt || i.resolvedByAgentId ||
i.idempotencyKey !== `paperclip-runner-question:${b.runId}:${request.requestId}` || !same(record(i.payload).questionSet, questionSet) ||
d.companyId !== b.companyId || d.issueId !== b.issueId || d.interactionId !== i.id || d.sourceRunId !== b.runId || d.targetRunId !== b.runId || d.status !== "delivered" || d.deliveryMode !== "steered" || !d.acknowledgedAt || (d.targetTurnId !== null && d.targetTurnId !== terminal.turnId)) return null;
const envelope = buildQuestionResponseDeliveryEnvelope(i as unknown as AskUserQuestionsInteraction);
if (d.payloadSha256 !== nativeSha256(envelope) || !same(parsePaperclipQuestionResponse(questionSet, envelope.response), response)) return null;
const source: NativeCursorPlanWaitSource = {
...b, schema: "paperclip.native_cursor_plan_wait.v1", contractId: contract.id, contractSha256: contract.canonicalSha256, interactionId: i.id, requestId: request.requestId, planRevision: plan[0]!.id,
turnId: terminal.turnId, normalizedSessionId: sessionId, sourceInstanceId: terminal.sourceInstanceId,
requestEventId: created.sourceEventId, resolvedEventId: resolved.sourceEventId, terminalEventId: terminal.sourceEventId, deliveryId: d.id,
...(toolBinding ?? {}),
authoritySha256: nativeSha256({ admission, contract, created, resolved, terminal, interaction: i, delivery: d, resolvedAt: i.resolvedAt?.toISOString(), acknowledgedAt: d.acknowledgedAt.toISOString(), ...(toolBinding ? { toolBinding } : {}) }),
};
if (committedSource && !same(source, committedSource)) return null;
const ref = `interaction:${i.id}`;
const result: PrpStructuredRunResult = {
schema: "paperclip.run_result.v1", reportedWorkDisposition: "yielded", summary: SUMMARY,
completionClaim: { contractRevision: String(contract.contractJson.revision), objectiveSatisfied: false,
criteria: (contract.contractJson.criteria as Array<{id: string}>).map(c => ({ criterionId: c.id, status: "unknown", evidenceRefs: [ref] })),
remainingWork: [{ description: "Continue or finalize the accepted plan only after explicit direction; remain in the selected mode.", blocksCompletion: true }] },
evidence: [{ ref }, { ref: `run-event:${resolved.sourceEventId}` }], verification: [], attentionRequests: [],
artifacts: [{ kind: "issue_thread_interaction", ref }],
continuation: { kind: "response_wake", summary: SUMMARY, idempotencyKey: `${PREFIX}${created.sourceEventId}` },
};
return { source, result };
} catch { return null; }
}
export function isNativeCursorPlanWaitResult(value: unknown): boolean {
const result = record(value);
return result.reportedWorkDisposition === "yielded" && typeof record(result.continuation).idempotencyKey === "string" && record(result.continuation).idempotencyKey.startsWith(PREFIX);
}
/** The committer calls this again under its issue lock; rows are share-locked then. */
export async function readNativeCursorPlanWait(db: Db, binding: Binding, locked = false) {
return readCursorPlanWaitProof(db, binding, locked);
}
// Historical profile validation is available only after the committed-receipt
// query below establishes its scoped authority. New wait callers cannot select it.
async function readCursorPlanWaitProof(db: Db, binding: Binding, locked: boolean, committedSource?: NativeCursorPlanWaitSource) {
const q = db.select({ run: heartbeatRuns, contract: completionContracts }).from(heartbeatRuns)
.innerJoin(completionContracts, and(eq(completionContracts.id, heartbeatRuns.completionContractId), eq(completionContracts.companyId, binding.companyId), eq(completionContracts.issueId, binding.issueId)))
.innerJoin(issues, and(eq(issues.id, binding.issueId), eq(issues.companyId, binding.companyId), eq(issues.assigneeAgentId, binding.agentId)))
.where(and(eq(heartbeatRuns.id, binding.runId), eq(heartbeatRuns.companyId, binding.companyId), eq(heartbeatRuns.agentId, binding.agentId), eq(heartbeatRuns.nativeIssueId, binding.issueId))).limit(1);
const [row] = await (locked ? q.for("share", { noWait: true }) : q);
if (!row || record(record(row.run.runnerProfileJson).nativeExecutionInput).provider?.agent !== "cursor" || record(record(row.run.runnerProfileJson).nativeExecutionInput).provider?.cursorMode !== "plan") return null;
const eventTypes = isHistoricalUnboundWait(committedSource) ? LEGACY_EVENT_TYPES : EVENT_TYPES;
const eqs = db.select().from(heartbeatRunEvents).where(and(eq(heartbeatRunEvents.companyId, binding.companyId), eq(heartbeatRunEvents.runId, binding.runId), inArray(heartbeatRunEvents.eventType, eventTypes))).orderBy(asc(heartbeatRunEvents.seq)).limit(1001);
const events = await (locked ? eqs.for("share", { noWait: true }) : eqs);
const iq = db.select({ interaction: issueThreadInteractions, delivery: issueQuestionResponseDeliveries }).from(issueThreadInteractions)
.innerJoin(issueQuestionResponseDeliveries, eq(issueQuestionResponseDeliveries.interactionId, issueThreadInteractions.id))
.where(and(eq(issueThreadInteractions.companyId, binding.companyId), eq(issueThreadInteractions.issueId, binding.issueId), eq(issueThreadInteractions.sourceRunId, binding.runId))).limit(101);
const interactions = await (locked ? iq.for("share", { noWait: true }) : iq);
if (interactions.length > 100) return null;
return cursorPlanWaitFromFacts({ binding, ...row, events, interactions }, committedSource);
}
/** An exact applied wait suppresses recovery, not a later independently admitted user wake. */
export async function hasCommittedNativeCursorPlanWait(db: Db, binding: Binding): Promise<boolean> {
const [receipt] = await db.select({ decision: statusDecisions, result: nativeRunResults })
.from(nativeRunFinalizations)
.innerJoin(statusDecisions, and(
eq(statusDecisions.id, nativeRunFinalizations.decisionId),
eq(statusDecisions.assessmentId, nativeRunFinalizations.assessmentId),
eq(statusDecisions.companyId, binding.companyId), eq(statusDecisions.issueId, binding.issueId),
eq(statusDecisions.runId, binding.runId), eq(statusDecisions.applicationState, "applied"),
eq(statusDecisions.toStatus, "in_progress"), eq(statusDecisions.reasonCode, "native_plan_accepted_waiting_for_continuation"),
))
.innerJoin(nativeRunResults, and(
eq(nativeRunResults.id, nativeRunFinalizations.resultId), eq(nativeRunResults.companyId, binding.companyId),
eq(nativeRunResults.issueId, binding.issueId), eq(nativeRunResults.runId, binding.runId), eq(nativeRunResults.schemaStatus, "accepted"),
))
.innerJoin(issues, and(
eq(issues.id, binding.issueId), eq(issues.companyId, binding.companyId), eq(issues.assigneeAgentId, binding.agentId),
eq(issues.status, "in_progress"), eq(issues.lastStatusDecisionId, statusDecisions.id), isNull(issues.hiddenAt),
sql`coalesce(${issues.executionState}->>'status', '') <> 'pending'`,
))
.where(and(eq(nativeRunFinalizations.companyId, binding.companyId), eq(nativeRunFinalizations.issueId, binding.issueId),
eq(nativeRunFinalizations.runId, binding.runId), eq(nativeRunFinalizations.phase, "committed"))).limit(1);
if (!receipt) return false;
const committedSource = record(receipt.decision.decisionJson).cursorPlanWait as NativeCursorPlanWaitSource | undefined;
if (!committedSource) return false;
const proof = await readCursorPlanWaitProof(db, binding, false, committedSource);
const envelope = record(receipt.result.resultJson), terminal = record(envelope.terminal);
const acceptedIdentity = record(record(receipt.decision.decisionJson).cursorPlanWaitResult);
if (!proof || receipt.result.completionContractId !== proof.source.contractId || receipt.result.turnId !== proof.source.turnId || acceptedIdentity.resultId !== receipt.result.id || acceptedIdentity.resultSha256 !== receipt.result.canonicalSha256 || !same(record(receipt.decision.decisionJson).cursorPlanWait, proof.source) || !same(envelope.result, proof.result) ||
terminal.schema !== "paperclip.prp.terminal.v1" || terminal.runTerminalState !== "succeeded" || terminal.turnTerminalState !== "completed" || terminal.reportedWorkDisposition !== "yielded") return false;
const [run] = await db.select().from(heartbeatRuns).where(and(eq(heartbeatRuns.id, binding.runId), eq(heartbeatRuns.companyId, binding.companyId))).limit(1);
if (!run || run.status !== "succeeded") return false;
// Settings for future runs cannot authorize work on this accepted plan.
// The original admission is checked above; only task-specific continuation
// or a superseding decision/run can release this committed passive wait.
const [interaction] = await db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, proof.source.interactionId)).limit(1);
if (!interaction?.resolvedAt) return false;
const [newRequest, newerRun] = await Promise.all([
db.select({ id: issueComments.id }).from(issueComments).where(and(
eq(issueComments.companyId, binding.companyId), eq(issueComments.issueId, binding.issueId),
eq(issueComments.authorType, "user"), gt(issueComments.createdAt, interaction.resolvedAt),
)).limit(1),
db.select({ id: heartbeatRuns.id }).from(heartbeatRuns).where(and(
eq(heartbeatRuns.companyId, binding.companyId), eq(heartbeatRuns.nativeIssueId, binding.issueId), ne(heartbeatRuns.id, binding.runId), gt(heartbeatRuns.createdAt, run.createdAt),
)).limit(1),
]);
return newRequest.length === 0 && newerRun.length === 0;
}
@@ -12,6 +12,23 @@ describe("explicit file output requirements", () => {
"Make a file but do not send it to anyone else.",
"Export a summary of this PDF as CSV.",
"Create no temporary files; export the results as CSV.",
"Write report.pdf. This is an internal verification file, not a deliverable. Also export the results as CSV.",
"Write a downloadable report.pdf. This is personal memory, not a task deliverable.",
"Attempt native write to report.txt with content requested.",
"Write report.pdf and checklist.md. This is an internal assertion file, not a deliverable.",
"Write report.pdf; write internal-proof.txt. This is an internal verification file, not a deliverable.",
"Write report.pdf and attempt native write to /outside/probe.txt; this negative test must be denied.",
"Attempt native write to /outside/probe.json and write report.pdf; this negative test must be denied.",
"Write report.txt and attach it. This is an internal verification file, not a deliverable.",
"Attach it.",
"Write report.txt and return it as an attachment. This is an internal verification file, not a deliverable.",
"Write report.txt and send it to me. This is an internal verification file, not a deliverable.",
"Write report.txt. This is an internal verification file, not a deliverable. Send it to me.",
"Write report.txt and provide it to me. This is an internal verification file, not a deliverable.",
"Write report.txt and give me it. This is an internal verification file, not a deliverable.",
"Write report.txt and return it. This is an internal verification file, not a deliverable.",
"Write report.txt and send it as an attachment in your response. This is an internal verification file, not a deliverable.",
"Write report.txt. This is an internal verification file, not a deliverable. Send it as a download link in your response.",
])("recognizes an explicit output request: %s", objective => {
expect(explicitlyRequestsFileOutput(objective)).toBe(true);
});
@@ -31,6 +48,20 @@ describe("explicit file output requirements", () => {
"Create no files.",
"Generate no attachments and answer in chat.",
"Write a reply without any files.",
"Use native write/read file tools for this task, not bash or the instructions API.",
"Write a memory entry to memory/pi-native.txt inside the registered AGENT_HOME. This is personal memory, not a task deliverable.",
"Use native write to copy those exact bytes into pi-agent-memory-proof.txt in the task workspace. This is an internal assertion file, not a deliverable.",
"Before finishing, attempt native write exactly once to /outside/pi-unassigned.txt with content forbidden. This intentionally unassigned root must be denied.",
"Write internal-proof.txt; then check it exists. This is an internal verification file, not a deliverable.",
"Attempt native write to /outside/probe.txt and create no files. This negative test must be denied.",
"Write internal-proof.txt; do not attach it. This is an internal verification file, not a deliverable.",
"Write a reply and return it in chat.",
"Write internal-proof.txt and return it in chat. This is an internal verification file, not a deliverable.",
"Write internal-proof.txt and send it in chat. This is an internal verification file, not a deliverable.",
"Write internal-proof.txt. This is an internal verification file, not a deliverable. Return it inline.",
"Write internal-proof.txt and send it as a code block in your response. This is an internal verification file, not a deliverable.",
"Write internal-proof.txt and return it in my reply. This is an internal verification file, not a deliverable.",
"Write internal-proof.txt and provide it as plain text. This is an internal verification file, not a deliverable.",
])("does not require a file for a text or source-review request: %s", objective => {
expect(explicitlyRequestsFileOutput(objective)).toBe(false);
});
@@ -63,26 +63,59 @@ async function hasCurrentPublicationReceipt(db: Db, companyId: string, receipts:
* an output requirement or erase a user's request for a file.
*/
export function explicitlyRequestsFileOutput(objective: string): boolean {
return objective.split(/(?:[.!?](?:\s|$)|\n|[;,]|\bbut\b)/iu).some(clause => {
const sentences = objective.replaceAll("\\_", "_").split(/(?:[.!?](?:\s|$)|\n)/iu);
let precedingFileOutput = false;
return sentences.some((sentence, index) => {
const file = /\b(?:files?|attachments?|downloads?|pdf|spreadsheets?|workbooks?|slide decks?|powerpoints?|docx|xlsx|csv)\b|\b[^\s/]+\.(?:md|txt|pdf|docx?|xlsx?|csv|pptx?|png|jpe?g|svg|zip)\b/giu;
const create = /\b(?:create|make|write|save|export|attach|send|generate|produce|prepare|provide|give|return|build)\b/iu.exec(clause);
if (create && /\b(?:do not|don't|never|no need to)\s*$/iu.test(clause.slice(0, create.index))) return false;
const output = create ? clause.slice(create.index + create[0].length) : "";
const fileObject = [...output.matchAll(file)].some(match => {
const prefix = output.slice(0, match.index);
const suffix = output.slice(match.index + match[0].length);
// "Create no files" is a prohibition, even though it contains a creation
// verb. Negate this object only; another explicit output can still count.
if (/\b(?:no|zero|without(?:\s+any)?)\s+(?:(?:new|temporary|downloadable|attached|additional)\s+)*$/iu.test(prefix)) return false;
// "Write a summary of this PDF" names input, not a requested file.
// Explicit export destinations still count after such input references.
const destination = /\b(?:as|into|to)\s+(?:(?:a|an|the|new|separate|markdown|word|excel)\s+)*$/iu.test(prefix);
if (!destination && /\b(?:of|about|on|from|using|for|with)\b/iu.test(prefix)) return false;
if (/^files?$/iu.test(match[0]) && /^\s+(?:permissions?|systems?|formats?|names?|paths?|types?|sizes?|descriptors?)\b/iu.test(suffix)) return false;
return true;
const outputs = sentence.split(/(?:[;,]|\bbut\b)/iu).flatMap(clause => {
const creates = [...clause.matchAll(/\b(?:create|make|write|save|export|attach|send|generate|produce|prepare|provide|give|return|build)\b/giu)];
return creates.flatMap((create, createIndex) => {
const before = clause.slice(0, create.index);
if (/\b(?:do not|don't|never|no need to)\s*$/iu.test(before)) return [];
// Bind each object to its own verb. A denied write or "create no files"
// cannot suppress a separate requested report in this same clause.
const output = clause.slice(create.index + create[0].length, creates[createIndex + 1]?.index);
const objects = [...output.matchAll(file)].filter(match => {
const prefix = output.slice(0, match.index);
const suffix = output.slice(match.index + match[0].length);
// "Create no files" is a prohibition, even though it contains a creation
// verb. Negate this object only; another explicit output can still count.
if (/\b(?:no|zero|without(?:\s+any)?)\s+(?:(?:new|temporary|downloadable|attached|additional)\s+)*$/iu.test(prefix)) return false;
// "Write a summary of this PDF" names input, not a requested file.
// Explicit export destinations still count after such input references.
const destination = /\b(?:as|into|to)\s+(?:(?:a|an|the|new|separate|markdown|word|excel)\s+)*$/iu.test(prefix);
if (!destination && /\b(?:of|about|on|from|using|for|with)\b/iu.test(prefix)) return false;
if (/^files?$/iu.test(match[0]) && /^\s+(?:tools?|permissions?|systems?|formats?|names?|paths?|types?|sizes?|descriptors?)\b/iu.test(suffix)) return false;
return true;
});
// An explicit attachment/export request can refer to the file by
// pronoun. It still requires publication when its name is omitted.
const referencesOutput = /^(?:attach|export|send|provide|give|return)$/iu.test(create[0])
&& /^\s+(?:me\s+)?(?:it|them|this|that)\b/iu.test(output);
const referencedAttachment = referencesOutput && /^(?:attach|export)$/iu.test(create[0]);
const inline = /\b(?:inline|(?:in|within|inside|as|into)\s+(?:(?:a|an|the|my|your|our|final|plain|markdown|chat|fenced)\s+)*(?:chat|response|reply|message|comment|text|code block)|(?:its|the) contents)\b/iu.test(output);
const downloadable = referencedAttachment || (!/\b(?:no|without)\s+(?:downloadable|attached)/iu.test(output)
&& /\b(?:downloadable|attached)\s+(?:file|report|document|checklist|draft)\b/iu.test(output));
const publication = /\b(?:downloadable|attached|attach|export|send|provide|return|give)\b/iu.test(create[0] + output);
const deniedAttempt = create[0].toLowerCase() === "write" && objects.length === 1
&& /\b(?:attempt|try)\s+(?:the\s+)?native\s*$/iu.test(before)
&& /\b(?:must be denied|denial is (?:the )?expected|(?:this|the) negative test)\b/iu.test(objective);
if (objects.length === 0 && !downloadable && /^\s+(?:no|zero|without)\b/iu.test(output)) return [];
return [{ objects: objects.length, downloadable, publication, deniedAttempt,
referencesOutput, publicationReference: referencesOutput && !inline }];
});
});
// "This ..." qualifies a single requested file in the preceding sentence,
// even when a later clause checks it. Multiple files cannot share this
// exception and separate report requests still require publication.
const internal = outputs.reduce((count, output) => count + output.objects + Number(output.downloadable && output.objects === 0), 0) === 1
&& /^\s*This is (?:an? )?(?:personal memory|internal (?:assertion|verification) file)\b[^.!?]*\bnot a (?:task )?deliverable\b/iu.test(sentences[index + 1] ?? "");
return outputs.some(output => {
const publicationReference = output.publicationReference && precedingFileOutput;
if (!output.referencesOutput) precedingFileOutput = output.objects > 0;
return (output.objects > 0 || output.downloadable || publicationReference)
&& (output.publication || (!internal && !output.deniedAttempt));
});
return fileObject ||
(!/\b(?:no|without)\s+(?:downloadable|attached)/iu.test(clause) && /\b(?:downloadable|attached)\s+(?:file|report|document|checklist|draft)\b/iu.test(clause));
});
}
@@ -602,7 +602,7 @@ describe("Cursor mode native execution projection", () => {
}
it.each([undefined, "agent", "plan", "ask"] as const)("preserves mode %s independently of permissions and task planning", mode => {
const result = buildNativeExecutionInput(fixture(mode));
expect(result.provider).toMatchObject({ kind: "acpx", agent: "cursor", cursorMode: mode ?? "agent", permissionMode: "deny-all" });
expect(result.provider).toMatchObject({ kind: "acpx", agent: "cursor", mode: mode ?? "agent", permissionMode: "deny-all" });
expect(result.executionMode).toBe("default");
expect(result.task.workMode).toBe("standard");
});
@@ -610,3 +610,27 @@ describe("Cursor mode native execution projection", () => {
expect(() => buildNativeExecutionInput({ ...fixture("plan"), acpxAgent: "copilot" })).toThrow("only for Cursor");
});
});
describe("Pi thinking native execution projection", () => {
function fixture(piThinkingLevel?: "off" | "low" | "high" | "max") {
return {
companyId: "company-1", runId: "run-1", agentId: "agent-1",
issue: { id: "issue-1", identifier: "MODE-1", title: "Review a plan", description: null, workMode: "standard" },
taskPrompt: "Review the project.",
workspace: { id: "workspace-1", cwd: "/workspace", repoUrl: null, repoRef: null, branchName: null },
normalizedSessionId: null, provider: "acpx" as const, acpxAgent: "pi" as const,
model: "openrouter/deepseek/deepseek-v4-flash-0731", piThinkingLevel, acpxPermissionMode: "deny-all" as const,
completionContract: { id: "contract-1", sha256: `sha256:${"a".repeat(64)}`, schemaVersion: "paperclip.run-result.v1", contract: { revision: "1", objective: "Review", criteria: [{ id: "output", requirement: "Review" }] } },
runtimeContext: nativeRuntimeContextFixture(),
};
}
it.each([undefined, "off", "low", "high", "max"] as const)("preserves mode %s independently of permissions and task planning", mode => {
const result = buildNativeExecutionInput(fixture(mode));
expect(result.provider).toMatchObject({ kind: "acpx", agent: "pi", piThinkingLevel: mode ?? "low", permissionMode: "deny-all" });
expect(result.executionMode).toBe("default");
expect(result.task.workMode).toBe("standard");
});
it("rejects a Pi thinking level attached to another harness", () => {
expect(() => buildNativeExecutionInput({ ...fixture("low"), acpxAgent: "copilot" })).toThrow("only for Pi");
});
});
@@ -1,5 +1,5 @@
import type { PaperclipTurnContext } from "@paperclipai/adapter-utils/server-utils";
import { resolvePaperclipRunnerCursorMode } from "@paperclipai/adapter-utils";
import { resolvePaperclipRunnerCursorMode, resolvePaperclipRunnerPiThinkingLevel } from "@paperclipai/adapter-utils";
import { createHash } from "node:crypto";
import { buildNativeContinuationPrompt } from "./native-continuation.js";
import type {
@@ -77,6 +77,7 @@ export interface BuildNativeExecutionInput {
opencodePermissionMode?: NativeOpenCodePermissionMode;
acpxPermissionMode?: NativeAcpxPermissionMode;
acpxSessionMode?: "agent" | "plan" | "ask";
piThinkingLevel?: "off" | "low" | "high" | "max";
model?: string | null;
managedProfile?: Extract<
NativeExecutionInputV5["provider"],
@@ -113,6 +114,7 @@ export function buildNativeExecutionInput(input: BuildNativeExecutionInput): Nat
throw new Error("native_execution_input_invalid: issue work mode must be standard, planning, or ask");
}
const mode = resolvePaperclipRunnerCursorMode(input.provider, input.acpxAgent, input.acpxSessionMode);
const piThinkingLevel = resolvePaperclipRunnerPiThinkingLevel(input.provider, input.acpxAgent, input.piThinkingLevel);
const executionMode = input.executionMode
?? (input.issue.workMode === "planning" ? "plan" : "default");
const acpxProfile = input.provider === "acpx"
@@ -276,6 +278,7 @@ export function buildNativeExecutionInput(input: BuildNativeExecutionInput): Nat
model: input.model,
permissionMode: input.acpxPermissionMode ?? "approve-all",
...(mode === undefined ? {} : { mode }),
...(piThinkingLevel === undefined ? {} : { piThinkingLevel }),
profile: {
driverKind: acpxProfile!.driverKind,
protocolVersion: acpxProfile!.protocolVersion,
@@ -1,8 +1,8 @@
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
import path from "node:path";
import {
cleanupGitHubOperationLaunchers,
githubOperationLauncherDirectory,
prepareGitHubOperationLaunchers,
startAdapterExecutionTargetPaperclipBridge,
} from "@paperclipai/adapter-utils/execution-target";
@@ -98,7 +98,7 @@ export async function createNativeGitHubAccess(input: {
bridge = await startBridge({
...location,
runtimeRootDir: input.target?.kind === "remote"
? path.posix.join(input.target.remoteCwd, ".paperclip-runtime", "github", location.runId)
? githubOperationLauncherDirectory(location)
: null,
adapterKey: "native-github",
hostApiToken: token,
@@ -332,6 +332,42 @@ describe("native runner file handoff", () => {
}
});
it.each([
"Use native write/read file tools. Write memory/pi-native.txt inside AGENT_HOME. This is personal memory, not a task deliverable.",
"Use native write to copy bytes into pi-agent-memory-proof.txt. This is an internal assertion file, not a deliverable.",
"Attempt native write once to /outside/pi-unassigned.txt. This intentionally unassigned root must be denied.",
"Write internal-proof.txt; then check it exists. This is an internal verification file, not a deliverable.",
"Attempt native write to /outside/probe.txt and create no files. This negative test must be denied.",
"Write internal-proof.txt and return it in chat. This is an internal verification file, not a deliverable.",
"Write internal-proof.txt and send it as a code block in your response. This is an internal verification file, not a deliverable.",
])("accepts an internal file outcome without treating it as published output: %s", async objective => {
await db.update(heartbeatRuns).set({ contextSnapshot: { issueId, executionContinuation: { objective } } })
.where(eq(heartbeatRuns.id, runId));
try {
await expect(nativeCompletionFeedback(db, runId, doneReport([])))
.resolves.toContain("Completion report accepted");
} finally {
await db.update(heartbeatRuns).set({ contextSnapshot: { issueId } }).where(eq(heartbeatRuns.id, runId));
}
});
it.each([
"Write report.pdf; write internal-proof.txt. This is an internal verification file, not a deliverable.",
"Write report.pdf and attempt native write to /outside/probe.txt; this negative test must be denied.",
"Write report.txt and attach it. This is an internal verification file, not a deliverable.",
"Write report.txt and send it to me. This is an internal verification file, not a deliverable.",
"Write report.txt. This is an internal verification file, not a deliverable. Send it as a download link in your response.",
])("still requires publication when the task also asks for internal or denied writes: %s", async objective => {
await db.update(heartbeatRuns).set({ contextSnapshot: { issueId, executionContinuation: { objective } } })
.where(eq(heartbeatRuns.id, runId));
try {
await expect(nativeCompletionFeedback(db, runId, doneReport([])))
.rejects.toThrow("requested file has no accessible delivery evidence");
} finally {
await db.update(heartbeatRuns).set({ contextSnapshot: { issueId } }).where(eq(heartbeatRuns.id, runId));
}
});
it("prepares one verified same-run attachment and replays without duplicates", async () => {
const body = Buffer.from("native runner file handoff\n", "utf8");
await mkdir(path.join(workspaceRoot, "out"), { recursive: true });
@@ -1268,12 +1268,23 @@ describe("remote provider pack manifest", () => {
Object.assign(payload, { candidateProviders: candidates });
await writeManifest();
expect(readRemoteProviderPackManifest(root).payload.candidateProviders?.pi?.qualification).toBe("qualified");
// The normal pack builder publishes Pi in both inventories. A Pi image
// must pass the same reader used by real remote runtime preparation.
Object.assign(payload, { providers: { cursor, pi: candidates.pi } });
await writeManifest();
expect(readRemoteProviderPackManifest(root).payload.providers?.pi?.qualification).toBe("qualified");
await mkdir(releaseMetadata, { recursive: true });
await cp(join(root, "provider-pack.json"), manifestPath);
expect(readBundledRemoteProviderPackManifest(manifestPath).payload.providers?.pi?.qualification).toBe("qualified");
Object.assign(payload, { providers: { cursor } });
await rm(releaseMetadata, { recursive: true, force: true });
await writeManifest();
await writeFile(join(root, candidatePath, "runtime"), "substitute runtime");
expect(() => readRemoteProviderPackManifest(root)).toThrow("candidate asset tree digest mismatch");
await writeFile(join(root, candidatePath, "runtime"), "pinned runtime");
candidates.pi.qualification = "pending";
await writeManifest();
expect(readRemoteProviderPackManifest(root).payload.candidateProviders?.pi?.qualification).toBe("pending");
expect(() => readRemoteProviderPackManifest(root)).toThrow("invalid candidate identity");
candidates.pi.qualification = "qualified";
for (const invalid of [{ path: "../outside" }, { qualification: "unknown" }]) {
const original = { ...candidates.pi };
@@ -1285,7 +1296,7 @@ describe("remote provider pack manifest", () => {
for (const provider of ["cursor", "copilot"]) {
Object.assign(candidates, { [provider]: { ...candidates.pi, path: `provider-assets/${provider}/linux-x64` } });
await writeManifest();
expect(() => readRemoteProviderPackManifest(root)).toThrow("invalid candidate identity");
expect(() => readRemoteProviderPackManifest(root)).toThrow(provider === "cursor" ? "Cursor profile or closure does not match this release" : "invalid candidate identity");
delete (candidates as Record<string, unknown>)[provider];
}
await writeManifest();
@@ -1560,12 +1571,12 @@ describe("verified native harness backups", () => {
it("binds Cursor mode across governed and identical recovery identities", () => {
const execution = {
...backupExecution,
provider: { kind: "acpx", agent: "cursor", model: "explicit-model", cursorMode: "plan" },
provider: { kind: "acpx", agent: "cursor", model: "explicit-model", mode: "plan" },
interactionResponses: [{ interactionId: "interaction-1" }],
} as unknown as NativeExecutionInputV1;
const identity = (suffix: string, cursorMode: unknown) => {
const identity = (suffix: string, mode: unknown) => {
const value = acpxIdentity(suffix);
return { ...value, providerSessionIdentity: { ...value.providerSessionIdentity, cursorMode } };
return { ...value, providerSessionIdentity: { ...value.providerSessionIdentity, mode } };
};
const previous = identity("previous", "plan");
const current = identity("current", "plan");
@@ -1580,7 +1591,37 @@ describe("verified native harness backups", () => {
for (const provider of [
{ kind: "acpx", agent: "cursor", model: "explicit-model" },
{ kind: "acpx", agent: "copilot", model: "explicit-model" },
{ kind: "acpx", agent: "copilot", model: "explicit-model", cursorMode: "plan" },
]) {
expect(providerSessionIdentityTransitionIsAllowed({
execution: { ...execution, provider } as unknown as NativeExecutionInputV1, previous, current,
})).toBe(false);
}
});
it("binds Pi thinking across governed and identical recovery identities", () => {
const execution = {
...backupExecution,
provider: { kind: "acpx", agent: "pi", model: "explicit-model", piThinkingLevel: "low" },
interactionResponses: [{ interactionId: "interaction-1" }],
} as unknown as NativeExecutionInputV1;
const identity = (suffix: string, piThinkingLevel: unknown) => {
const value = acpxIdentity(suffix);
return { ...value, providerSessionIdentity: { ...value.providerSessionIdentity, piThinkingLevel } };
};
const previous = identity("previous", "low");
const current = identity("current", "low");
expect(providerSessionIdentityTransitionIsAllowed({ execution, previous, current })).toBe(true);
expect(providerSessionIdentityTransitionIsAllowed({ execution, previous, current: previous })).toBe(true);
for (const mode of [undefined, null, "high", "max", "medium"]) {
const wrong = identity("wrong", mode);
expect(providerSessionIdentityTransitionIsAllowed({ execution, previous, current: wrong })).toBe(false);
expect(providerSessionIdentityTransitionIsAllowed({ execution, previous: wrong, current })).toBe(false);
expect(providerSessionIdentityTransitionIsAllowed({ execution, previous: wrong, current: wrong })).toBe(false);
}
for (const provider of [
{ kind: "acpx", agent: "pi", model: "explicit-model" },
{ kind: "acpx", agent: "copilot", model: "explicit-model" },
{ kind: "acpx", agent: "copilot", model: "explicit-model", piThinkingLevel: "low" },
]) {
expect(providerSessionIdentityTransitionIsAllowed({
execution: { ...execution, provider } as unknown as NativeExecutionInputV1, previous, current,
@@ -1952,24 +1993,23 @@ describe("split durable provider checkpoint identity", () => {
it("requires the exact observed Cursor mode in suspended descriptor and identity", () => {
const profileDigest = `sha256:${"a".repeat(64)}`;
const input = execution({ kind: "acpx", agent: "cursor", model: "explicit-model", permissionMode: "deny-all", cursorMode: "plan" }, "acpx_runtime");
const input = execution({ kind: "acpx", agent: "cursor", model: "explicit-model", permissionMode: "deny-all", mode: "plan" }, "acpx_runtime");
const providerState = {
schema: "paperclip.runner.acpx-provider-state.v3", lifecycle: "suspended", activeTurnId: null, providerExitUnconfirmed: false,
descriptor: { kind: "acpx", provider: "acpx", driver: "acpx_runtime", agent: "cursor", model: "explicit-model", commandDigest: profileDigest, normalizedSessionId: "native-session", cursorMode: "plan" },
descriptor: { kind: "acpx", provider: "acpx", driver: "acpx_runtime", agent: "cursor", model: "explicit-model", commandDigest: profileDigest, normalizedSessionId: "native-session", mode: "plan" },
identity: { kind: "acpx", normalizedSessionId: "native-session", acpxRecordId: "record-1", backendSessionId: "backend-1", agentSessionId: "agent-1", profileDigest,
workspaceDigest: `sha256:${"b".repeat(64)}`, requestedModel: "explicit-model", effectiveModel: "explicit-model", permissionMode: "deny-all", cursorMode: "plan", providerLifetimeFenceCandidates: [53001, 53002, 53003] },
workspaceDigest: `sha256:${"b".repeat(64)}`, requestedModel: "explicit-model", effectiveModel: "explicit-model", permissionMode: "deny-all", mode: "plan", providerLifetimeFenceCandidates: [53001, 53002, 53003] },
};
const read = (state: unknown, candidate = input) => providerSessionIdentityFromDurableProviderState({ execution: candidate, providerState: state });
expect(read(providerState).providerSessionIdentity).toEqual(providerState.identity);
for (const cursorMode of [undefined, null, "agent", "ask", "autopilot"]) {
expect(read({ ...providerState, identity: { ...providerState.identity, cursorMode } }).providerSessionIdentity).toBeNull();
expect(read({ ...providerState, descriptor: { ...providerState.descriptor, cursorMode } }).providerSessionIdentity).toBeNull();
expect(read(providerState, execution({ ...input.provider, cursorMode }, "acpx_runtime")).providerSessionIdentity).toBeNull();
for (const mode of [undefined, null, "agent", "ask", "autopilot"]) {
expect(read({ ...providerState, identity: { ...providerState.identity, mode } }).providerSessionIdentity).toBeNull();
expect(read({ ...providerState, descriptor: { ...providerState.descriptor, mode } }).providerSessionIdentity).toBeNull();
expect(read(providerState, execution({ ...input.provider, mode }, "acpx_runtime")).providerSessionIdentity).toBeNull();
}
const foreign = { ...providerState, descriptor: { ...providerState.descriptor, agent: "copilot" } };
for (const cursorMode of [undefined, "plan"]) {
expect(read(foreign, execution({ ...input.provider, agent: "copilot", cursorMode }, "acpx_runtime")).providerSessionIdentity).toBeNull();
}
expect(read(foreign, execution({ ...input.provider, agent: "copilot", mode: undefined }, "acpx_runtime")).providerSessionIdentity).toBeNull();
expect(read(foreign, execution({ ...input.provider, agent: "copilot", mode: "plan" }, "acpx_runtime")).providerSessionIdentity).toEqual(providerState.identity);
});
it.each([
@@ -4914,6 +4954,16 @@ describe("native governed waits", () => {
payload: { kind: "dynamicToolCall" },
};
// The durable fallback preserves the question after provider loss, but
// cannot turn that lost execution into a successful governed wait.
const providerLost = { ...replayedEvent, eventType: "runtime_request.expired" as const,
payload: { reason: "provider_process_lost", replayAllowed: false } };
const lostObservation = createGovernedWaitEventObservation(async () => waitResult);
await lostObservation.observe(providerLost, true);
expect(lostObservation.consume(providerLost)).toBeNull();
await lostObservation.observe(replayedEvent, true);
expect(lostObservation.consume(replayedEvent)).toBeNull();
// The event consumer can lag the provider: a commentary event emitted
// before the tool began may be processed after its approval exists in DB.
// It is not proof that the approval-creating tool response has settled.
@@ -9411,7 +9461,7 @@ describe("native process ownership", () => {
},
);
it.each(["pi", "copilot"])("rejects ACPX candidate %s without host authorization before constructing a backend", async (agent) => {
it.each(["copilot"])("rejects ACPX candidate %s without host authorization before constructing a backend", async (agent) => {
const piExecution = {
...execution,
binding: { ...execution.binding, runId: "run-acpx-pi-rejected" },
@@ -1,6 +1,6 @@
import { configuredEnvironment } from "../../vendor/paperclip-runner/index.js";
import { agents } from "@paperclipai/db";
import { dotRunnerBroker } from "../dot-runner-broker.js";
import { configuredEnvironment } from "../../vendor/paperclip-runner/index.js";
import { CURSOR_DISTRIBUTION_PINS, QUALIFIED_ACPX_PROFILES, QUALIFIED_ACPX_VERSION } from "../../vendor/paperclip-runner/index.js";
import { isProviderMode } from "../../vendor/paperclip-runner/index.js";
import { bundledRemoteProviderPackManifestPath, bundledRemoteRunnerBinary } from "../../vendor/paperclip-runner/index.js";
@@ -38,6 +38,7 @@ import { nativeCompletionFeedback } from "./native-completion-feedback.js";
import { hasAcknowledgedNativeReassignmentStopIntent, hasAcknowledgedNativeStopIntent } from "../acknowledged-native-stop.js";
import { stoppedCodexTurnIsTextOnly } from "./stopped-codex-turn.js";
import { prepareVerifiedRemoteProviderPack } from "./remote-provider-pack.js";
import { selectRemotePiCompanion } from "./remote-pi-companion.js";
import { readNativeLocalProcessStop, PROCESS_START_REQUESTED } from "../native-local-process-stop.js";
import { remoteLeaseCleanupScope } from "../remote-execution-termination.js";
import { resolveConnectorAssignments, isConnectorSkill } from "../connector-runtime.js";
@@ -1248,6 +1249,7 @@ export function createGovernedWaitEventObservation(
resolvePending: () => Promise<PrpStructuredRunResult | null>,
) {
const pendingTools = new Set<string>();
let providerLost = false;
let generation = 0;
let observation: {
sourceInstanceId: string;
@@ -1261,6 +1263,7 @@ export function createGovernedWaitEventObservation(
const currentGeneration = ++generation;
observation = null;
const payload = record(event.payload);
providerLost ||= event.eventType === "runtime_request.expired" && payload.reason === "provider_process_lost";
const kind = payload.kind;
const tool = ["dynamicToolCall", "mcpToolCall", "commandExecution"].includes(String(kind));
if (event.itemId) {
@@ -1276,7 +1279,9 @@ export function createGovernedWaitEventObservation(
if (event.eventType === "item.completed" && (
pendingTools.size > 0 || (!tool && !(kind === "agentMessage" && payload.channel === "final"))
)) return;
if (!eligible) return;
// A replacement question preserves human input after a provider crash;
// it does not authorize a successful suspension of the failed execution.
if (providerLost || !eligible) return;
const result = await resolvePending();
if (generation !== currentGeneration || result === null) return;
// If the interaction is answered after this read, parking remains the
@@ -1656,8 +1661,8 @@ function nativeSessionKey(execution: NativeExecutionInput): string {
);
}
function nativeSessionWorkspaceScope(execution: NativeExecutionInput) {
if (execution.provider.kind === "openai_dot") return { kind: "none" as const };
export function nativeSessionWorkspaceScope(execution: { binding: Pick<NativeExecutionInput["binding"], "runId" | "executionWorkspaceId">; workspace: NativeExecutionInput["workspace"] }) {
if ("access" in execution.workspace) return { kind: "none" as const };
// Projectless local runs use the heartbeat run id as a durable placeholder
// rather than fabricating an execution_workspaces row. Do not let that
// per-run placeholder break continuity for the same provider session; the
@@ -5623,6 +5628,7 @@ export function providerSessionIdentityFromDurableProviderState(input: {
identity.effectiveModel !== expectedModel ||
identity.permissionMode !== input.execution.provider.permissionMode ||
!acpxRecoveryModeMatches(input.execution.provider, descriptor.mode, identity.mode) ||
!acpxRecoveryPiThinkingMatches(input.execution.provider, descriptor.piThinkingLevel, identity.piThinkingLevel) ||
!["approve-all", "approve-paperclip", "approve-reads", "deny-all"].includes(
String(identity.permissionMode),
) ||
@@ -5730,6 +5736,12 @@ function acpxRecoveryModeMatches(
&& observedModes.every(mode => mode === expected);
}
function acpxRecoveryPiThinkingMatches(provider: NativeExecutionInput["provider"], ...observed: unknown[]): boolean {
const expected = record(provider).piThinkingLevel;
if (provider.kind === "acpx" && provider.agent === "pi") return ["off", "low", "high", "max"].includes(String(expected)) && observed.every(value => value === expected);
return expected === undefined && observed.every(value => value === undefined);
}
export function providerSessionIdentityTransitionIsAllowed(input: {
execution: NativeExecutionInput;
previous: unknown;
@@ -5740,6 +5752,7 @@ export function providerSessionIdentityTransitionIsAllowed(input: {
record(record(input.previous).providerSessionIdentity).mode,
record(record(input.current).providerSessionIdentity).mode,
)) return false;
if (!acpxRecoveryPiThinkingMatches(input.execution.provider, record(record(input.previous).providerSessionIdentity).piThinkingLevel, record(record(input.current).providerSessionIdentity).piThinkingLevel)) return false;
if (canonicalJson(input.previous) === canonicalJson(input.current)) {
return true;
}
@@ -7651,7 +7664,7 @@ async function executePaperclipNativeSessionWithinScope(
}
if (
input.execution.provider.kind === "acpx" &&
["pi", "copilot"].includes(input.execution.provider.agent) &&
["copilot"].includes(input.execution.provider.agent) &&
!resolveAcpxQualification(input.execution.provider, process.env)
) {
throw new Error(
@@ -9890,7 +9903,7 @@ type RemoteProviderPackManifest = {
distDigest: string;
bridgeDigest: string;
acpxProfileDigests: typeof REMOTE_PROVIDER_PACK_PROFILE_DIGESTS;
providers?: Partial<Record<"cursor", {
providers?: Partial<Record<"pi" | "cursor", {
version: string; profileDigest: string; closureDigest: string; qualification: "qualified" | "pending";
path: string; sha256: string;
}>>;
@@ -10079,9 +10092,10 @@ function readRemoteProviderPackIdentity(packRoot: string, verifyControllerFiles:
}
for (const [provider, candidate] of Object.entries(candidates)) {
const expectedPath = `provider-assets/${provider}/${payload.target.platform}-${payload.target.architecture}`;
if (!(inventory === "providers" ? ["cursor"] : ["cursor", "copilot", "pi"]).includes(provider) || !candidate
if (!(inventory === "providers" ? ["pi", "cursor"] : ["cursor", "copilot", "pi"]).includes(provider) || !candidate
|| Object.keys(candidate).some(key => !["version", "profileDigest", "closureDigest", "qualification", "path", "sha256"].includes(key))
|| candidate.qualification !== (provider === "cursor" ? "qualified" : "pending") || candidate.path !== expectedPath
|| candidate.qualification !== (["pi", "cursor"].includes(provider) ? "qualified" : "pending")
|| candidate.path !== expectedPath
|| typeof candidate.version !== "string" || !candidate.version || candidate.version.length > 120
|| !/^sha256:[a-f0-9]{64}$/.test(candidate.profileDigest)
|| !/^sha256:[a-f0-9]{64}$/.test(candidate.closureDigest)
@@ -11348,8 +11362,16 @@ async function createRunnerdBackendWithinSessionClaim(
remoteTarget !== null &&
(input.execution.provider.kind === "opencode" ||
input.execution.provider.kind === "acpx");
// Pi's explicit operator import supplies the target-platform authority. Never
// substitute a Mac controller daemon or trust an image's self-reported hashes.
// Existing explicit overrides and every other provider keep their old path.
const remotePiCompanion = await selectRemotePiCompanion({
provider: input.execution.provider, remote: remoteTarget !== null,
binaryOverride: input.runnerRemoteBinaryPath, packOverride: input.runnerRemoteProviderPackPath,
workspaceRoot: input.execution.workspace.cwd,
});
const configuredProviderPackRoot =
input.runnerRemoteProviderPackPath?.trim() || null;
input.runnerRemoteProviderPackPath?.trim() || remotePiCompanion?.providerPack || null;
let expectedProviderPackManifest: RemoteProviderPackManifest | null = null;
const useBundledCursorImageAssets = requiresRemoteProviderPack && !configuredProviderPackRoot &&
input.execution.provider.kind === "acpx" && input.execution.provider.agent === "cursor";
@@ -11382,7 +11404,7 @@ async function createRunnerdBackendWithinSessionClaim(
// When an explicit remote artifact is configured, prepareRemoteRunner stages
// these exact bytes at remoteBinary before launch.
const controllerRunnerBinary = remoteTarget
? input.runnerRemoteBinaryPath?.trim() || (useBundledCursorImageAssets ? bundledRemoteRunnerBinary() : resolvePaperclipRunnerBinary())
? input.runnerRemoteBinaryPath?.trim() || remotePiCompanion?.runnerBinary || (useBundledCursorImageAssets ? bundledRemoteRunnerBinary() : resolvePaperclipRunnerBinary())
: resolvePaperclipRunnerBinary();
const explicitRemoteCodex = input.runnerRemoteCodexPath?.trim() || null;
const remoteCodexNpmSpec = input.runnerRemoteCodexNpmSpec?.trim() || null;
@@ -11761,7 +11783,7 @@ async function createRunnerdBackendWithinSessionClaim(
if (!existsSync(sourceBinary)) {
throw new Error("runner_remote_artifact_unavailable");
}
if (!explicitRemoteBinary) {
if (!explicitRemoteBinary && !remotePiCompanion) {
const platform = await remoteCommandRunner.execute({
command: "sh",
args: ["-c", "uname -s; uname -m"],
@@ -13019,6 +13041,7 @@ async function createRunnerdBackendWithinSessionClaim(
: resolveAcpxQualification(input.execution.provider, process.env),
acpxPermissionMode: input.execution.provider.permissionMode,
acpxMode: input.execution.provider.mode,
piThinkingLevel: input.execution.provider.piThinkingLevel,
acpxPermissionModePinned:
input.execution.schema === "paperclip.native-execution-input.v4" ||
input.execution.schema === "paperclip.native-execution-input.v5",
@@ -734,6 +734,10 @@ const recoveryFakeCodex = resolve(
const previousStateBase = process.env.PAPERCLIP_RUNNER_STATE_DIR;
const server = createServer();
let firstSession: NativeSession | undefined;
const ownedSessions = new Set<NativeSession>();
const ownedSpawns: Array<{ pid: number; processGroupId: number | null; startedAt: string }> = [];
const onSpawn = async (meta: typeof ownedSpawns[number]) => { ownedSpawns.push(meta); };
let executionCloseCompleted = false;
const runnerDiagnostics: string[] = [];
const onRunnerLog = async (_stream: "stdout" | "stderr", chunk: string) => {
runnerDiagnostics.push(chunk.slice(-4_096));
@@ -860,6 +864,7 @@ const recoveryFakeCodex = resolve(
runnerInstanceId,
runnerEnvironment: environment,
onLog: onRunnerLog,
onSpawn,
});
firstSession = await firstBackend.openSession({
identity: {
@@ -871,6 +876,7 @@ const recoveryFakeCodex = resolve(
},
workingDirectory: workspace,
});
ownedSessions.add(firstSession);
const checkpoint = await firstSession.snapshot();
expect(checkpoint.identity).toEqual({
companyId,
@@ -1009,6 +1015,7 @@ const recoveryFakeCodex = resolve(
runnerInstanceId,
runnerEnvironment: environment,
onLog: onRunnerLog,
onSpawn,
});
let continuity: Record<string, unknown> | undefined;
const controlPlaneInstanceId = randomUUID();
@@ -1031,6 +1038,12 @@ const recoveryFakeCodex = resolve(
runnerInstanceId,
controlPlaneInstanceId,
timeoutMs: 20_000,
// This disposable real-daemon fixture must join full retirement before
// removing its controller routes and durable state. The production
// default deliberately permits asynchronous cleanup after a result.
requireSessionCloseBeforeReturn: true,
onSession(value) { if (value) ownedSessions.add(value); },
async onSessionClosed() { executionCloseCompleted = true; },
controlPlane: port,
async onContinuityBreak(value) {
continuity = value;
@@ -1064,6 +1077,12 @@ const recoveryFakeCodex = resolve(
terminal: { runTerminalState: "succeeded" },
normalizedSessionId,
});
expect(executionCloseCompleted).toBe(true);
expect(ownedSpawns.length).toBeGreaterThanOrEqual(2);
for (const { pid } of ownedSpawns) {
expect(() => process.kill(pid, 0)).toThrow(expect.objectContaining({ code: "ESRCH" }));
}
console.info("Recovery fixture retired owned daemons", ownedSpawns);
expect(continuity).toMatchObject({
// The daemon can reject the damaged retained input during startup,
// before attach gets a chance to reject the unsettled provider session.
@@ -1201,9 +1220,13 @@ const recoveryFakeCodex = resolve(
.where(eq(issues.id, issueId));
expect(task).toEqual({ id: issueId, assigneeAgentId: agentId });
} finally {
await firstSession
?.close({ reason: "Recovery fixture cleanup" })
.catch(() => undefined);
// onSession(null) quarantines the runtime owner before close finishes;
// keep every published handle so an assertion/error cannot lose cleanup.
if (firstSession) ownedSessions.add(firstSession);
const closed = await Promise.allSettled([...ownedSessions].map((session) =>
Promise.resolve().then(() => session.close({ reason: "Recovery fixture cleanup" })),
));
const closeFailures = closed.filter((result) => result.status === "rejected");
runnerPrpWebSocketInternals.resetForTests();
server.closeAllConnections();
await new Promise<void>((done) => server.close(() => done()));
@@ -1211,6 +1234,10 @@ const recoveryFakeCodex = resolve(
delete process.env.PAPERCLIP_RUNNER_STATE_DIR;
else process.env.PAPERCLIP_RUNNER_STATE_DIR = previousStateBase;
await database.cleanup();
if (closeFailures.length) {
// Retain exact fixture state if owned retirement cannot be proven.
throw new AggregateError(closeFailures.map((result) => result.reason), "Recovery fixture session cleanup failed");
}
await rm(scratch, { recursive: true, force: true });
}
},
@@ -5,6 +5,7 @@ import {
PAPERCLIP_RUNNER_ACPX_PROFILES,
resolvePaperclipRunnerPermissionMode,
resolvePaperclipRunnerCursorMode,
resolvePaperclipRunnerPiThinkingLevel,
type PaperclipRunnerProvider,
} from "@paperclipai/adapter-utils";
import {
@@ -29,6 +30,7 @@ export const DEFAULT_ACPX_RUNNER_MODELS = {
// These profiles require explicit configuration. Admission belongs to the runner.
codex: null,
cursor: null,
pi: null,
} as const;
export type QualifiedPaperclipRunnerAcpxAgent =
@@ -127,6 +129,7 @@ export type PaperclipRunnerNativeProviderInput =
acpxAgent: AdmittedPaperclipRunnerAcpxAgent;
acpxPermissionMode: "approve-all" | "approve-paperclip" | "approve-reads" | "deny-all";
acpxSessionMode?: "agent" | "plan" | "ask";
piThinkingLevel?: "off" | "low" | "high" | "max";
};
export class PaperclipRunnerProviderProfileError extends Error {
@@ -376,10 +379,13 @@ export function resolvePaperclipRunnerProviderProfile(
resolvePaperclipRunnerCursorMode(candidate, config.acpxAgent, config.acpxSessionMode);
} catch (error) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_cursor_mode_invalid",
"paperclip_runner_mode_invalid",
error instanceof Error ? error.message : "Invalid Cursor session mode",
);
}
try { resolvePaperclipRunnerPiThinkingLevel(candidate, config.acpxAgent, config.piThinkingLevel); } catch (error) {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_pi_thinking_invalid", error instanceof Error ? error.message : "Invalid Pi thinking level");
}
const model = optionalString(config.model);
if (candidate === "codex") {
return {
@@ -486,10 +492,10 @@ export function resolvePaperclipRunnerProviderProfile(
}
throw new PaperclipRunnerProviderProfileError("paperclip_runner_acpx_agent_unavailable", `${pendingAcpxProfile.label} is awaiting local and Daytona qualification. Its profile is not enabled for production runs.`);
}
if (acpxAgent === "cursor" && !model) {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_acpx_model_required", "Cursor requires an explicit model ID; there is no default model.");
if ((acpxAgent === "cursor" || acpxAgent === "pi") && !model) {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_acpx_model_required", "This ACPX agent requires an explicit model ID; there is no default model.");
}
if (acpxAgent !== "claude" && acpxAgent !== "codex" && acpxAgent !== "grok" && acpxAgent !== "cursor") {
if (acpxAgent !== "claude" && acpxAgent !== "codex" && acpxAgent !== "grok" && acpxAgent !== "cursor" && acpxAgent !== "pi") {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_acpx_agent_unavailable",
"Paperclip Runner ACPX requires a qualified agent profile.",
@@ -567,6 +573,7 @@ export function resolvePaperclipRunnerNativeProviderInput(input: {
provider: "acpx",
model: profile.model,
acpxAgent: profile.acpxAgent,
...(profile.acpxAgent === "pi" ? { piThinkingLevel: resolvePaperclipRunnerPiThinkingLevel("acpx", "pi", config.piThinkingLevel) } : {}),
...(profile.acpxAgent === "cursor" ? {
acpxSessionMode: resolvePaperclipRunnerCursorMode("acpx", "cursor", config.acpxSessionMode),
} : {}),
@@ -0,0 +1,162 @@
/** Explicit operator-imported Linux authority. No downloads or executable probes. */
import { setImmediate as yieldToSignals } from "node:timers/promises";
import { createHash } from "node:crypto";
import { type Stats, constants } from "node:fs";
import * as fs from "node:fs/promises";
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { QUALIFIED_ACPX_PROFILES } from "../../vendor/paperclip-runner/index.js";
const SERVER_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
const MAX_BYTES = 4 * 1024 ** 3;
const MAX_FILE = 512 * 1024 ** 2;
const MANIFEST = "companion.json";
const AUTHORITY = ".import-authority.json";
type Entry = { path: string; mode: number; kind: "directory" } | { path: string; mode: number; kind: "file"; size: number; sha256: string } | { path: string; mode: number; kind: "symlink"; target: string };
export interface RemotePiCompanionManifest { schema: "paperclip.remote-pi-companion/v1"; sourceRevision: string; target: "linux-x64"; profileDigest: string; providerPackDigest: string; daemonSha256: string; entries: Entry[] }
function require(value: unknown, reason: string): asserts value { if (!value) throw new Error(`runner_remote_companion_invalid: ${reason}`); }
const digest = (value: Buffer | string) => createHash("sha256").update(value).digest("hex");
const safe = (path: string) => path.length > 0 && path.length < 4096 && !isAbsolute(path) && !/[\\\x00-\x1f\x7f]/u.test(path) && path.split("/").every(p => p !== "" && p !== "." && p !== "..");
const inside = (root: string, path: string) => path === root || (!relative(root, path).startsWith("..") && !isAbsolute(relative(root, path)));
const same = (a: Stats, b: Stats) => a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mode === b.mode && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs && a.nlink === b.nlink;
type Checkpoint = () => Promise<void>;
function checkpoints(cancel?: () => void): Checkpoint {
const deadline = Date.now() + 600_000;
return async () => { await yieldToSignals(); cancel?.(); require(Date.now() < deadline, "import deadline exceeded"); };
}
async function directory(path: string) { const st = await fs.lstat(path); require(st.isDirectory() && !st.isSymbolicLink() && await fs.realpath(path) === path, "directory is linked or noncanonical"); return st; }
/** One descriptor and 1MiB buffer; no whole executable allocation or sync hashing. */
async function readStable(path: string, maximum: number, privateFile: boolean, check: Checkpoint, consume?: (chunk: Buffer) => Promise<void>) {
require(await fs.realpath(dirname(path)) === dirname(path), "linked parent");
const file = await fs.open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
try {
const before = await file.stat(); require(before.isFile() && before.size <= maximum && (!privateFile || before.nlink === 1), "file type, link or byte bound");
const hash = createHash("sha256"); let size = 0; const buffer = Buffer.alloc(1024 * 1024); let header = Buffer.alloc(0);
for (;;) {
await check(); const { bytesRead } = await file.read(buffer); if (!bytesRead) break;
size += bytesRead; require(size <= maximum && size <= before.size, "file grew beyond bound");
const chunk = buffer.subarray(0, bytesRead); hash.update(chunk); if (!header.length) header = Buffer.from(chunk.subarray(0, 64));
await consume?.(chunk);
}
require(size === before.size && same(before, await file.stat()) && same(before, await fs.lstat(path)), "file changed during read");
return { size, sha256: hash.digest("hex"), header, stat: before };
} finally { await file.close(); }
}
async function readOwned(path: string, maximum: number, check: Checkpoint, privateFile = false): Promise<Buffer> {
const chunks: Buffer[] = []; await readStable(path, maximum, privateFile, check, async chunk => { chunks.push(Buffer.from(chunk)); }); return Buffer.concat(chunks);
}
/** Complete no-follow inventory, including modes and contained symbolic links. */
export async function inventoryRemoteCompanion(root: string, check = checkpoints()): Promise<Entry[]> {
await directory(root); const entries: Entry[] = []; const links = new Map<string, { count: number; links: number }>(); let total = 0;
const visit = async (path: string): Promise<void> => {
const before = await directory(path);
for (const name of (await fs.readdir(path)).sort()) {
await check();
if (path === root && [MANIFEST, AUTHORITY].includes(name)) continue;
const file = join(path, name); const rel = relative(root, file); require(safe(rel), "unsafe path");
const st = await fs.lstat(file); const mode = st.mode & 0o7777;
require(entries.length < 100_000 && (st.isSymbolicLink() || (mode & 0o7022) === 0), "entry count or unsafe mode");
if (st.isDirectory()) { require((mode & 0o700) === 0o700, "directory requires owner read/write/search for owned cleanup"); entries.push({ path: rel, mode, kind: "directory" }); await visit(file); }
else if (st.isFile()) {
total += st.size; require(total <= MAX_BYTES, "tree byte bound"); const read = await readStable(file, MAX_FILE, false, check);
require(same(st, read.stat), "discovered file changed");
entries.push({ path: rel, mode, kind: "file", size: read.size, sha256: read.sha256 });
const key = `${st.dev}:${st.ino}`; const group = links.get(key) ?? { count: 0, links: st.nlink }; group.count++; require(group.links === st.nlink, "hardlink identity drift"); links.set(key, group);
} else if (st.isSymbolicLink()) {
const target = await fs.readlink(file); require(!isAbsolute(target) && !/[\x00-\x1f\x7f]/u.test(target) && inside(root, resolve(dirname(file), target)) && inside(root, await fs.realpath(file)), "escaping symbolic link");
require(same(st, await fs.lstat(file)), "link changed"); entries.push({ path: rel, mode, kind: "symlink", target });
} else throw new Error("runner_remote_companion_invalid: special entry");
}
const after = await directory(path); require(before.dev === after.dev && before.ino === after.ino && before.mtimeMs === after.mtimeMs, "directory changed");
};
await visit(root); require([...links.values()].every(g => g.count === g.links), "external hardlink");
return entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
}
async function installedIdentity(serverRoot: string, check: Checkpoint) {
require(process.platform === "darwin" || process.platform === "linux", "companion import supports macOS and Linux controllers");
await directory(serverRoot); const pkg = JSON.parse((await readOwned(join(serverRoot, "package.json"), 65536, check, true)).toString()); require(pkg.name === "@paperclipai/server", "public server package required");
const source = JSON.parse((await readOwned(join(serverRoot, "dist/build-info.json"), 65536, check, true)).toString()).commit;
require(typeof source === "string" && /^[a-f0-9]{40}$/u.test(source), "installed build source is missing");
const profile = QUALIFIED_ACPX_PROFILES.pi; require(profile.qualificationStatus !== "pending", "Pi is not qualified");
return { source, profileDigest: profile.commandDigest };
}
async function validate(root: string, manifest: RemotePiCompanionManifest, identity: Awaited<ReturnType<typeof installedIdentity>>, check: Checkpoint) {
require(manifest.schema === "paperclip.remote-pi-companion/v1" && manifest.target === "linux-x64" && manifest.sourceRevision === identity.source && manifest.profileDigest === identity.profileDigest, "source/profile/target mismatch");
require(JSON.stringify(await inventoryRemoteCompanion(root, check)) === JSON.stringify(manifest.entries), "complete inventory mismatch");
const daemon = manifest.entries.find(e => e.path === "bin/paperclip-runnerd");
require(daemon?.kind === "file" && daemon.size > 64 && (daemon.mode & 0o111) !== 0 && daemon.sha256 === manifest.daemonSha256, "daemon identity");
const elf = (await readStable(join(root, daemon.path), MAX_FILE, false, check)).header; require(elf.subarray(0, 4).equals(Buffer.from([127, 69, 76, 70])) && elf[4] === 2 && elf[5] === 1 && elf.readUInt16LE(18) === 62, "Linux x64 ELF required");
const pack = JSON.parse((await readOwned(join(root, "provider-pack/provider-pack.json"), 16 * 1024 ** 2, check)).toString());
const canonical = (v: unknown): string => Array.isArray(v) ? `[${v.map(canonical).join(",")}]` : v && typeof v === "object" ? `{${Object.keys(v).sort().map(k => `${JSON.stringify(k)}:${canonical((v as Record<string, unknown>)[k])}`).join(",")}}` : JSON.stringify(v);
require(pack.schema === "paperclip-runner/remote-provider-pack/v1" && pack.digest === manifest.providerPackDigest && pack.digest === `sha256:${digest(canonical(pack.payload))}`, "pack manifest digest");
require(pack.payload.runnerSourceRevision === identity.source && pack.payload.target.platform === "linux" && pack.payload.target.architecture === "x64", "pack source/target");
const pi = pack.payload.candidateProviders?.pi; require(pi?.qualification === "qualified" && pi.profileDigest === identity.profileDigest && pi.path === "provider-assets/pi/linux-x64", "normal Pi pack required");
}
/** Release-side command uses this same inventory contract before publication. */
export async function createRemotePiCompanionManifest(root: string, sourceRevision: string): Promise<RemotePiCompanionManifest> {
const check = checkpoints();
const profile = QUALIFIED_ACPX_PROFILES.pi; const pack = JSON.parse((await readOwned(join(root, "provider-pack/provider-pack.json"), 16 * 1024 ** 2, check)).toString()); const entries = await inventoryRemoteCompanion(root, check);
const daemon = entries.find(e => e.path === "bin/paperclip-runnerd"); require(daemon?.kind === "file", "daemon missing");
const manifest: RemotePiCompanionManifest = { schema: "paperclip.remote-pi-companion/v1", sourceRevision, target: "linux-x64", profileDigest: profile.commandDigest, providerPackDigest: pack.digest, daemonSha256: daemon.sha256, entries };
require(/^[a-f0-9]{40}$/u.test(sourceRevision), "release source"); await validate(root, manifest, { source: sourceRevision, profileDigest: profile.commandDigest }, check); return manifest;
}
function cacheParent(serverRoot: string) { return join(serverRoot, "remote-companions"); }
async function removeOwned(path: string, owned: Stats) { const st = await fs.lstat(path); require(st.dev === owned.dev && st.ino === owned.ino && !st.isSymbolicLink(), "cleanup root ownership changed"); await fs.rm(path, { recursive: true }); }
export async function importRemotePiCompanion(input: { directory: string; sha256: string; serverRoot?: string; checkCancelled?: () => void }) {
const checkpoint = checkpoints(input.checkCancelled);
await checkpoint();
const serverRoot = input.serverRoot ?? SERVER_ROOT; const identity = await installedIdentity(serverRoot, checkpoint); const source = await fs.realpath(input.directory); require(source === resolve(input.directory), "linked import root"); await directory(source);
require(/^[a-f0-9]{64}$/u.test(input.sha256), "expected manifest SHA256 required"); const bytes = await readOwned(join(source, MANIFEST), 32 * 1024 ** 2, checkpoint); require(digest(bytes) === input.sha256, "operator manifest digest mismatch");
const manifest = JSON.parse(bytes.toString()) as RemotePiCompanionManifest; await validate(source, manifest, identity, checkpoint); await checkpoint();
const parent = cacheParent(serverRoot); try { await fs.mkdir(parent, { mode: 0o700 }); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw new Error("Remote companion setup requires a writable installed server package", { cause: error }); }
const parentInfo = await directory(parent); require((parentInfo.mode & 0o077) === 0 && parentInfo.uid === process.getuid?.(), "cache must be private and operator-owned");
const lockPath = join(parent, ".import-linux-x64.lock"); const lock = await fs.open(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600); const lockInfo = await lock.stat();
let staging: string | undefined; let stagingInfo: Stats | undefined; let outputInfo: Stats | undefined; let committed = false; const output = join(parent, "linux-x64");
try {
await checkpoint();
try { await fs.lstat(output); const existing = await resolveRemotePiCompanion({ serverRoot, checkpoint }); require(existing?.manifestSha256 === input.sha256, "existing companion differs; remove only after stopping all runs"); await checkpoint(); return { status: "verified_existing", ...existing }; } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
staging = await fs.mkdtemp(join(parent, ".import-")); stagingInfo = await fs.lstat(staging);
for (const entry of manifest.entries.filter(e => e.kind === "directory").sort((a, b) => a.path.split("/").length - b.path.split("/").length)) await fs.mkdir(join(staging, entry.path), { mode: 0o700 });
for (const entry of manifest.entries) {
if (entry.kind !== "file") continue;
const file = join(staging, entry.path); const outputFile = await fs.open(file, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
try {
const read = await readStable(join(source, entry.path), MAX_FILE, false, checkpoint, async chunk => {
let offset = 0; while (offset < chunk.length) { await checkpoint(); offset += (await outputFile.write(chunk, offset)).bytesWritten; }
});
require(read.size === entry.size && read.sha256 === entry.sha256, "source changed while copied");
} finally { await outputFile.close(); }
await fs.chmod(file, entry.mode); await checkpoint();
}
for (const entry of manifest.entries) if (entry.kind === "symlink") await fs.symlink(entry.target, join(staging, entry.path));
for (const entry of manifest.entries.filter(e => e.kind === "directory").reverse()) await fs.chmod(join(staging, entry.path), entry.mode);
await fs.writeFile(join(staging, MANIFEST), bytes, { flag: "wx", mode: 0o600 }); await validate(staging, manifest, identity, checkpoint); await validate(source, manifest, identity, checkpoint); await checkpoint();
// Claim the final path exclusively; no rename may replace a concurrent directory.
await fs.mkdir(output, { mode: 0o700 }); outputInfo = await fs.lstat(output);
for (const name of await fs.readdir(staging)) { const now = await directory(output); require(now.dev === outputInfo.dev && now.ino === outputInfo.ino, "publication ownership changed"); await fs.rename(join(staging, name), join(output, name)); }
await fs.writeFile(join(output, AUTHORITY), JSON.stringify({ sha256: input.sha256 }) + "\n", { flag: "wx", mode: 0o600 });
const result = await resolveRemotePiCompanion({ serverRoot, checkpoint }); require(result?.manifestSha256 === input.sha256, "publication verification"); await checkpoint(); committed = true; return { status: "imported_verified", ...result };
} finally {
const failures: unknown[] = []; const cleanup = async (fn: () => Promise<void>) => { try { await fn(); } catch (error) { failures.push(error); } };
if (outputInfo && !committed) await cleanup(() => removeOwned(output, outputInfo!));
if (staging && stagingInfo) await cleanup(() => removeOwned(staging!, stagingInfo!));
await cleanup(async () => { const now = await fs.lstat(lockPath); require(now.dev === lockInfo.dev && now.ino === lockInfo.ino, "lock ownership changed"); await fs.unlink(lockPath); }); await lock.close();
if (failures.length) throw new AggregateError(failures, "Remote companion cleanup incomplete; inspect owned paths before retrying");
}
}
export async function resolveRemotePiCompanion(input: { serverRoot?: string; workspaceRoot?: string; checkpoint?: Checkpoint } = {}) {
const check = input.checkpoint ?? checkpoints();
const serverRoot = input.serverRoot ?? SERVER_ROOT; const parent = cacheParent(serverRoot); const root = join(parent, "linux-x64");
try { await fs.lstat(root); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; throw error; }
const parentInfo = await directory(parent); require((parentInfo.mode & 0o077) === 0 && parentInfo.uid === process.getuid?.(), "cache privacy"); await directory(root);
if (input.workspaceRoot) { const workspace = await fs.realpath(input.workspaceRoot); require(!inside(workspace, root) && !inside(root, workspace), "companion cache cannot overlap a workspace"); }
const authority = JSON.parse((await readOwned(join(root, AUTHORITY), 65536, check, true)).toString()); const bytes = await readOwned(join(root, MANIFEST), 32 * 1024 ** 2, check, true); require(digest(bytes) === authority.sha256, "installed authority changed");
const manifest = JSON.parse(bytes.toString()) as RemotePiCompanionManifest; await validate(root, manifest, await installedIdentity(serverRoot, check), check);
return { root, runnerBinary: join(root, "bin/paperclip-runnerd"), providerPack: join(root, "provider-pack"), manifestSha256: authority.sha256 as string, sourceRevision: manifest.sourceRevision, target: manifest.target };
}
/** Explicit legacy overrides remain authoritative; C/C never gain this Pi path. */
export async function selectRemotePiCompanion(input: { provider: { kind: string; agent?: string }; remote: boolean; binaryOverride?: string | null; packOverride?: string | null; workspaceRoot: string; serverRoot?: string }) {
if (!input.remote || input.provider.kind !== "acpx" || input.provider.agent !== "pi" || input.binaryOverride?.trim() || input.packOverride?.trim()) return null;
return resolveRemotePiCompanion({ serverRoot: input.serverRoot, workspaceRoot: input.workspaceRoot });
}
@@ -44,71 +44,27 @@ function arbitrate(
}
describe("native status authority", () => {
it("waits on persisted monitors without an immediate continuation, even with unfinished work", () => {
const pending = assessment({ reportedDisposition: "yielded", hasBlockingRemainingWork: true,
continuation: { kind: "monitor", summary: "Check CI", idempotencyKey: "ci" } });
for (const priorIssueStatus of ["in_progress", "in_review"] as const) {
expect(arbitrate({ assessment: pending, priorIssueStatus, monitorWaitAuthorized: true }))
.toMatchObject({ statusAction: "preserve", toStatus: priorIssueStatus,
reasonCode: "scheduled_monitor_waiting", effects: [{ kind: "release_checkout" }] });
}
expect(arbitrate({ assessment: pending })).toMatchObject({ reasonCode: "monitor_wait_authority_lost",
effects: [{ kind: "record_finalization_error" }] });
expect(arbitrate({ assessment: pending, monitorWaitAuthorized: true, hasUnresolvedIssueBlockers: true }).toStatus).toBe("blocked");
expect(arbitrate({ assessment: pending, monitorWaitAuthorized: true, governanceGate: { kind: "approval", id: "pending" } }).toStatus).toBe("in_review");
});
it("keeps a pending child result non-terminal without adding another continuation", () => {
expect(arbitrate({ hasPendingChildCompletion: true })).toMatchObject({
statusAction: "in_progress", toStatus: "in_progress", reasonCode: "native_child_completion_pending",
effects: [{ kind: "release_checkout" }],
it("keeps a verified accepted Cursor plan passive without completing or replaying work", () => {
const passive = assessment({ reportedDisposition: "yielded", objectiveSatisfied: false,
allCriteriaSatisfied: false, hasBlockingRemainingWork: true,
continuation: { kind: "response_wake", summary: "Explicit continuation needed", idempotencyKey: "cursor-plan-wait:event" } });
expect(arbitrate({ assessment: passive, planWaitAuthorized: true })).toMatchObject({
toStatus: "in_progress", reasonCode: "native_plan_accepted_waiting_for_continuation", effects: [],
});
expect(arbitrate({ hasPendingChildCompletion: false }).toStatus).toBe("done");
for (const priorIssueStatus of ["done", "cancelled"] as const) {
expect(arbitrate({ priorIssueStatus, hasPendingChildCompletion: true }).toStatus).toBe(priorIssueStatus);
}
expect(arbitrate({ hasPendingChildCompletion: true, hasUnresolvedIssueBlockers: true }).toStatus).toBe("blocked");
expect(arbitrate({ hasPendingChildCompletion: true, governanceGate: { kind: "approval", id: "approval" } }).toStatus)
.toBe("in_review");
expect(arbitrate({ hasPendingChildCompletion: true, workspaceFinalizeStatus: "failed" }).statusAction).toBe("preserve");
expect(arbitrate({ assessment: passive })).toMatchObject({
toStatus: "in_progress",
reasonCode: "completion_evidence_incomplete",
effects: [expect.objectContaining({
kind: "enqueue_continuation",
continuationKind: "same_agent",
idempotencyKey: "native-completion-incomplete",
})],
});
expect(arbitrate({ assessment: passive, planWaitAuthorized: true, terminalState: "failed" }).reasonCode).not.toBe("native_plan_accepted_waiting_for_continuation");
expect(arbitrate({ assessment: passive, planWaitAuthorized: true, priorIssueStatus: "cancelled" }).toStatus).toBe("cancelled");
expect(arbitrate({ assessment: passive, planWaitAuthorized: true, governanceGate: { kind: "interaction", id: "pending" } }).toStatus).toBe("in_review");
});
it("schedules a capacity retry while preserving partial work and review authority", () => {
for (const nativeReviewOutcome of [undefined, "pending"] as const) {
const decision = arbitrate({ terminalState: "failed", providerOverloaded: true, nativeReviewOutcome });
expect(decision).toMatchObject({ statusAction: "preserve", reasonCode: "native_provider_overloaded" });
expect(decision.effects).toContainEqual(expect.objectContaining({ kind: "schedule_retry", cause: "native_provider_overloaded" }));
}
expect(arbitrate({ terminalState: "failed", providerOverloaded: true, failureRetryCount: 2 }))
.toMatchObject({ statusAction: "blocked", reasonCode: "native_provider_overloaded_exhausted", effects: [{ kind: "bind_blocker", owner: "board", action: expect.stringContaining("Automatic retries exhausted") }] });
});
it.each([
{ hasActivePauseHold: true }, { hasUnresolvedIssueBlockers: true },
{ governanceGate: { kind: "approval" as const, id: "approval" } },
{ priorIssueStatus: "blocked" as const }, { priorIssueStatus: "done" as const },
{ workspaceFinalizeStatus: "failed" as const }, { nativeReviewOutcome: "stale" as const },
{ nativeReviewOutcome: "resolved" as const },
])("does not schedule capacity retries through existing authority or cleanup gates (%j)", (gate) => {
const decision = arbitrate({ terminalState: "failed", providerOverloaded: true, ...gate });
expect(decision.effects.some(effect => effect.kind === "schedule_retry")).toBe(false);
});
it.each(["in_progress", "in_review"] as const)("blocks a current worker's proven model rejection without a retry (%s)", (priorIssueStatus) => {
const decision = arbitrate({ priorIssueStatus, terminalState: "failed", providerModelRejected: true });
expect(decision).toMatchObject({ statusAction: "blocked", toStatus: "blocked", reasonCode: "native_provider_model_rejected", unblockDescriptor: { owner: "board" } });
expect(decision.effects).toEqual([{ kind: "bind_blocker", owner: "board", action: expect.any(String) }]);
expect(arbitrate({ terminalState: "failed", providerModelRejected: false }).effects).toContainEqual(expect.objectContaining({ kind: "schedule_retry" }));
expect(arbitrate({ terminalState: "succeeded", providerModelRejected: true }).reasonCode).not.toBe("native_provider_model_rejected");
expect(arbitrate({ terminalState: "failed", providerModelRejected: true, workspaceFinalizeStatus: "failed" }).reasonCode).toBe("finalization_failed_claim_preserved");
expect(arbitrate({ terminalState: "failed", providerModelRejected: true, priorIssueStatus: "done" }).toStatus).toBe("done");
for (const nativeReviewOutcome of ["stale", "resolved"] as const) {
expect(arbitrate({ terminalState: "failed", providerModelRejected: true, priorIssueStatus, nativeReviewOutcome }))
.toMatchObject({ statusAction: "preserve", toStatus: priorIssueStatus, reasonCode: "native_review_action_finished" });
}
});
it("preserves pending review authority without automatic recovery after model rejection", () => {
expect(arbitrate({ priorIssueStatus: "in_review", terminalState: "failed", providerModelRejected: true, nativeReviewOutcome: "pending" }))
.toMatchObject({ statusAction: "preserve", toStatus: "in_review", reasonCode: "native_provider_model_rejected", unblockDescriptor: null, effects: [{ kind: "release_checkout" }] });
});
it("a reviewer finishes its decision without completing rejected or still-reviewed work", () => {
for (const priorIssueStatus of ["in_progress", "in_review"] as const) {
const decision = arbitrate({ priorIssueStatus, nativeReviewOutcome: "resolved" });
@@ -1699,32 +1699,6 @@ export async function commitNativeStatusDecision(input: {
throw new NativeStatusRaceError();
}
}
let cursorPlanWait: Awaited<ReturnType<typeof readNativeCursorPlanWait>> = null;
let cursorPlanWaitResult: { resultId: string; resultSha256: string } | null = null;
if (reasonCode === "native_plan_accepted_waiting_for_continuation") {
const expected = input.requireCursorPlanWaitSource;
if (!expected || expected.companyId !== input.companyId || expected.issueId !== input.issueId || expected.runId !== input.runId || input.decision.effects.length !== 0 || input.decision.statusAction !== "in_progress" || input.decision.toStatus !== "in_progress") throw new NativeStatusRaceError();
try {
cursorPlanWait = await readNativeCursorPlanWait(tx as unknown as Db, expected, true);
if (!cursorPlanWait || nativeSha256(cursorPlanWait.source) !== nativeSha256(expected)) throw new NativeStatusRaceError();
// Recheck the committed semantic result too: an arbitrary caller cannot
// use a genuine plan receipt to authorize a different finalization.
const [accepted] = await tx.select().from(nativeRunResults).where(and(
eq(nativeRunResults.id, coordinator!.resultId!), eq(nativeRunResults.companyId, input.companyId),
eq(nativeRunResults.issueId, input.issueId), eq(nativeRunResults.runId, input.runId), eq(nativeRunResults.schemaStatus, "accepted"),
)).for("share", { noWait: true });
const envelope = record(accepted?.resultJson), terminal = record(envelope.terminal);
if (!accepted || accepted.completionContractId !== cursorPlanWait.source.contractId || accepted.turnId !== cursorPlanWait.source.turnId ||
nativeSha256(envelope.result) !== nativeSha256(cursorPlanWait.result) ||
terminal.schema !== "paperclip.prp.terminal.v1" || terminal.runTerminalState !== "succeeded" || terminal.turnTerminalState !== "completed" || terminal.reportedWorkDisposition !== "yielded") throw new NativeStatusRaceError();
// completeRun hashes its full private binding, not the stored resultJson.
// Preserve that accepted identity instead of inventing a new digest.
cursorPlanWaitResult = { resultId: accepted.id, resultSha256: accepted.canonicalSha256 };
} catch (error) {
if (isExternalChatWaitAuthorizationContention(error)) throw new NativeStatusRaceError();
throw error;
}
}
const passiveBoardResponseWait =
reasonCode === "board_response_waiting" ||
reasonCode === "board_response_wait_superseded";
@@ -39,4 +39,3 @@ export type Capability = {
name: string; description: string; schema: z.ZodObject; write?: boolean; configure?: boolean; destructive?: boolean; ephemeral?: boolean;
run: (p: McpPrincipal, args: Record<string, unknown>, api: ApiDispatch, origin: string, transfers?: PublicMcpTransfers) => Promise<Record<string, unknown>>;
};
-1
View File
@@ -1,4 +1,3 @@
import { hasCommittedNativeCursorPlanWait } from "../native-runtime/native-cursor-plan-wait.js";
import { isAiAuthenticationBlocked } from "../ai-auth-failure.js";
import { hasCommittedNativePlanWait } from "../native-runtime/native-plan-wait.js";
import { isNativeWorkspaceExportRepairCause } from "@paperclipai/shared";
+6 -1
View File
@@ -28,6 +28,7 @@ export type {
ControlPlanePort,
HarnessRuntimeRequestKind,
HarnessRuntimeRequestResolution,
LinuxProcessStartOptions,
NativeAcpxAgent,
NativeAcpxPermissionMode,
NativeCodexApprovalPolicy,
@@ -80,6 +81,7 @@ export const DurablePrpControlPlane = runner.DurablePrpControlPlane;
export const runnerCodexDynamicToolsFit = runner.runnerCodexDynamicToolsFit;
export const inspectWarmRunTransition = runner.inspectWarmRunTransition;
export const readRunnerdArtifactBinding = runner.readRunnerdArtifactBinding;
export const readLinuxProcessStartedAt = runner.readLinuxProcessStartedAt;
export const NativeSessionCleanupQuarantinedError =
runner.NativeSessionCleanupQuarantinedError;
export const NativeSessionProtocolIntegrityError =
@@ -143,10 +145,13 @@ export const nativeRestartInterruptedTurnId = runner.nativeRestartInterruptedTur
export const completeTerminatedRemoteNativeSessionCleanup = runner.completeTerminatedRemoteNativeSessionCleanup;
export const completeTerminatedLocalNativeSessionCleanup = runner.completeTerminatedLocalNativeSessionCleanup;
export const externalOperationDigest = runner.externalOperationDigest;
export const probeAcpxClaudeInstallation = runner.probeAcpxClaudeInstallation;
export const probeAcpxGrokInstallation = runner.probeAcpxGrokInstallation;
export const probeAcpxPiInstallation = runner.probeAcpxPiInstallation;
export const bundledRemoteProviderPackManifestPath = runner.bundledRemoteProviderPackManifestPath;
export const bundledRemoteRunnerBinary = runner.bundledRemoteRunnerBinary;
export const externalOperationDigest = runner.externalOperationDigest;
export const CONFIGURED_ENVIRONMENT_KEYS = runner.CONFIGURED_ENVIRONMENT_KEYS;
export const GENERATED_RUNTIME_ENVIRONMENT_KEYS = runner.GENERATED_RUNTIME_ENVIRONMENT_KEYS;
export const configuredEnvironmentProjection = runner.configuredEnvironmentProjection;
+1
View File
@@ -3,4 +3,5 @@ export {
probeAcpxClaudeInstallation,
probeAcpxGrokInstallation,
probeAcpxCursorInstallation,
probeAcpxPiInstallation,
} from "@paperclipai/paperclip-runner/live";