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:
DottaandPaperclip committed 2026-10-12 16:46:40 -05:00
1 parent e052378572
commit bc5e9f1414
9 files changed
+109 -56

No files matched your search

+2 -1
View File
@@ -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.
+2 -1
View File
@@ -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).
+3 -1
View File
@@ -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;
}
+2 -1
View File
@@ -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;