mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-14 08:26:46 +02:00
fix: allow bounded cold verification of persistent computer packs
This commit is contained in:
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: {
|
||||
|
||||
Reference in new issue
Block a user