mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +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
+2
-1
@@ -1531,7 +1531,8 @@ CLI paths that use stdin avoid that separate limit for the wake prompt.
|
||||
Paperclip Runner offers native Codex, OpenCode, and **ACPX Claude**. New
|
||||
Claude Code agents default to ACPX Claude on Apple Silicon, Intel Mac, Linux x64
|
||||
and Linux ARM64 with supported glibc. Unsupported or unqualified targets retain
|
||||
Legacy. The runner override is in Advanced. Ordinary Claude models default to `claude-opus-5-5`; explicit models
|
||||
Legacy. Darwin SSH keeps an automatic Legacy default; explicit native execution
|
||||
requires matching remote assets. The runner override is in Advanced. Ordinary Claude models default to `claude-opus-5-5`; explicit models
|
||||
and provider routing remain authoritative. Existing agents keep their saved runner. Converting an existing Claude, Codex, or OpenCode agent selects its
|
||||
corresponding provider, preserves compatible models, credentials, workspace,
|
||||
and instructions, and resets execution sessions while retaining run history.
|
||||
|
||||
@@ -24,7 +24,8 @@ native hires.
|
||||
Creation requests can use `runner: auto | paperclip | legacy`; the server resolves
|
||||
and persists execution configuration. Claude Auto selects the native ACPX profile
|
||||
on qualified macOS and GNU/Linux x64/ARM64 targets; unknown environment
|
||||
qualification remains Legacy. Saved runner/model/provider overrides survive edits
|
||||
qualification remains Legacy. Darwin SSH defaults to Legacy while preserving
|
||||
explicit native assets and saved execution. Saved runner/model/provider overrides survive edits
|
||||
and approval activation. This selection does not grant connection access or
|
||||
bypass hiring authorization. Source: [runner resolver](../../server/src/services/agent-runner-selection.ts).
|
||||
|
||||
|
||||
@@ -46,7 +46,9 @@ and ordinary model `claude-opus-5-5`. The Advanced runner selector can explicitl
|
||||
choose Legacy. Keep explicit model/account choices and saved runners on edits.
|
||||
Verify useful output and a follow-up after a controller restart; setup success
|
||||
alone does not establish session continuity. Unsupported architectures/libc and unknown environment qualification retain
|
||||
Legacy. Linux ARM64 cloud-image production remains deferred.
|
||||
Legacy. Darwin SSH also defaults to Legacy until automatic matching assets are
|
||||
qualified; explicit matching native assets and saved native configurations remain
|
||||
supported. Linux ARM64 cloud-image production remains deferred.
|
||||
|
||||
The current hiring wizard assigns the first hire CEO and subsequent hires
|
||||
General reporting to the CEO. Change manager/role afterward through supported
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -193,7 +193,8 @@ Choose the underlying harness in `adapterType`, such as `codex_local` or
|
||||
`claude_local`. New Codex agents use Paperclip Runner automatically on supported
|
||||
execution targets. New Claude Code agents use Paperclip Runner on Apple Silicon,
|
||||
Intel Mac, Linux x64 and Linux ARM64 with supported glibc. Unknown or unsupported
|
||||
targets retain Legacy. The native Claude profile is
|
||||
targets retain Legacy. Darwin SSH also defaults to Legacy; native execution
|
||||
requires an explicit runner choice and matching remote assets. The native Claude profile is
|
||||
`provider: acpx`, `acpxAgent: claude`, with ordinary model `claude-opus-5-5`.
|
||||
Explicit models and existing saved runners remain intact. The saved response
|
||||
may therefore have `adapterType: paperclip_runner` and a resolved provider;
|
||||
|
||||
Reference in new issue
Block a user