mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
merge: preserve Codex settings and intentional import removals
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
ec6a2aa545
8 files changed
+259
-7
No files matched your search
@@ -2,6 +2,43 @@
|
||||
|
||||
Updated 2026-10-12.
|
||||
|
||||
## Codex configuration review closure checkpoint
|
||||
|
||||
Runtime prerequisite [#16028](https://github.com/paperclipai/paperclip/pull/16028)
|
||||
advances to `d16eccf109b524778ceb896f79ccb3881bc61a9b`, with 57 files. Its small
|
||||
shared builder correction preserves saved or selected Codex reasoning effort
|
||||
without adding Codex settings to other providers. All 52 existing builder tests
|
||||
pass in the actual prerequisite checkout. The earlier `cae179` fresh 5/5 review
|
||||
is retained historical evidence; this new source requires fresh checks/review.
|
||||
|
||||
Codex behavior source is `b63b779fe141ffe8db6d200580335de84ee78651`, on
|
||||
`codex/codex-runner-default`; [#15683](https://github.com/paperclipai/paperclip/pull/15683)
|
||||
retains 99 changed files. Review reproduced eight selection failures and six
|
||||
import/builder failures before correction. All six actual ACP aliases now follow
|
||||
Legacy precedence before native compatibility checks. Unsupported settings name
|
||||
the effective source field and offer explicit Legacy; Legacy itself is unchanged.
|
||||
Native imports retain hidden runtime settings and account bindings through
|
||||
ordinary edits. Explicit clears/removals and intentional harness changes remain
|
||||
effective. Existing controls pass: 100 selection, 91 UI, and 52 builder; counts
|
||||
can overlap. Canonical server/UI/adapter no-emit, token gates, and independent
|
||||
source-matching reviews pass. No new suite, schema, or API was introduced.
|
||||
|
||||
The [local test-drive](http://127.0.0.1:3125/COD/agents/all) restarted on this exact
|
||||
behavior source and serves `2026.1005.0+403.git.b63b779fe1`. Startup recovery is
|
||||
ready, active runs are zero, and all four complete legacy/native execution
|
||||
configurations and two historical native run records match before/after.
|
||||
The deprecated flag remains false. Prefix-specific receipts are
|
||||
`/private/tmp/pr2-b63b-preservation-{before,after}.json`. The staged Mac daemon
|
||||
passed preflight with `sourceBuild=false` and `needsBuild=false`; startup used
|
||||
JavaScript preparation only. Provider actions, Docker builds, and Cargo builds
|
||||
are zero. This checkpoint's subsequent commit changes only this dated record.
|
||||
|
||||
Current-head full CI/review and managed onboarding/task/follow-up remain required.
|
||||
No merge or production deployment has occurred. Keep expensive final previews
|
||||
deferred until the prerequisite merges and Codex is reconciled with actual master.
|
||||
The authorized Claude owner is coordinating its dependent PRs and existing
|
||||
bounded evidence; root retains normal sequential merge ownership.
|
||||
|
||||
## Metadata review closure checkpoint
|
||||
|
||||
Runtime prerequisite source is `cae179c21412048d08aeb94e0fe8b0f7844119de`,
|
||||
|
||||
@@ -118,6 +118,11 @@ describe("buildCodexLocalConfig", () => {
|
||||
});
|
||||
|
||||
describe("buildPaperclipRunnerConfig", () => {
|
||||
it.each(["", "max"])("preserves or overrides saved native Codex reasoning effort (%s)", thinkingEffort => {
|
||||
const values = makeValues({ thinkingEffort, adapterSchemaValues: { provider: "codex", modelReasoningEffort: "high" } });
|
||||
expect(buildPaperclipRunnerConfig(values).modelReasoningEffort).toBe(thinkingEffort || "high");
|
||||
expect(buildPaperclipRunnerConfig({ ...values, adapterSchemaValues: { provider: "acpx", acpxAgent: "claude", modelReasoningEffort: "high" } })).not.toHaveProperty("modelReasoningEffort");
|
||||
});
|
||||
it.each([undefined, false, true])("preserves Dot attachment consent in create/import forms: %s", value => {
|
||||
const config = buildPaperclipRunnerConfig(makeValues({ adapterType: "paperclip_runner", adapterSchemaValues: { provider: "openai_dot", dotAttachmentAccess: value } }));
|
||||
expect(config.dotAttachmentAccess).toBe(value === true);
|
||||
@@ -142,6 +147,7 @@ describe("buildPaperclipRunnerConfig", () => {
|
||||
codexPermissionMode: "never",
|
||||
lifecycleMode: "per_turn",
|
||||
model: "gpt-5.4",
|
||||
modelReasoningEffort: "high",
|
||||
timeoutSec: 0,
|
||||
graceSec: 15,
|
||||
});
|
||||
@@ -149,7 +155,6 @@ describe("buildPaperclipRunnerConfig", () => {
|
||||
"engine",
|
||||
"agentCommand",
|
||||
"stateDir",
|
||||
"modelReasoningEffort",
|
||||
"search",
|
||||
"fastMode",
|
||||
"dangerouslyBypassApprovalsAndSandbox",
|
||||
|
||||
@@ -241,7 +241,8 @@ export function buildPaperclipRunnerConfig(v: CreateConfigValues): Record<string
|
||||
...schemaValues,
|
||||
provider,
|
||||
...(provider === "codex"
|
||||
? { model: resolvePaperclipRunnerModel("codex", config.model) }
|
||||
? { model: resolvePaperclipRunnerModel("codex", config.model),
|
||||
...(configuredEffort ? { modelReasoningEffort: configuredEffort } : {}) }
|
||||
: {}),
|
||||
codexPermissionMode: "never",
|
||||
opencodePermissionMode: resolvePaperclipRunnerPermissionMode(
|
||||
|
||||
@@ -9,6 +9,7 @@ vi.mock("./environment-probe.js", () => ({ readSandboxPlatformQualification: sta
|
||||
vi.mock("./environment-config.js", () => ({ resolveEnvironmentDriverConfigForRuntime: state.resolveEnvironment }));
|
||||
vi.mock("@paperclipai/adapter-utils/ssh", () => ({ runSshCommand: state.ssh }));
|
||||
import { AGENT_RUNNER_SSH_PLATFORM_COMMAND, agentRunnerAvailability, resolveAgentRunnerTargetForCompany, resolveNewAgentRunner, resolveNewAgentRunnerForCompany } from "./agent-runner-selection.js";
|
||||
import { buildCodexAcpConfig } from "@paperclipai/adapter-codex-local/server";
|
||||
import { localAgentRunnerTarget } from "../lib/agent-runner-platform.js";
|
||||
const target = { driver: "local", platform: "linux", architecture: "x64" };
|
||||
const qualifiedTargets = [target, { driver: "local", platform: "linux", architecture: "arm64" }, { driver: "local", platform: "darwin", architecture: "arm64" }, { driver: "local", platform: "darwin", architecture: "x64" }];
|
||||
@@ -117,6 +118,71 @@ describe("server-owned Codex runner selection", () => {
|
||||
try { resolveNewAgentRunner({ adapterType: "codex_local", target, adapterConfig: { [field]: true } }); }
|
||||
catch (error) { expect(error).toMatchObject({ status: 422, details: { fields: [field] } }); }
|
||||
});
|
||||
it.each([
|
||||
["agentCommand", "acpAgentCommand", "custom-codex-acp"],
|
||||
["stateDir", "acpStateDir", "/custom/acp-state"],
|
||||
["mode", "acpMode", "oneshot"],
|
||||
["permissionMode", "acpPermissionMode", "deny-all"],
|
||||
["nonInteractivePermissions", "acpNonInteractivePermissions", "fail"],
|
||||
["warmHandleIdleMs", "acpWarmHandleIdleMs", 30_000],
|
||||
])("rejects effective Legacy ACP alias %s/%s before automatic native translation", (canonical, alias, value) => {
|
||||
const adapterConfig = { engine: "acp", [alias]: value };
|
||||
expect(buildCodexAcpConfig(adapterConfig)).toHaveProperty(canonical, value);
|
||||
for (const runner of ["auto", "paperclip"] as const) {
|
||||
expect(() => resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target, runner }))
|
||||
.toThrow(expect.objectContaining({ status: 422, details: { code: "agent_runner_config_incompatible", fields: [alias] } }));
|
||||
}
|
||||
expect(resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target, runner: "legacy" }))
|
||||
.toEqual({ adapterType: "codex_local", adapterConfig });
|
||||
});
|
||||
|
||||
it.each([
|
||||
["agentCommand", "acpAgentCommand", "canonical-command", "alias-command"],
|
||||
["stateDir", "acpStateDir", "/canonical-state", "/alias-state"],
|
||||
["permissionMode", "acpPermissionMode", "approve-all", "deny-all"],
|
||||
])("checks the effective canonical ACP %s ahead of its alias", (canonical, alias, value, aliasedValue) => {
|
||||
const adapterConfig = { engine: "acp", [canonical]: value, [alias]: aliasedValue };
|
||||
expect(buildCodexAcpConfig(adapterConfig)).toHaveProperty(canonical, value);
|
||||
expect(() => resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target }))
|
||||
.toThrow(expect.objectContaining({ status: 422, details: { code: "agent_runner_config_incompatible", fields: [canonical] } }));
|
||||
});
|
||||
|
||||
it("uses Legacy ACP canonical precedence and removes compatible or shadowed aliases idempotently", () => {
|
||||
const adapterConfig = { engine: "acp", mode: "persistent", acpMode: "oneshot",
|
||||
nonInteractivePermissions: "deny", acpNonInteractivePermissions: "fail",
|
||||
warmHandleIdleMs: 0, acpWarmHandleIdleMs: 30_000 };
|
||||
expect(buildCodexAcpConfig(adapterConfig)).toMatchObject({ mode: "persistent", nonInteractivePermissions: "deny", warmHandleIdleMs: 0 });
|
||||
const resolved = resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target });
|
||||
expect(resolved).toMatchObject({ adapterType: "paperclip_runner", adapterConfig: { codexPermissionMode: "never" } });
|
||||
for (const field of Object.keys(adapterConfig)) expect(resolved.adapterConfig).not.toHaveProperty(field);
|
||||
expect(resolveNewAgentRunner(resolved)).toEqual(resolved);
|
||||
expect(resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target, runner: "legacy" }))
|
||||
.toEqual({ adapterType: "codex_local", adapterConfig });
|
||||
});
|
||||
|
||||
it("accepts equivalent ACP default aliases without persisting legacy-only controls", () => {
|
||||
const adapterConfig = { engine: "acp", acpMode: " persistent ", acpNonInteractivePermissions: " deny ", acpWarmHandleIdleMs: 0 };
|
||||
expect(buildCodexAcpConfig(adapterConfig)).toMatchObject({ mode: "persistent", nonInteractivePermissions: "deny", warmHandleIdleMs: 0 });
|
||||
const resolved = resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target });
|
||||
expect(resolved.adapterType).toBe("paperclip_runner");
|
||||
for (const field of Object.keys(adapterConfig)) expect(resolved.adapterConfig).not.toHaveProperty(field);
|
||||
expect(resolveNewAgentRunner(resolved)).toEqual(resolved);
|
||||
});
|
||||
|
||||
it("uses nullish ACP idle precedence instead of treating a canonical zero as absent", () => {
|
||||
const adapterConfig = { engine: "acp", warmHandleIdleMs: null, acpWarmHandleIdleMs: 30_000 };
|
||||
expect(buildCodexAcpConfig(adapterConfig)).toHaveProperty("warmHandleIdleMs", 30_000);
|
||||
expect(() => resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target }))
|
||||
.toThrow(expect.objectContaining({ status: 422, details: { code: "agent_runner_config_incompatible", fields: ["acpWarmHandleIdleMs"] } }));
|
||||
});
|
||||
|
||||
it("uses a nonempty ACP alias when its canonical string is blank", () => {
|
||||
const adapterConfig = { engine: "acp", permissionMode: " ", acpPermissionMode: " deny-all " };
|
||||
expect(buildCodexAcpConfig(adapterConfig)).toHaveProperty("permissionMode", "deny-all");
|
||||
expect(() => resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target }))
|
||||
.toThrow(expect.objectContaining({ status: 422, details: { code: "agent_runner_config_incompatible", fields: ["acpPermissionMode"] } }));
|
||||
});
|
||||
|
||||
it("preserves explicit historical native profiles, including managed configurations", () => {
|
||||
for (const config of [{ provider: "acpx", acpxAgent: "claude", model: "claude-sonnet-5" }, { provider: "claude_managed", managedProfileId: "profile", managedAgentsRetentionAcknowledged: true }, { provider: "aws_agentcore", agentCoreProfileId: "profile" }]) expect(resolveNewAgentRunner({ adapterType: "paperclip_runner", adapterConfig: config })).toEqual({ adapterType: "paperclip_runner", adapterConfig: config });
|
||||
});
|
||||
|
||||
@@ -120,6 +120,32 @@ export function resolveNewAgentRunner(input: AgentRunnerSelection): { adapterTyp
|
||||
}
|
||||
return { adapterType: "paperclip_runner", adapterConfig: config };
|
||||
}
|
||||
const aliasSources = new Map<string, string>();
|
||||
// Legacy ACP uses the first nonempty canonical/alias string, and nullish
|
||||
// precedence for its idle timeout. Validate that effective choice, not an
|
||||
// unchecked prefixed field, while retaining its name in actionable errors.
|
||||
for (const [canonical, alias] of [
|
||||
["agentCommand", "acpAgentCommand"], ["stateDir", "acpStateDir"],
|
||||
["mode", "acpMode"], ["permissionMode", "acpPermissionMode"],
|
||||
["nonInteractivePermissions", "acpNonInteractivePermissions"],
|
||||
]) {
|
||||
if (config[alias] === undefined) continue;
|
||||
const canonicalValue = typeof config[canonical] === "string" ? config[canonical].trim() : "";
|
||||
const aliasValue = typeof config[alias] === "string" ? config[alias].trim() : "";
|
||||
if (canonicalValue || aliasValue) {
|
||||
config[canonical] = canonicalValue || aliasValue;
|
||||
if (!canonicalValue) aliasSources.set(canonical, alias);
|
||||
} else if (config[alias] !== null && typeof config[alias] !== "string") incompatible([alias]);
|
||||
else if (typeof config[canonical] === "string") delete config[canonical];
|
||||
delete config[alias];
|
||||
}
|
||||
if (config.acpWarmHandleIdleMs !== undefined) {
|
||||
if (config.warmHandleIdleMs == null && config.acpWarmHandleIdleMs != null) {
|
||||
config.warmHandleIdleMs = config.acpWarmHandleIdleMs;
|
||||
aliasSources.set("warmHandleIdleMs", "acpWarmHandleIdleMs");
|
||||
}
|
||||
delete config.acpWarmHandleIdleMs;
|
||||
}
|
||||
const unsupported = ["agentCommand", "stateDir", "warmHandleIdleMs", "extraArgs", "args", "search", "fastMode", "variant", "filesystemScope", "filesystemExtraPaths", "filesystemSandboxCommand", "networkScope", "networkAllowlist", "permissionMode", "maxTurns", "disableWebSearch", "outputInactivityTimeoutMs", "terminalResultCleanupGraceMs"]
|
||||
.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 !== "codex") unsupported.push("command");
|
||||
@@ -128,7 +154,7 @@ export function resolveNewAgentRunner(input: AgentRunnerSelection): { adapterTyp
|
||||
if (config.nonInteractivePermissions && config.nonInteractivePermissions !== "deny") unsupported.push("nonInteractivePermissions");
|
||||
if (config.dangerouslyBypassApprovalsAndSandbox === false) unsupported.push("dangerouslyBypassApprovalsAndSandbox");
|
||||
if (config.dangerouslyBypassSandbox === false) unsupported.push("dangerouslyBypassSandbox");
|
||||
if (unsupported.length) incompatible(unsupported);
|
||||
if (unsupported.length) incompatible(unsupported.map(field => aliasSources.get(field) ?? field));
|
||||
const effort = config.modelReasoningEffort ?? config.reasoningEffort ?? config.effort;
|
||||
for (const key of ["engine", "command", "extraArgs", "args", "agentCommand", "mode", "nonInteractivePermissions", "stateDir", "warmHandleIdleMs", "dangerouslyBypassApprovalsAndSandbox", "dangerouslyBypassSandbox", "search", "fastMode", "reasoningEffort", "effort"]) delete config[key];
|
||||
if (config.model !== undefined && typeof config.model !== "string") incompatible(["model"]);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { CodexLocalConfigFields } from "./config-fields";
|
||||
import { buildCodexRunnerConfig } from "./config-fields";
|
||||
import { defaultCreateValues } from "@/components/agent-config-defaults";
|
||||
import { resolveCodexLocalCliPolicy } from "@paperclipai/adapter-codex-local";
|
||||
import { buildPaperclipRunnerConfig } from "@paperclipai/adapter-codex-local/ui";
|
||||
|
||||
beforeAll(() => { Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }); });
|
||||
|
||||
@@ -54,6 +55,11 @@ async function renderRunner(config: Record<string, unknown>, expand?: string, in
|
||||
}
|
||||
|
||||
describe("Paperclip Runner Codex configuration", () => {
|
||||
it.each(["", "max"])("preserves or overrides saved native Codex reasoning effort (%s)", thinkingEffort => {
|
||||
const values = { ...defaultCreateValues, thinkingEffort, adapterSchemaValues: { provider: "codex", modelReasoningEffort: "high" } };
|
||||
expect(buildPaperclipRunnerConfig(values).modelReasoningEffort).toBe(thinkingEffort || "high");
|
||||
expect(buildPaperclipRunnerConfig({ ...values, adapterSchemaValues: { provider: "acpx", acpxAgent: "claude", modelReasoningEffort: "high" } })).not.toHaveProperty("modelReasoningEffort");
|
||||
});
|
||||
it.each(["", "--sandbox,read-only"])("shows actual automatic Legacy policy and preserves the explicit toggle (%s)", async (extraArgs) => {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
|
||||
@@ -1360,6 +1360,76 @@ describe("CompanyImport", () => {
|
||||
expect(lastImportMeta().adapterOverrides?.coder?.adapterConfig).not.toHaveProperty("codexLegacySandboxBypass");
|
||||
});
|
||||
|
||||
it.each(["model-clear", "effort-clear", "env-remove", "legacy-model", "changed-harness"] as const)("preserves native Codex source configuration during an ordinary import edit (%s)", async edit => {
|
||||
mockOptionalCompany.mockReturnValue({ selectedCompanyId: "company-1" });
|
||||
mockAdaptersApi.list.mockResolvedValue([
|
||||
{ type: "claude_local", disabled: false },
|
||||
{ type: "codex_local", disabled: false, supportedRunners: ["paperclip", "legacy"], defaultRunner: "paperclip" },
|
||||
{ type: "paperclip_runner", disabled: false },
|
||||
]);
|
||||
const preview = buildPreviewResult();
|
||||
const sourceConfig = {
|
||||
provider: "codex", model: "gpt-5.6-sol", modelReasoningEffort: "high", reasoningEffort: "low",
|
||||
env: { CODEX_HOME: { type: "secret_ref", secretId: "saved-account", version: "latest" },
|
||||
OPENAI_API_KEY: { type: "user_secret_ref", key: "OPENAI_API_KEY", version: "latest", required: true } },
|
||||
timeoutSec: 120, graceSec: 30, cwd: "/saved/workspace", instructionsFilePath: "/saved/AGENTS.md",
|
||||
workspaceStrategy: { type: "git_worktree", baseRef: "main", branchTemplate: "qa/{runId}", worktreeParentDir: "/saved/worktrees" },
|
||||
workspaceRuntime: { services: [{ name: "app", command: "pnpm dev" }] },
|
||||
lifecycleMode: "warm", idleTimeoutMs: 45000,
|
||||
};
|
||||
preview.manifest.agents[0]!.adapterType = "paperclip_runner";
|
||||
preview.manifest.agents[0]!.runner = "paperclip";
|
||||
preview.manifest.agents[0]!.adapterConfig = sourceConfig;
|
||||
mockCompaniesApi.importPreview.mockResolvedValue(preview);
|
||||
await renderPage();
|
||||
await enterGithubUrl();
|
||||
await clickButton(text => text === "Preview import");
|
||||
if (edit === "changed-harness") {
|
||||
const harness = findAdapterSelects()[0]!;
|
||||
await act(async () => {
|
||||
Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, "value")!.set!.call(harness, "claude_local");
|
||||
harness.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
});
|
||||
}
|
||||
await clickButton(text => text.includes("configure adapter"));
|
||||
await clickButton(text => text.includes("Advanced"));
|
||||
const expected: Record<string, unknown> = structuredClone(sourceConfig);
|
||||
if (edit === "legacy-model") {
|
||||
await act(async () => container.querySelector<HTMLButtonElement>('[aria-label="Runner"]')!.click());
|
||||
await act(async () => document.querySelector<HTMLButtonElement>('[role="option"][data-value="legacy"]')!.click());
|
||||
}
|
||||
if (edit === "model-clear" || edit === "legacy-model" || edit === "changed-harness") {
|
||||
await act(async () => container.querySelector<HTMLButtonElement>('[aria-label="Model"]')!.click());
|
||||
await act(async () => [...document.querySelectorAll<HTMLButtonElement>('[role="option"]')].find(option => option.textContent?.trim().startsWith("Default"))!.click());
|
||||
delete expected.model;
|
||||
} else if (edit === "effort-clear") {
|
||||
expect(container.querySelector('[aria-label="Thinking effort"]')?.textContent).toContain("High");
|
||||
await act(async () => container.querySelector<HTMLButtonElement>('[aria-label="Thinking effort"]')!.click());
|
||||
await act(async () => document.querySelector<HTMLButtonElement>('[role="option"][data-value=""]')!.click());
|
||||
delete expected.modelReasoningEffort;
|
||||
delete expected.reasoningEffort;
|
||||
} else {
|
||||
const remove = container.querySelector<HTMLButtonElement>('[aria-label="Remove CODEX_HOME"]');
|
||||
expect(remove).toBeTruthy();
|
||||
await act(async () => remove!.click());
|
||||
await clickButton(text => text === "Save");
|
||||
expected.env = { OPENAI_API_KEY: sourceConfig.env.OPENAI_API_KEY };
|
||||
}
|
||||
await clickButton(text => text.startsWith("Import 3 file"));
|
||||
await settle();
|
||||
if (edit === "changed-harness") {
|
||||
expect(lastImportMeta().adapterOverrides?.coder?.adapterType).toBe("claude_local");
|
||||
const config = lastImportMeta().adapterOverrides?.coder?.adapterConfig;
|
||||
for (const field of ["provider", "env", "workspaceStrategy", "workspaceRuntime", "lifecycleMode", "idleTimeoutMs", "modelReasoningEffort"]) expect(config).not.toHaveProperty(field);
|
||||
expect(config?.timeoutSec).toBe(0);
|
||||
return;
|
||||
}
|
||||
expect(lastImportMeta().adapterOverrides?.coder).toEqual({
|
||||
adapterType: "paperclip_runner", runner: edit === "legacy-model" ? "legacy" : "paperclip", adapterConfig: expected,
|
||||
});
|
||||
expect(preview.manifest.agents[0]!.adapterConfig).toEqual(sourceConfig);
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
@@ -26,8 +26,8 @@ import { getAgentOrderStorageKey, writeAgentOrder } from "../lib/agent-order";
|
||||
import { MarkdownBody } from "../components/MarkdownBody";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { EmptyState } from "../components/EmptyState";
|
||||
import { AgentConfigForm } from "../components/AgentConfigForm";
|
||||
import { cn } from "../lib/utils";
|
||||
import { AgentConfigForm, overlayValuesEqual } from "../components/AgentConfigForm";
|
||||
import { asObject, cn } from "../lib/utils";
|
||||
import {
|
||||
ArrowRight,
|
||||
Check,
|
||||
@@ -585,7 +585,7 @@ function importAdapterValues(
|
||||
...(runner !== undefined ? { runner } : nativeHarness ? { runner: agentRunner("paperclip_runner") } : {}),
|
||||
...(sourceConfig && choice === "codex_local" ? {
|
||||
model: typeof sourceConfig.model === "string" ? sourceConfig.model : "",
|
||||
thinkingEffort: String(sourceConfig.reasoningEffort ?? sourceConfig.modelReasoningEffort ?? ""),
|
||||
thinkingEffort: String(sourceConfig.modelReasoningEffort ?? sourceConfig.reasoningEffort ?? ""),
|
||||
...(typeof savedBypass === "boolean" ? { codexLegacySandboxBypass: savedBypass } : {}),
|
||||
// Runner-only imports preserve server-translated policy; display that policy too.
|
||||
dangerouslyBypassSandbox: typeof savedBypass === "boolean" ? savedBypass
|
||||
@@ -594,6 +594,18 @@ function importAdapterValues(
|
||||
: resolveCodexLocalCliPolicy(sourceConfig).bypass,
|
||||
} : {}),
|
||||
...(sourceConfig && choice === "claude_local" ? { model: typeof sourceConfig.model === "string" ? sourceConfig.model : "", thinkingEffort: String(sourceConfig.modelReasoningEffort ?? sourceConfig.reasoningEffort ?? sourceConfig.effort ?? "") } : {}),
|
||||
...(nativeHarness && sourceConfig ? {
|
||||
cwd: typeof sourceConfig.cwd === "string" ? sourceConfig.cwd : "",
|
||||
instructionsFilePath: typeof sourceConfig.instructionsFilePath === "string" ? sourceConfig.instructionsFilePath : "",
|
||||
envBindings: { ...asObject(sourceConfig.env) },
|
||||
workspaceStrategyType: String(asObject(sourceConfig.workspaceStrategy).type ?? "project_primary"),
|
||||
workspaceBaseRef: String(asObject(sourceConfig.workspaceStrategy).baseRef ?? ""),
|
||||
workspaceBranchTemplate: String(asObject(sourceConfig.workspaceStrategy).branchTemplate ?? ""),
|
||||
worktreeParentDir: String(asObject(sourceConfig.workspaceStrategy).worktreeParentDir ?? ""),
|
||||
runtimeServicesJson: sourceConfig.workspaceRuntime ? JSON.stringify(sourceConfig.workspaceRuntime) : "",
|
||||
...(sourceConfig.lifecycleMode === "warm" || sourceConfig.lifecycleMode === "per_turn" ? { paperclipRunnerLifecycleMode: sourceConfig.lifecycleMode } : {}),
|
||||
...(typeof sourceConfig.idleTimeoutMs === "number" ? { paperclipRunnerIdleTimeoutMs: sourceConfig.idleTimeoutMs } : {}),
|
||||
} : {}),
|
||||
...(sourceAdapterType === "paperclip_runner" && choice !== "openai_dot"
|
||||
? { adapterSchemaValues: { ...sourceConfig } } : {}),
|
||||
...(choice === "openai_dot" ? {
|
||||
@@ -1738,7 +1750,36 @@ 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.");
|
||||
}
|
||||
override.adapterConfig = getUIAdapter(retainsNativeHarness ? "paperclip_runner" : configVals.adapterType).buildAdapterConfig(configVals);
|
||||
const build = getUIAdapter(retainsNativeHarness && selectedType === "claude_local" ? "paperclip_runner" : configVals.adapterType).buildAdapterConfig;
|
||||
const edited = build(configVals);
|
||||
if (agent.adapterType === "paperclip_runner" && selectedType === agent.adapterChoice
|
||||
&& ["codex_local", "claude_local"].includes(selectedType) && configVals.adapterType === selectedType
|
||||
&& agentHarnessType("paperclip_runner", configVals.adapterSchemaValues) === selectedType) {
|
||||
// Project only real form changes onto the saved native configuration.
|
||||
// Form defaults must not replace bindings or runtime fields; explicit
|
||||
// clears must not be filled back in from the source either.
|
||||
const original = importAdapterValues(selectedType, agent.adapterConfig, agent.adapterType, configVals.runner);
|
||||
const baseline = build(original);
|
||||
const retained = { ...agent.adapterConfig };
|
||||
const applyChanges = (before: Record<string, unknown>, after: Record<string, unknown>) => {
|
||||
for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
|
||||
if (overlayValuesEqual(before[key], after[key])) continue;
|
||||
if (after[key] === undefined) delete retained[key];
|
||||
else retained[key] = after[key];
|
||||
}
|
||||
};
|
||||
applyChanges(original.adapterSchemaValues ?? {}, configVals.adapterSchemaValues ?? {});
|
||||
applyChanges(baseline, edited);
|
||||
if (original.model !== configVals.model && !configVals.model) delete retained.model;
|
||||
if (original.thinkingEffort !== configVals.thinkingEffort) {
|
||||
delete retained.reasoningEffort;
|
||||
delete retained.effort;
|
||||
if (!configVals.thinkingEffort) delete retained.modelReasoningEffort;
|
||||
}
|
||||
override.adapterConfig = retained;
|
||||
} else {
|
||||
override.adapterConfig = edited;
|
||||
}
|
||||
}
|
||||
overrides[agent.slug] = override;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user