mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-05 03:51:54 +01:00
fix(claude): verify all installed packs and gate Darwin SSH defaults
Try each standard preinstalled pack with the same integrity verifier. Keep Darwin SSH Auto on Legacy while preserving explicit assets and saved native execution. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
e052378572
commit
bc5e9f1414
9 files changed
+109
-56
No files matched your search
@@ -215,6 +215,17 @@ describe("Claude runner defaults", () => {
|
||||
expect(resolveNewAgentRunner({ adapterType: "claude_local", target, runner: "legacy", adapterConfig: { model: "custom", dangerouslySkipPermissions: true } }))
|
||||
.toEqual({ adapterType: "claude_local", adapterConfig: { model: "custom", dangerouslySkipPermissions: true } });
|
||||
});
|
||||
it.each(["x64", "arm64"])("keeps Darwin SSH %s automatic selection Legacy while preserving explicit assets", architecture => {
|
||||
const target = { driver: "ssh", platform: "darwin", architecture };
|
||||
expect(agentRunnerAvailability("claude_local", target)).toEqual({ supportedRunners: ["paperclip", "legacy"], defaultRunner: "legacy" });
|
||||
const config = { model: "custom-model", dangerouslySkipPermissions: false };
|
||||
expect(resolveNewAgentRunner({ adapterType: "claude_local", target, adapterConfig: config })).toEqual({ adapterType: "claude_local", adapterConfig: config });
|
||||
const assets = { runnerRemoteProviderPackPath: "/operator/mac-pack", runnerRemoteBinaryPath: "/operator/mac-daemon" };
|
||||
expect(resolveNewAgentRunner({ adapterType: "claude_local", target, runner: "paperclip", adapterConfig: assets }))
|
||||
.toMatchObject({ adapterType: "paperclip_runner", adapterConfig: { ...assets, provider: "acpx", acpxAgent: "claude" } });
|
||||
const saved = { adapterType: "paperclip_runner", adapterConfig: { ...assets, provider: "acpx", acpxAgent: "claude" }, target };
|
||||
expect(resolveNewAgentRunner(saved)).toEqual({ adapterType: saved.adapterType, adapterConfig: saved.adapterConfig });
|
||||
});
|
||||
it.each(["x64", "arm64"])("keeps incompatible Linux Claude %s targets legacy", architecture => {
|
||||
for (const libc of ["musl", "unknown"] as const) {
|
||||
expect(agentRunnerAvailability("claude_local", { driver: "local", platform: "linux", architecture, libc }).defaultRunner).toBe("legacy");
|
||||
|
||||
@@ -33,7 +33,10 @@ export function agentRunnerAvailability(harness: string, target: RunnerTarget =
|
||||
&& (harness === "claude_local"
|
||||
? !!target.platform && paperclipRunnerSupportsPlatform(harness, target.platform, target.architecture ?? "", target.libc, target.glibcVersion)
|
||||
: !target.platform || paperclipRunnerSupportsPlatform(harness, target.platform, target.architecture ?? "", target.libc, target.glibcVersion));
|
||||
return { supportedRunners: supported ? ["paperclip", "legacy"] : ["legacy"], defaultRunner: supported ? "paperclip" : "legacy" };
|
||||
// Darwin SSH execution needs operator-provided matching assets. It remains
|
||||
// an explicit choice until automatic Darwin remote packs are qualified.
|
||||
const automatic = supported && !(harness === "claude_local" && target.driver === "ssh" && target.platform === "darwin");
|
||||
return { supportedRunners: supported ? ["paperclip", "legacy"] : ["legacy"], defaultRunner: automatic ? "paperclip" : "legacy" };
|
||||
}
|
||||
|
||||
function incompatible(fields: string[]): never {
|
||||
@@ -105,7 +108,7 @@ export function resolveNewAgentRunner(input: AgentRunnerSelection): { adapterTyp
|
||||
return { adapterType: "codex_local", adapterConfig: { ...config, dangerouslyBypassApprovalsAndSandbox: config.dangerouslyBypassApprovalsAndSandbox ?? true } };
|
||||
}
|
||||
if (native && harness !== "codex_local" && choice === "paperclip") return { adapterType, adapterConfig: normalizeLegacyRunnerProvider(config) };
|
||||
if (availability.defaultRunner !== "paperclip") throw unprocessable("Paperclip Runner is unavailable for this harness and environment. Select Legacy runner in Advanced.", { code: "agent_runner_unavailable" });
|
||||
if (!availability.supportedRunners.includes("paperclip")) throw unprocessable("Paperclip Runner is unavailable for this harness and environment. Select Legacy runner in Advanced.", { code: "agent_runner_unavailable" });
|
||||
if (native) return { adapterType, adapterConfig: normalizeLegacyRunnerProvider(config) };
|
||||
if (harness === "claude_local") {
|
||||
config = resolveClaudeRunnerConfig(config);
|
||||
|
||||
@@ -30,7 +30,7 @@ import { copyBackCodexAuth } from "@paperclipai/adapter-codex-local/server";
|
||||
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 { prepareVerifiedRemoteProviderPack, useVerifiedPreinstalledRemoteProviderPack } 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";
|
||||
@@ -11734,19 +11734,20 @@ async function createRunnerdBackendWithinSessionClaim(
|
||||
};
|
||||
|
||||
const discoverPreinstalledProviderPack = async () => {
|
||||
if (!remoteTarget || !remoteCommandRunner) return null;
|
||||
if (!remoteTarget || !remoteCommandRunner) return [];
|
||||
const result = await remoteCommandRunner.execute({
|
||||
command: "sh",
|
||||
args: [
|
||||
"-c",
|
||||
'for candidate in /opt/paperclip-runner/provider-pack "$HOME/.local/share/paperclip-runner/provider-pack"; do if [ -f "$candidate/provider-pack.json" ]; then printf \'%s\\n\' "$candidate"; break; fi; done',
|
||||
'for candidate in /opt/paperclip-runner/provider-pack "$HOME/.local/share/paperclip-runner/provider-pack"; do if [ -f "$candidate/provider-pack.json" ]; then printf \'%s\\n\' "$candidate"; fi; done',
|
||||
],
|
||||
cwd: remoteTarget.remoteCwd,
|
||||
bypassSession: true,
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
if (result.exitCode !== 0 || result.timedOut) return null;
|
||||
return parseRemoteExecutableCandidate(result.stdout);
|
||||
if (result.exitCode !== 0 || result.timedOut) return [];
|
||||
return result.stdout.split(/\r?\n/).map(parseRemoteExecutableCandidate)
|
||||
.filter((candidate): candidate is string => candidate !== null);
|
||||
};
|
||||
|
||||
const remoteNativeArtifacts = remoteTarget && remoteCommandRunner && remoteBinary && remoteRuntimeRoot
|
||||
@@ -11796,52 +11797,47 @@ async function createRunnerdBackendWithinSessionClaim(
|
||||
"provider_pack.verify",
|
||||
() => verifyRemoteProviderPack(stagedRemoteProviderPackRoot),
|
||||
),
|
||||
usePreinstalled: async () => {
|
||||
let preinstalledProviderPack = await discoverPreinstalledProviderPack();
|
||||
if (preinstalledProviderPack) {
|
||||
try {
|
||||
await measureNativeRunnerSpan(
|
||||
input.trace,
|
||||
"provider_pack.verify_preinstalled",
|
||||
() => verifyRemoteProviderPack(preinstalledProviderPack!),
|
||||
usePreinstalled: async () => useVerifiedPreinstalledRemoteProviderPack({
|
||||
candidates: await discoverPreinstalledProviderPack(),
|
||||
verifyAndLink: async (preinstalledProviderPack) => {
|
||||
await measureNativeRunnerSpan(
|
||||
input.trace,
|
||||
"provider_pack.verify_preinstalled",
|
||||
() => verifyRemoteProviderPack(preinstalledProviderPack),
|
||||
);
|
||||
const escapedSource = preinstalledProviderPack.replaceAll(
|
||||
"'",
|
||||
"'\\''",
|
||||
);
|
||||
const escapedTarget = stagedRemoteProviderPackRoot.replaceAll(
|
||||
"'",
|
||||
"'\\''",
|
||||
);
|
||||
const escapedParent = posix
|
||||
.dirname(stagedRemoteProviderPackRoot)
|
||||
.replaceAll("'", "'\\''");
|
||||
const linked = await remoteCommandRunner.execute({
|
||||
command: "sh",
|
||||
args: [
|
||||
"-c",
|
||||
`umask 077; mkdir -p '${escapedParent}' && rm -rf '${escapedTarget}' && ln -s '${escapedSource}' '${escapedTarget}'`,
|
||||
],
|
||||
cwd: remoteTarget.remoteCwd,
|
||||
bypassSession: true,
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
if (linked.exitCode !== 0 || linked.timedOut) {
|
||||
throw new Error(
|
||||
"runner_remote_provider_artifact_incompatible: preinstalled provider pack could not be linked",
|
||||
);
|
||||
const escapedSource = preinstalledProviderPack.replaceAll(
|
||||
"'",
|
||||
"'\\''",
|
||||
);
|
||||
const escapedTarget = stagedRemoteProviderPackRoot.replaceAll(
|
||||
"'",
|
||||
"'\\''",
|
||||
);
|
||||
const escapedParent = posix
|
||||
.dirname(stagedRemoteProviderPackRoot)
|
||||
.replaceAll("'", "'\\''");
|
||||
const linked = await remoteCommandRunner.execute({
|
||||
command: "sh",
|
||||
args: [
|
||||
"-c",
|
||||
`umask 077; mkdir -p '${escapedParent}' && rm -rf '${escapedTarget}' && ln -s '${escapedSource}' '${escapedTarget}'`,
|
||||
],
|
||||
cwd: remoteTarget.remoteCwd,
|
||||
bypassSession: true,
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
if (linked.exitCode !== 0 || linked.timedOut) {
|
||||
throw new Error(
|
||||
"runner_remote_provider_artifact_incompatible: preinstalled provider pack could not be linked",
|
||||
);
|
||||
}
|
||||
activeRemoteProviderPackRoot = stagedRemoteProviderPackRoot;
|
||||
await input.onLog?.(
|
||||
"stderr",
|
||||
"[paperclip-runner] using manifest-matched provider pack from the sandbox image\n",
|
||||
);
|
||||
} catch {
|
||||
preinstalledProviderPack = null;
|
||||
}
|
||||
}
|
||||
return preinstalledProviderPack !== null;
|
||||
},
|
||||
activeRemoteProviderPackRoot = stagedRemoteProviderPackRoot;
|
||||
await input.onLog?.(
|
||||
"stderr",
|
||||
"[paperclip-runner] using manifest-matched preinstalled provider pack\n",
|
||||
);
|
||||
},
|
||||
}),
|
||||
stageAndVerify: async () => {
|
||||
if (!configuredProviderPackRoot) {
|
||||
throw new Error("runner_remote_provider_artifact_incompatible: install the matching Paperclip package and Daytona image; the image provider pack did not match the bundled release identity");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { prepareVerifiedRemoteProviderPack } from "./remote-provider-pack.js";
|
||||
import { prepareVerifiedRemoteProviderPack, useVerifiedPreinstalledRemoteProviderPack } from "./remote-provider-pack.js";
|
||||
|
||||
describe("remote provider pack reuse", () => {
|
||||
it("keeps a verified staged pack without consulting the image or uploading again", async () => {
|
||||
@@ -37,3 +37,25 @@ describe("remote provider pack reuse", () => {
|
||||
await expect(prepareVerifiedRemoteProviderPack(input)).resolves.toBe("uploaded");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("standard remote provider pack discovery", () => {
|
||||
const roots = ["/opt/paperclip-runner/provider-pack", "/home/operator/.local/share/paperclip-runner/provider-pack"];
|
||||
it.each(["old manifest", "modified artifact", "failed link"])("tries the matching home pack after %s rejects /opt", async reason => {
|
||||
const verifyAndLink = vi.fn(async (root: string) => {
|
||||
if (root === roots[0]) throw new Error(reason);
|
||||
});
|
||||
await expect(useVerifiedPreinstalledRemoteProviderPack({ candidates: roots, verifyAndLink })).resolves.toBe(true);
|
||||
expect(verifyAndLink.mock.calls.map(([root]) => root)).toEqual(roots);
|
||||
});
|
||||
it("keeps the first matching pack without consulting another installation", async () => {
|
||||
const verifyAndLink = vi.fn(async () => {});
|
||||
await expect(useVerifiedPreinstalledRemoteProviderPack({ candidates: roots, verifyAndLink })).resolves.toBe(true);
|
||||
expect(verifyAndLink).toHaveBeenCalledExactlyOnceWith(roots[0]);
|
||||
});
|
||||
it("fails closed when every standard installation is incompatible", async () => {
|
||||
const verifyAndLink = vi.fn().mockRejectedValue(new Error("manifest mismatch"));
|
||||
await expect(useVerifiedPreinstalledRemoteProviderPack({ candidates: roots, verifyAndLink })).resolves.toBe(false);
|
||||
expect(verifyAndLink).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -14,3 +14,19 @@ export async function prepareVerifiedRemoteProviderPack(input: {
|
||||
await input.stageAndVerify();
|
||||
return "uploaded";
|
||||
}
|
||||
|
||||
/** A stale standard installation must not hide a matching later installation. */
|
||||
export async function useVerifiedPreinstalledRemoteProviderPack(input: {
|
||||
candidates: readonly string[];
|
||||
verifyAndLink: (root: string) => Promise<void>;
|
||||
}): Promise<boolean> {
|
||||
for (const candidate of input.candidates) {
|
||||
try {
|
||||
await input.verifyAndLink(candidate);
|
||||
return true;
|
||||
} catch {
|
||||
// Continue only to another installation with the same full verifier.
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Reference in new issue
Block a user