From 2d8030417ce3ab06d2fa73f67cbf00fff474ce16 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 10 Oct 2026 22:50:03 -0500 Subject: [PATCH] fix: allow bounded cold verification of persistent computer packs --- .../native-runtime/native-session-executor.ts | 9 ++-- .../remote-provider-pack-cache.test.ts | 44 ++++++++++++++++++- .../remote-provider-pack-cache.ts | 17 +++++++ 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index a22f0aa0ab..b4130a56c7 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -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, diff --git a/server/src/services/native-runtime/remote-provider-pack-cache.test.ts b/server/src/services/native-runtime/remote-provider-pack-cache.test.ts index 0665266083..3b7ad01276 100644 --- a/server/src/services/native-runtime/remote-provider-pack-cache.test.ts +++ b/server/src/services/native-runtime/remote-provider-pack-cache.test.ts @@ -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(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; + const execute = vi.fn(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); + }); +}); diff --git a/server/src/services/native-runtime/remote-provider-pack-cache.ts b/server/src/services/native-runtime/remote-provider-pack-cache.ts index 70d51432e8..c7ae3c89dc 100644 --- a/server/src/services/native-runtime/remote-provider-pack-cache.ts +++ b/server/src/services/native-runtime/remote-provider-pack-cache.ts @@ -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 { + 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: {