mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
Merge branch 'codex/claude-runner-default' into codex/claude-runner-platforms
* codex/claude-runner-default: Preserve Claude model bindings and effort across configuration changes
This commit is contained in:
commit
06c4f5e5a0
7 files changed
+146
-7
No files matched your search
@@ -174,3 +174,45 @@ The existing successful preview/data and all unknown-charge holds remain.
|
||||
No additional paid turns occurred during this review/packaging work; aggregate
|
||||
active company caps remain $42 of the $50 Claude ceiling. Remaining live checks
|
||||
use API keys per the user's steering. No Docker or Rust build ran locally.
|
||||
|
||||
## Installed API-key acceptance and latest review corrections
|
||||
|
||||
The installed 8d07b409 public package preview is now usable at
|
||||
[CLAA-2](http://127.0.0.1:3105/CLAA/issues/CLAA-2). Runs
|
||||
`11155946-5239-4127-9c67-ff23acf961c2` and
|
||||
`5f0e4191-9dd6-44ef-bc0a-0b66a910ec2c` succeeded using the saved API-key
|
||||
connection, Opus 5.5, admitted high effort, and normal installed daemon
|
||||
resolution with no runner override or ambient Claude credential. Accounting
|
||||
completed: estimates $0.197602000 and $0.136576400. Known estimates now total
|
||||
$1.331719; unknown-charge reservations and aggregate $42 caps remain intact.
|
||||
|
||||
The controller restarted between turns. Task, provider and backend session
|
||||
identities matched privately; IDs are redacted from the sanitized report
|
||||
`/private/tmp/claude-installed-api-acceptance.json`. Agent settings and the
|
||||
completed task persisted after reload with the native experiment disabled.
|
||||
Independent downloads matched v1 (168 bytes, SHA256
|
||||
`858f0438f048937dcd2e357a77f77e058582968215d33d5857637cd37fcfdb94`) and
|
||||
v2 (222 bytes, SHA256
|
||||
`4efd87a1f3a9d946d4278906bb779c4e3c0c83ec92faafdc29b9d6da28cada6d`);
|
||||
v1 remained unchanged. [v1 download](http://127.0.0.1:3105/api/attachments/4fa30ccf-f6bd-47a5-83f3-564c1b16a138/content?download=1),
|
||||
[v2 download](http://127.0.0.1:3105/api/attachments/af0f2c2e-a0f1-4138-980e-f93f5b48d2cf/content?download=1).
|
||||
Installed daemon SHA256:
|
||||
`36bbf4d878bfaa23e2fcec443e117cf04f46006ff8815c64855491b7bd3e440f`;
|
||||
sidecar SHA256:
|
||||
`ca6cad1f9b2fb2822d940d281a383360076c2ac03ea2ac875e64662f4719ae13`.
|
||||
This source predates subsequent review fixes; final package qualification and
|
||||
subscription follow-up are not claimed.
|
||||
|
||||
Latest behavior corrections preserve plain environment model bindings, reject
|
||||
unresolved default/model-routing bindings, retain native translation when an
|
||||
import explicitly switches to Legacy, and clear every effort alias on an
|
||||
incompatible model change. Selection/import/form controls: 271 passed; server
|
||||
noemit and token gates passed. Current historical instruction admission: 6
|
||||
passed. Platform `33077bed73` pins producer Node to the image's 24.21.0 version;
|
||||
its narrow policy exception and workflow controls passed (71 tests).
|
||||
|
||||
[Four-target campaign](https://github.com/paperclipai/paperclip/actions/runs/38293566808)
|
||||
and [cloud image build](https://github.com/paperclipai/paperclip/actions/runs/38293576503)
|
||||
retain source `557c3064a30779e86e1ae312eab3d1d42c3a8b8d`. Actual pack/image
|
||||
identity agreement remains required. Additional Claude defaults remain gated
|
||||
pending clean-install and execution evidence. No local Docker or Rust build.
|
||||
@@ -219,6 +219,21 @@ describe("Claude runner defaults", () => {
|
||||
expect(resolveNewAgentRunner(legacy)).toEqual({ adapterType: legacy.adapterType, adapterConfig: legacy.adapterConfig });
|
||||
expect(resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: { model: "custom" } }).adapterConfig.model).toBe("custom");
|
||||
});
|
||||
it.each(["claude-sonnet-5-5", "custom-model"])("preserves the plain environment model binding %s", model => {
|
||||
const env = { ANTHROPIC_MODEL: { type: "plain", value: model } };
|
||||
const native = resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: { env } });
|
||||
expect(native.adapterConfig.model).toBe(model);
|
||||
expect(native.adapterConfig.env).toEqual(env);
|
||||
});
|
||||
it.each(["ANTHROPIC_MODEL", "CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "ANTHROPIC_BEDROCK_BASE_URL"])("rejects unresolved %s before choosing a default", key => {
|
||||
const env = { [key]: { type: "secret_ref", secretId: "override" } };
|
||||
expect(() => resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: { env } })).toThrow(`env.${key}`);
|
||||
expect(resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: { env, model: "custom-model" } }).adapterConfig.model).toBe("custom-model");
|
||||
});
|
||||
it("does not pin the ordinary model over a plain provider-routing binding", () => {
|
||||
expect(() => resolveNewAgentRunner({ adapterType: "claude_local", target: mac,
|
||||
adapterConfig: { env: { CLAUDE_CODE_USE_BEDROCK: { type: "plain", value: "1" } } } })).toThrow("Legacy runner");
|
||||
});
|
||||
it.each(["permissionMode", "acpPermissionMode"])("translates the legacy Claude %s without granting broader permissions", key => {
|
||||
for (const mode of ["deny-all", "approve-reads", "approve-all"]) {
|
||||
const native = resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: { engine: "acp", [key]: mode } });
|
||||
|
||||
@@ -59,7 +59,17 @@ function resolveClaudeRunnerConfig(config: Record<string, unknown>): Record<stri
|
||||
if (unsupported.length) incompatible(unsupported);
|
||||
for (const key of ["engine", "command", "mode", "acpMode", "nonInteractivePermissions", "acpNonInteractivePermissions", "dangerouslySkipPermissions", "chrome", "maxTurnsPerRun", "effort", "reasoningEffort"]) delete translated[key];
|
||||
const env = config.env && typeof config.env === "object" ? config.env as Record<string, unknown> : {};
|
||||
const model = resolveClaudeModel(config.model, env);
|
||||
const modelEnv = { ...env };
|
||||
if (!(typeof config.model === "string" && config.model.trim())) {
|
||||
for (const key of ["ANTHROPIC_MODEL", "CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "ANTHROPIC_BEDROCK_BASE_URL"]) {
|
||||
const binding = env[key];
|
||||
if (binding === undefined || typeof binding === "string") continue;
|
||||
if (binding && typeof binding === "object" && "type" in binding && binding.type === "plain"
|
||||
&& "value" in binding && typeof binding.value === "string") modelEnv[key] = binding.value;
|
||||
else incompatible([`env.${key}`]);
|
||||
}
|
||||
}
|
||||
const model = resolveClaudeModel(config.model, modelEnv);
|
||||
if (!model) incompatible(["model"]);
|
||||
return { ...translated, ...(effort === undefined || effort === "" ? {} : { modelReasoningEffort: effort }), provider: "acpx", acpxAgent: "claude", model, acpxPermissionMode: translated.acpxPermissionMode ?? "approve-all", lifecycleMode: config.lifecycleMode ?? "per_turn" };
|
||||
}
|
||||
|
||||
@@ -1006,6 +1006,53 @@ describe("AgentConfigForm environment selector", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["modelReasoningEffort", "reasoningEffort", "effort"])("clears native Claude %s when the model does not support it", async key => {
|
||||
mockAgentsApi.adapterModels.mockResolvedValue([
|
||||
{ id: "claude-opus-5-5", label: "claude-opus-5-5" },
|
||||
{ id: "claude-haiku-4-5", label: "claude-haiku-4-5" },
|
||||
]);
|
||||
const result = await renderForm(
|
||||
[makeEnvironment({ id: "local-1", name: "Local", driver: "local" })],
|
||||
{
|
||||
adapterType: "paperclip_runner",
|
||||
adapterConfig: {
|
||||
provider: "acpx", acpxAgent: "claude",
|
||||
model: "claude-opus-5-5",
|
||||
[key]: "max",
|
||||
},
|
||||
},
|
||||
);
|
||||
roots.push(result.root);
|
||||
|
||||
const modelButton = Array.from(result.container.querySelectorAll("button"))
|
||||
.find((button) => button.textContent?.trim() === "claude-opus-5-5");
|
||||
expect(modelButton).not.toBeUndefined();
|
||||
await act(async () => {
|
||||
modelButton!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
await flushReact();
|
||||
|
||||
const astraOption = Array.from(document.body.querySelectorAll("button"))
|
||||
.find((button) => button.textContent?.trim() === "claude-haiku-4-5");
|
||||
expect(astraOption).not.toBeUndefined();
|
||||
await act(async () => {
|
||||
astraOption!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
await flushReact();
|
||||
|
||||
const saveButton = Array.from(result.container.querySelectorAll("button"))
|
||||
.find((button) => button.textContent?.trim() === "Save");
|
||||
expect(saveButton).not.toBeUndefined();
|
||||
await act(async () => {
|
||||
saveButton!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(result.onSave).toHaveBeenCalledWith({
|
||||
adapterConfig: { provider: "acpx", acpxAgent: "claude", model: "claude-haiku-4-5" },
|
||||
replaceAdapterConfig: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("names the Claude default for new and existing agents without pinning it", async () => {
|
||||
const environments = [makeEnvironment({ id: "local-1", name: "Local", driver: "local" })];
|
||||
const existing = await renderForm(environments, { adapterType: "claude_local", adapterConfig: {} });
|
||||
|
||||
@@ -1776,8 +1776,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
||||
}
|
||||
mark("adapterConfig", "model", connectionModels?.resolveModel(v) || v || undefined);
|
||||
if (clearUnsupportedEffort) {
|
||||
mark("adapterConfig", thinkingEffortKey, undefined);
|
||||
mark("adapterConfig", "reasoningEffort", undefined);
|
||||
for (const key of ["modelReasoningEffort", "reasoningEffort", "effort"]) mark("adapterConfig", key, undefined);
|
||||
}
|
||||
}}
|
||||
open={modelOpen}
|
||||
|
||||
@@ -1265,7 +1265,7 @@ describe("CompanyImport", () => {
|
||||
}
|
||||
await clickButton(text => text.startsWith("Import 3 file"));
|
||||
await settle();
|
||||
expect(lastImportMeta().adapterOverrides).toEqual({ coder: { adapterType: "codex_local", runner } });
|
||||
expect(lastImportMeta().adapterOverrides).toEqual({ coder: { adapterType: sourceType === "paperclip_runner" ? "paperclip_runner" : "codex_local", runner } });
|
||||
expect(preview.manifest.agents[0]!.adapterConfig).toEqual(savedSourceConfig);
|
||||
});
|
||||
|
||||
@@ -1302,6 +1302,32 @@ describe("CompanyImport", () => {
|
||||
expect(preview.manifest.agents[0]!.adapterConfig).toEqual(source);
|
||||
});
|
||||
|
||||
it("keeps native Claude configuration at the server translation boundary when switching an import to Legacy", async () => {
|
||||
mockOptionalCompany.mockReturnValue({ selectedCompanyId: "company-1" });
|
||||
mockAdaptersApi.list.mockResolvedValue([
|
||||
{ type: "claude_local", disabled: false, supportedRunners: ["paperclip", "legacy"], defaultRunner: "paperclip" },
|
||||
{ type: "paperclip_runner", disabled: false },
|
||||
]);
|
||||
const preview = buildPreviewResult();
|
||||
const source = { provider: "acpx", acpxAgent: "claude", model: "claude-opus-5-5", modelReasoningEffort: "high", acpxPermissionMode: "approve-all", timeoutSec: 180, lifecycleMode: "per_turn" };
|
||||
preview.manifest.agents[0]!.adapterType = "paperclip_runner";
|
||||
preview.manifest.agents[0]!.runner = "paperclip";
|
||||
preview.manifest.agents[0]!.adapterConfig = source;
|
||||
mockCompaniesApi.importPreview.mockResolvedValue(preview);
|
||||
await renderPage();
|
||||
await enterGithubUrl();
|
||||
await clickButton(text => text === "Preview import");
|
||||
await clickButton(text => text.includes("configure adapter"));
|
||||
await clickButton(text => text.includes("Advanced"));
|
||||
await act(async () => container.querySelector<HTMLButtonElement>('[aria-label="Runner"]')!.click());
|
||||
await act(async () => document.querySelector<HTMLButtonElement>('[role="option"][data-value="legacy"]')!.click());
|
||||
await flushReact();
|
||||
await clickButton(text => text.startsWith("Import 3 file"));
|
||||
await settle();
|
||||
expect(lastImportMeta().adapterOverrides?.coder).toMatchObject({ adapterType: "paperclip_runner", runner: "legacy" });
|
||||
expect(preview.manifest.agents[0]!.adapterConfig).toEqual(source);
|
||||
});
|
||||
|
||||
it("does not reinterpret an unknown native provider as Codex when its configuration is edited", async () => {
|
||||
mockAdaptersApi.list.mockResolvedValue([{ type: "paperclip_runner", disabled: false }]);
|
||||
const preview = buildPreviewResult();
|
||||
|
||||
@@ -1724,7 +1724,9 @@ export function CompanyImport() {
|
||||
const selectedType = effectiveAdapterType(agent);
|
||||
const configVals = adapterConfigValues[agent.slug];
|
||||
if (selectedType === agent.adapterChoice && !configVals) continue;
|
||||
const apiType = selectedType === "openai_dot" ? "paperclip_runner" : selectedType;
|
||||
const retainsNativeHarness = agent.adapterType === "paperclip_runner"
|
||||
&& selectedType === agent.adapterChoice && ["codex_local", "claude_local"].includes(selectedType);
|
||||
const apiType = selectedType === "openai_dot" || retainsNativeHarness ? "paperclip_runner" : selectedType;
|
||||
const override: CompanyPortabilityAdapterOverride = { adapterType: apiType };
|
||||
if (configVals?.runner !== undefined) override.runner = configVals.runner;
|
||||
if (configVals && (selectedType !== agent.adapterChoice || adapterConfigEditedSlugs.has(agent.slug))) {
|
||||
@@ -1735,8 +1737,6 @@ export function CompanyImport() {
|
||||
if (selectedType === "openai_dot" && configVals.adapterSchemaValues?.allowUnmeteredProvider !== true) {
|
||||
throw new Error("OpenAI Dot requires acknowledgement of external billing. Configure the Dot adapter and enable Allow externally billed provider.");
|
||||
}
|
||||
const retainsNativeHarness = agent.adapterType === "paperclip_runner"
|
||||
&& selectedType === agent.adapterChoice && ["codex_local", "claude_local"].includes(selectedType);
|
||||
override.adapterConfig = getUIAdapter(retainsNativeHarness ? "paperclip_runner" : configVals.adapterType).buildAdapterConfig(configVals);
|
||||
}
|
||||
overrides[agent.slug] = override;
|
||||
|
||||
Reference in new issue
Block a user