feat: default Claude to the native runner on Apple Silicon

Carry Claude effort through versioned native admission and recovery. Preserve existing connections and saved runner choices, and price Opus 5.5 API receipts.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-12 10:48:21 -05:00
1 parent 058af8f1b6
commit 6fd56c445d
77 files changed
+909 -296

No files matched your search

@@ -72,7 +72,7 @@ describe("host ACPX qualification admission", () => {
}
});
it("preserves product defaults separately from qualification examples", () => {
for (const [acpxAgent, model] of [["claude", "claude-sonnet-5"], ["grok", "grok-4.7"]]) {
for (const [acpxAgent, model] of [["claude", "claude-opus-5-5"], ["grok", "grok-4.7"]]) {
expect(resolvePaperclipRunnerProviderProfile({ provider: "acpx", acpxAgent }))
.toMatchObject({ acpxAgent, model });
}
@@ -8,6 +8,7 @@ import type {
NativeCodexApprovalPolicy,
NativeExecutionInputV5,
NativeExecutionInputV6,
NativeExecutionInputV7,
DotBindingSnapshot,
NativeCompletionSource,
NativeInteractionResponseEnvelope,
@@ -80,6 +81,7 @@ export interface BuildNativeExecutionInput {
acpxPermissionMode?: NativeAcpxPermissionMode;
acpxSessionMode?: "agent" | "plan" | "ask";
piThinkingLevel?: "off" | "low" | "high" | "max";
claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max";
model?: string | null;
managedProfile?: Extract<
NativeExecutionInputV5["provider"],
@@ -109,9 +111,9 @@ export interface BuildNativeExecutionInput {
runtimeContext: NativeRuntimeContextSnapshot;
}
export function buildNativeExecutionInput(input: BuildNativeExecutionInput & { provider: "openai_dot"; dotBinding: DotBindingSnapshot }): NativeExecutionInputV6;
export function buildNativeExecutionInput(input: BuildNativeExecutionInput & { provider?: Exclude<BuildNativeExecutionInput["provider"], "openai_dot"> }): NativeExecutionInputV5;
export function buildNativeExecutionInput(input: BuildNativeExecutionInput): NativeExecutionInputV5 | NativeExecutionInputV6;
export function buildNativeExecutionInput(input: BuildNativeExecutionInput): NativeExecutionInputV5 | NativeExecutionInputV6 {
export function buildNativeExecutionInput(input: BuildNativeExecutionInput & { provider?: Exclude<BuildNativeExecutionInput["provider"], "openai_dot"> }): NativeExecutionInputV5 | NativeExecutionInputV7;
export function buildNativeExecutionInput(input: BuildNativeExecutionInput): NativeExecutionInputV5 | NativeExecutionInputV6 | NativeExecutionInputV7;
export function buildNativeExecutionInput(input: BuildNativeExecutionInput): NativeExecutionInputV5 | NativeExecutionInputV6 | NativeExecutionInputV7 {
if (input.issue.workMode !== "standard" && input.issue.workMode !== "planning" && input.issue.workMode !== "ask") {
throw new Error("native_execution_input_invalid: issue work mode must be standard, planning, or ask");
}
@@ -211,7 +213,7 @@ export function buildNativeExecutionInput(input: BuildNativeExecutionInput): Nat
? verifiedCompletionSources(input.turnContext, input.completionContract.sources ?? [])
: [];
const prepared = {
schema: "paperclip.native-execution-input.v5",
schema: input.claudeEffort === undefined ? "paperclip.native-execution-input.v5" : "paperclip.native-execution-input.v7",
...((input.initialCommunicationGuidance || input.freshSessionHandoff) ? {
initialCommunicationGuidance: [input.initialCommunicationGuidance, input.freshSessionHandoff].filter(Boolean).join("\n\n"),
} : {}),
@@ -290,6 +292,7 @@ export function buildNativeExecutionInput(input: BuildNativeExecutionInput): Nat
permissionMode: input.acpxPermissionMode ?? "approve-all",
...(mode === undefined ? {} : { mode }),
...(piThinkingLevel === undefined ? {} : { piThinkingLevel }),
...(input.claudeEffort === undefined ? {} : { claudeEffort: input.claudeEffort }),
profile: {
driverKind: acpxProfile!.driverKind,
protocolVersion: acpxProfile!.protocolVersion,
@@ -334,7 +337,7 @@ export function buildNativeExecutionInput(input: BuildNativeExecutionInput): Nat
schema: "paperclip.native-execution-input.v6", provider: { kind: "openai_dot", model: null, binding: input.dotBinding },
workspace: { access: "none", cwd: null, repoUrl: null, repoRef: null, branchName: null },
session: { ...prepared.session, driverKind: "openai_dot_mcp", lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null } }
} : prepared) as NativeExecutionInputV5 | NativeExecutionInputV6;
} : prepared) as NativeExecutionInputV5 | NativeExecutionInputV6 | NativeExecutionInputV7;
}
@@ -5625,6 +5625,7 @@ export function providerSessionIdentityFromDurableProviderState(input: {
identity.permissionMode !== input.execution.provider.permissionMode ||
!acpxRecoveryModeMatches(input.execution.provider, descriptor.mode, identity.mode) ||
!acpxRecoveryPiThinkingMatches(input.execution.provider, descriptor.piThinkingLevel, identity.piThinkingLevel) ||
!acpxRecoveryClaudeEffortMatches(input.execution.provider, descriptor.claudeEffort, identity.claudeEffort) ||
!["approve-all", "approve-paperclip", "approve-reads", "deny-all"].includes(
String(identity.permissionMode),
) ||
@@ -5732,6 +5733,13 @@ function acpxRecoveryModeMatches(
&& observedModes.every(mode => mode === expected);
}
function acpxRecoveryClaudeEffortMatches(provider: unknown, projected: unknown, observed: unknown): boolean {
const config = record(provider);
const expected = config.claudeEffort;
if (config.kind !== "acpx" || config.agent !== "claude") return expected === undefined && projected === undefined && observed === undefined;
return expected === projected && expected === observed && (expected === undefined || ["low", "medium", "high", "xhigh", "max"].includes(String(expected)));
}
function acpxRecoveryPiThinkingMatches(provider: NativeExecutionInput["provider"], ...observed: unknown[]): boolean {
const expected = record(provider).piThinkingLevel;
if (provider.kind === "acpx" && provider.agent === "pi") return ["off", "low", "high", "max"].includes(String(expected)) && observed.every(value => value === expected);
@@ -5749,6 +5757,7 @@ export function providerSessionIdentityTransitionIsAllowed(input: {
record(record(input.current).providerSessionIdentity).mode,
)) return false;
if (!acpxRecoveryPiThinkingMatches(input.execution.provider, record(record(input.previous).providerSessionIdentity).piThinkingLevel, record(record(input.current).providerSessionIdentity).piThinkingLevel)) return false;
if (!acpxRecoveryClaudeEffortMatches(input.execution.provider, record(record(input.previous).providerSessionIdentity).claudeEffort, record(record(input.current).providerSessionIdentity).claudeEffort)) return false;
if (canonicalJson(input.previous) === canonicalJson(input.current)) {
return true;
}
@@ -12872,9 +12881,11 @@ async function createRunnerdBackendWithinSessionClaim(
acpxPermissionMode: input.execution.provider.permissionMode,
acpxMode: input.execution.provider.mode,
piThinkingLevel: input.execution.provider.piThinkingLevel,
claudeEffort: "claudeEffort" in input.execution.provider ? input.execution.provider.claudeEffort : undefined,
acpxPermissionModePinned:
input.execution.schema === "paperclip.native-execution-input.v4" ||
input.execution.schema === "paperclip.native-execution-input.v5",
input.execution.schema === "paperclip.native-execution-input.v5" ||
input.execution.schema === "paperclip.native-execution-input.v7",
acpxRuntimeDirectory: remoteRunnerFilesystemRoot
? posix.join(remoteRunnerFilesystemRoot, "acpx")
: resolve(
@@ -1,4 +1,4 @@
import { resolveQualifiedAcpxProfile } from "../../vendor/paperclip-runner/index.js";
import { resolveClaudeEffort, resolveQualifiedAcpxProfile } from "../../vendor/paperclip-runner/index.js";
import {
isPaperclipRunnerProvider,
PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES,
@@ -26,7 +26,7 @@ export const CLAUDE_MANAGED_BETA_VERSION = "managed-agents-2026-04-01" as const;
export const DEFAULT_ACPX_RUNNER_MODELS = {
grok: "grok-4.7",
claude: "claude-sonnet-5",
claude: "claude-opus-5-5",
// These profiles require explicit configuration. Admission belongs to the runner.
codex: null,
cursor: null,
@@ -130,6 +130,7 @@ export type PaperclipRunnerNativeProviderInput =
acpxPermissionMode: "approve-all" | "approve-paperclip" | "approve-reads" | "deny-all";
acpxSessionMode?: "agent" | "plan" | "ask";
piThinkingLevel?: "off" | "low" | "high" | "max";
claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max";
};
export class PaperclipRunnerProviderProfileError extends Error {
@@ -165,9 +166,9 @@ export function validatePaperclipRunnerDotConfig(config: Record<string, unknown>
return bindingId;
}
/** A task may change a local Runner model (and Codex effort), but not provider identity. */
/** A task may change a local Runner model and supported effort, but not provider identity. */
export function projectPaperclipRunnerTaskConfig(
backend: "codex_app_server" | "opencode_server",
backend: "codex_app_server" | "opencode_server" | "acpx_runtime",
agentConfig: unknown,
taskOverrides: unknown,
managedModel?: string,
@@ -178,10 +179,11 @@ export function projectPaperclipRunnerTaskConfig(
// The server resolves a connection's model namespace after task overrides.
// Carry that model into durable input without allowing routing to change the harness.
const model = optionalString(managedModel) ?? optionalString(task.model);
const effortKey = backend === "codex_app_server"
const supportsEffort = backend === "codex_app_server" || (backend === "acpx_runtime" && base.acpxAgent === "claude");
const effortKey = supportsEffort
? ["modelReasoningEffort", "reasoningEffort", "effort"].find((key) => key in task)
: undefined;
if (backend === "codex_app_server" && ((model !== null && model !== optionalString(base.model)) || effortKey !== undefined)) {
if (supportsEffort && ((model !== null && model !== optionalString(base.model)) || effortKey !== undefined)) {
delete config.modelReasoningEffort;
delete config.reasoningEffort;
delete config.effort;
@@ -352,6 +354,17 @@ export function assertAgentCoreProfileRecoveryBinding(input: {
}
}
export function claudeRunnerUnsupportedSettings(config: Record<string, unknown>): string[] {
const unsupported = ["chrome", "agentCommand", "stateDir", "warmHandleIdleMs", "extraArgs", "args", "filesystemScope", "filesystemExtraPaths", "filesystemSandboxCommand", "networkScope", "networkAllowlist", "maxTurnsPerRun"]
.filter(key => config[key] !== undefined && config[key] !== false && config[key] !== "" && config[key] !== 0 && !(Array.isArray(config[key]) && config[key].length === 0));
if (config.command && config.command !== "claude") unsupported.push("command");
if (config.mode && config.mode !== "persistent") unsupported.push("mode");
if (config.engine && !["auto", "cli", "acp"].includes(String(config.engine))) unsupported.push("engine");
if (config.nonInteractivePermissions && config.nonInteractivePermissions !== "deny") unsupported.push("nonInteractivePermissions");
if (config.dangerouslySkipPermissions === false) unsupported.push("dangerouslySkipPermissions");
return unsupported;
}
/**
* Resolve the immutable provider identity used for a fresh Paperclip Runner
* selection. The persisted adapterConfig is the authority; runtimeConfig is
@@ -374,6 +387,11 @@ export function resolvePaperclipRunnerProviderProfile(
const bindingId = validatePaperclipRunnerDotConfig(config)!;
return { provider: "openai_dot", backend: "openai_dot_mcp", model: null, dotBindingId: bindingId };
}
if (candidate === "acpx" && config.acpxAgent === "claude") {
const unsupported = claudeRunnerUnsupportedSettings(config);
if (unsupported.length) throw new PaperclipRunnerProviderProfileError("agent_runner_config_incompatible",
`Paperclip Runner cannot honor these settings: ${unsupported.join(", ")}. Remove them or select Legacy runner in Advanced.`);
}
assertPermissionMode(candidate, config);
try {
resolvePaperclipRunnerCursorMode(candidate, config.acpxAgent, config.acpxSessionMode);
@@ -383,6 +401,9 @@ export function resolvePaperclipRunnerProviderProfile(
error instanceof Error ? error.message : "Invalid Cursor session mode",
);
}
try { if (candidate === "acpx" && config.acpxAgent === "claude") resolveClaudeEffort("claude", config.modelReasoningEffort ?? config.reasoningEffort ?? config.effort); } catch (error) {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_claude_effort_invalid", error instanceof Error ? error.message : "Invalid Claude effort");
}
try { resolvePaperclipRunnerPiThinkingLevel(candidate, config.acpxAgent, config.piThinkingLevel); } catch (error) {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_pi_thinking_invalid", error instanceof Error ? error.message : "Invalid Pi thinking level");
}
@@ -573,6 +594,7 @@ export function resolvePaperclipRunnerNativeProviderInput(input: {
provider: "acpx",
model: profile.model,
acpxAgent: profile.acpxAgent,
...(profile.acpxAgent === "claude" ? { claudeEffort: resolveClaudeEffort("claude", config.modelReasoningEffort ?? config.reasoningEffort ?? config.effort) } : {}),
...(profile.acpxAgent === "pi" ? { piThinkingLevel: resolvePaperclipRunnerPiThinkingLevel("acpx", "pi", config.piThinkingLevel) } : {}),
...(profile.acpxAgent === "cursor" ? {
acpxSessionMode: resolvePaperclipRunnerCursorMode("acpx", "cursor", config.acpxSessionMode),
@@ -50,6 +50,11 @@ describe("resolveNativeRuntimeMode", () => {
})).toMatchObject({ kind: "native", profile: { backend: "codex_app_server" } });
});
it("admits native Claude without enabling other ACPX providers", () => {
expect(resolveNativeRuntimeMode({ ...eligible, enabled: false, adapterConfig: { provider: "acpx", acpxAgent: "claude" } })).toMatchObject({ kind: "native", profile: { backend: "acpx_runtime" } });
expect(() => resolveNativeRuntimeMode({ ...eligible, enabled: false, adapterConfig: { provider: "acpx", acpxAgent: "grok" } })).toThrow(expect.objectContaining({ code: "paperclip_runner_rollout_disabled" }));
});
it("still rejects experimental providers while the rollout flag is disabled", () => {
expect(() => resolveNativeRuntimeMode({
...eligible,
@@ -130,7 +130,7 @@ export function resolveNativeRuntimeMode(input: {
// enabling Dot does not opt in any of those providers.
if (runnerProfile.provider === "openai_dot") {
if (input.dotEnabled !== true) throw ineligible("paperclip_runner_dot_disabled", "Enable OpenAI Dot and Assistant connections (MCP) in experimental settings before assigning new work.");
} else if (runnerProfile.provider !== "codex" && !input.enabled) {
} else if (runnerProfile.provider !== "codex" && !(runnerProfile.provider === "acpx" && runnerProfile.acpxAgent === "claude") && !input.enabled) {
throw ineligible(
"paperclip_runner_rollout_disabled",
"Paperclip Runner is experimental and disabled on this instance.",