fix: allow bounded cold verification of persistent computer packs

This commit is contained in:
Dotta committed 2026-10-10 22:53:17 -05:00
1 parent b48ac8412f
commit 2d8030417c
3 files changed
+64 -6

No files matched your search

@@ -42,7 +42,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 { assertRemoteProviderPackVerificationResult, computerProviderPackCachePath, prepareComputerProviderPackCache, pinnedComputerProviderPack, computerAcpxLaunchProfileDigest, readPinnedComputerProviderMetadata, assertPinnedComputerProviderState } from "./remote-provider-pack-cache.js";
import { verifyRemoteProviderPackArtifacts, assertRemoteProviderPackVerificationResult, computerProviderPackCachePath, prepareComputerProviderPackCache, pinnedComputerProviderPack, computerAcpxLaunchProfileDigest, readPinnedComputerProviderMetadata, assertPinnedComputerProviderState } from "./remote-provider-pack-cache.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";
@@ -12311,14 +12311,13 @@ async function createRunnerdBackendWithinSessionClaim(
"const expectedPackages={acpx:manifest.payload.pins.acpx,'@agentclientprotocol/claude-agent-acp':manifest.payload.pins.claudeAcp,'@agentclientprotocol/codex-acp':manifest.payload.pins.codexAcp,'opencode-ai':manifest.payload.pins.opencode}",
"for(const [pkg,version] of Object.entries(expectedPackages))if(packageVersion(pkg)!==version)fail('package_version_mismatch')",
].join(";");
const verified = await remoteCommandRunner.execute({
await verifyRemoteProviderPackArtifacts({
runner: remoteCommandRunner,
command: providerNodeCommand,
args: ["-e", verifyScript, packRoot, expected],
cwd: remoteTarget.remoteCwd,
bypassSession: true,
timeoutMs: 30_000,
persistentComputer: remoteTarget.transport === "computer",
});
assertRemoteProviderPackVerificationResult(verified);
const opencodeCommand = posix.join(
packRoot,
expectedProviderPackManifest.payload.artifacts.opencodeCommand.path,
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { CommandManagedRuntimeRunner } from "@paperclipai/adapter-utils/command-managed-runtime";
import { RemoteProviderPackVerificationError, assertRemoteProviderPackVerificationResult, computerProviderPackCachePath, prepareComputerProviderPackCache, pinnedComputerProviderPack, computerAcpxLaunchProfileDigest, readPinnedComputerProviderMetadata, assertPinnedComputerProviderState } from "./remote-provider-pack-cache.js";
import { verifyRemoteProviderPackArtifacts, RemoteProviderPackVerificationError, assertRemoteProviderPackVerificationResult, computerProviderPackCachePath, prepareComputerProviderPackCache, pinnedComputerProviderPack, computerAcpxLaunchProfileDigest, readPinnedComputerProviderMetadata, assertPinnedComputerProviderState } from "./remote-provider-pack-cache.js";
const roots: string[] = [];
const digest = `sha256:${"a".repeat(64)}`;
@@ -267,3 +267,45 @@ describe("remote provider verification result classification", () => {
.toThrow(expect.objectContaining({ kind: "incompatible", reason }));
});
});
describe("provider pack full-tree verification deadline", () => {
afterEach(() => vi.useRealTimers());
it.each([true, false])("a 45-second cold hash walk respects the transport budget (computer=%s)", async persistentComputer => {
vi.useFakeTimers();
const execute = vi.fn<CommandManagedRuntimeRunner["execute"]>(input => new Promise(resolve => {
const timedOut = input.timeoutMs! < 45_000;
setTimeout(() => resolve({ exitCode: timedOut ? null : 0, timedOut, signal: null,
stdout: "", stderr: "", pid: null, startedAt: null }), Math.min(input.timeoutMs!, 45_000));
}));
const verification = verifyRemoteProviderPackArtifacts({ runner: { execute }, command: "/pack/node",
args: ["-e", "hash all artifacts"], cwd: "/agent", persistentComputer });
const assertion = persistentComputer ? expect(verification).resolves.toBeUndefined()
: expect(verification).rejects.toMatchObject({ kind: "unavailable", reason: "timeout" });
await vi.advanceTimersByTimeAsync(45_000);
await assertion;
expect(execute).toHaveBeenCalledTimes(1);
});
it.each(["deadline", "owner-retired"])("fails closed without retrying when %s interrupts a computer hash walk", async cause => {
vi.useFakeTimers();
const retired = new Error("owner retired during verification");
let rejectCommand!: (error: Error) => void;
let commandTimer!: ReturnType<typeof setTimeout>;
const execute = vi.fn<CommandManagedRuntimeRunner["execute"]>(input => new Promise((resolve, reject) => {
rejectCommand = reject;
commandTimer = setTimeout(() => resolve({ exitCode: null, timedOut: true, signal: null,
stdout: "", stderr: "", pid: null, startedAt: null }), input.timeoutMs);
}));
const verification = verifyRemoteProviderPackArtifacts({ runner: { execute }, command: "/pack/node",
args: ["-e", "hash all artifacts"], cwd: "/agent", persistentComputer: true });
const assertion = expect(verification).rejects.toMatchObject({ kind: "unavailable",
reason: cause === "deadline" ? "timeout" : "transport_error", ...(cause === "owner-retired" ? { cause: retired } : {}) });
if (cause === "owner-retired") {
await vi.advanceTimersByTimeAsync(1_000);
clearTimeout(commandTimer);
rejectCommand(retired);
} else await vi.advanceTimersByTimeAsync(120_000);
await assertion;
expect(execute).toHaveBeenCalledTimes(1);
expect(vi.getTimerCount()).toBe(0);
});
});
@@ -37,6 +37,23 @@ export function assertRemoteProviderPackVerificationResult(
throw new RemoteProviderPackVerificationError("unavailable", "command_failed", result);
}
/** Persistent computers can restore a large pack lazily. Give the complete
* hash walk a bounded cold-read budget; the owner's runner still fences and
* cancels the command. Version probes keep their separate short deadline. */
export async function verifyRemoteProviderPackArtifacts(input: {
runner: CommandManagedRuntimeRunner; command: string; args: string[]; cwd: string;
persistentComputer: boolean;
}): Promise<void> {
let result;
try {
result = await input.runner.execute({ command: input.command, args: input.args, cwd: input.cwd,
bypassSession: true, timeoutMs: input.persistentComputer ? 120_000 : 30_000 });
} catch (cause) {
throw new RemoteProviderPackVerificationError("unavailable", "transport_error", cause);
}
assertRemoteProviderPackVerificationResult(result);
}
/** The controller's retained attachment template pins content-addressed paths.
* Remote manifests may supply bytes, but cannot choose a new code authority. */
export function pinnedComputerProviderPack(input: {