fix(agents): preserve legacy policy and require measured sandbox defaults

Keep explicit Legacy permission choices through automatic selection, import editing, draft reset and policy display. Use company-scoped measured immutable-image facts for automatic sandbox defaults without allocating resources during creation or discovery.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-12 16:19:00 -05:00
1 parent f340d92b39
commit 65700c6908
12 files changed
+222 -29

No files matched your search

@@ -2,6 +2,38 @@
Updated 2026-10-12.
## Corrective source checkpoint: ready for fresh CI and review
Runtime prerequisite source is `cf19e086ae4a0142c8b4314771c55f73b294805b`
on `codex/runner-native-platform-prerequisites`, with 50 changed files. The
Codex-default branch `codex/codex-runner-default` is composed on it and retains
99 changed files. Both reviews remain under 100 files. All four original review
findings and the relevant independent-review follow-ups are fixed.
Existing controls pass: release workflow 70, assembly 7, registration 15,
measured environment/probe routes 32, UI permission/forms 207, and selection/import
140. Counts describe separate runs and can overlap. Canonical server, runner and
UI no-emit checks and token gates pass. Source-matching independent reviews
confirm cleanup, immutable-image binding and saved Legacy permission handling.
No local Docker/Rust build, new provider turn, or agent migration was used.
Remaining gates are fresh exact-head full CI/review and managed Codex acceptance.
The existing managed eval catalog already requires OCI digests. Its image-backed
fixture helper must use the existing explicit environment probe before automatic
Codex creation and inspect the stored default; explicit-runner coverage remains
separate. First-task onboarding fixtures are local-only, so fresh managed
onboarding still requires live proof. The pinned fleet tenant currently uses a
name-only snapshot and cannot establish native-default compatibility. Use a
qualified immutable image-backed QA environment, preserve the tenant pin, and
verify the serving commit and independently checked task/follow-up artifacts.
Merge the prerequisite after its applicable checks pass. Then reconcile the
Codex PR with actual master, verify its final source, and build the immutable
preview and DB/shared pair remotely once. All three cancelled previews remain
excluded. The original budget and cleanup holds remain unchanged. Claude owns
its separate bounded proof and downstream attestation reconciliation. No merge
or production deployment has occurred at this checkpoint.
## Final review checkpoint: shared fixes and sandbox qualification
The user authorizes normal sequential merges after the applicable gates pass.
+2
View File
@@ -727,6 +727,8 @@ export interface CreateConfigValues {
runner?: "auto" | "paperclip" | "legacy";
adapterType: string;
codexPermissionMode?: "never" | "on-request" | "untrusted";
/** Explicit Legacy permission choice; never persisted as a runner setting. */
codexLegacySandboxBypass?: boolean;
paperclipRunnerLifecycleMode?: "per_turn" | "warm";
paperclipRunnerIdleTimeoutMs?: number;
cwd: string;
@@ -1,10 +1,11 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { agentHarnessType, agentRunner, paperclipRunnerProfileForHarness, paperclipRunnerSupportsPlatform, PAPERCLIP_RUNNER_MINIMUM_GLIBC_VERSION, createAgentSchema, updateAgentSchema, testAdapterEnvironmentSchema } from "@paperclipai/shared";
const state = vi.hoisted(() => ({ disabled: [] as string[], overridden: new Set<string>(), settings: {} as Record<string, unknown>, getEnvironment: vi.fn(), bindings: vi.fn(), managedEnvironment: vi.fn(), ssh: vi.fn(), resolveEnvironment: vi.fn() }));
const state = vi.hoisted(() => ({ disabled: [] as string[], overridden: new Set<string>(), settings: {} as Record<string, unknown>, getEnvironment: vi.fn(), bindings: vi.fn(), managedEnvironment: vi.fn(), ssh: vi.fn(), resolveEnvironment: vi.fn(), sandboxQualification: vi.fn() }));
vi.mock("../adapters/registry.js", () => ({ findActiveServerAdapter: (type: string) => ({ type }), hasActiveAdapterOverride: (type: string) => state.overridden.has(type), waitForExternalAdapters: async () => {} }));
vi.mock("./adapter-plugin-store.js", () => ({ getDisabledAdapterTypes: () => state.disabled }));
vi.mock("./instance-settings.js", () => ({ instanceSettingsService: () => ({ get: async () => state.settings }) }));
vi.mock("./environments.js", () => ({ environmentService: () => ({ getById: state.getEnvironment, listBoundCompanyIds: state.bindings, findManagedSandboxEnvironment: state.managedEnvironment, findKubernetesEnvironment: state.managedEnvironment }) }));
vi.mock("./environment-probe.js", () => ({ readSandboxPlatformQualification: state.sandboxQualification }));
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";
@@ -16,7 +17,7 @@ const originalArchitecture = Object.getOwnPropertyDescriptor(process, "arch")!;
afterEach(() => { vi.restoreAllMocks(); Object.defineProperty(process, "platform", originalPlatform); Object.defineProperty(process, "arch", originalArchitecture); });
describe("server-owned Codex runner selection", () => {
beforeEach(() => { state.disabled = []; state.overridden.clear(); state.settings = {}; state.getEnvironment.mockReset(); state.bindings.mockReset().mockResolvedValue([]); state.managedEnvironment.mockReset(); state.ssh.mockReset().mockResolvedValue({ stdout: "Linux\nx86_64\n" }); state.resolveEnvironment.mockReset().mockResolvedValue({ driver: "ssh", config: {} }); });
beforeEach(() => { state.disabled = []; state.overridden.clear(); state.settings = {}; state.getEnvironment.mockReset(); state.bindings.mockReset().mockResolvedValue([]); state.managedEnvironment.mockReset(); state.ssh.mockReset().mockResolvedValue({ stdout: "Linux\nx86_64\n" }); state.sandboxQualification.mockReset().mockResolvedValue(null); state.resolveEnvironment.mockReset().mockResolvedValue({ driver: "ssh", config: {} }); });
it.each(["x64", "arm64"])("keeps detected musl/unknown Linux %s legacy and preserves saved execution", architecture => {
for (const libc of ["musl", "unknown"] as const) {
const target = { driver: "local", platform: "linux", architecture, libc };
@@ -166,10 +167,30 @@ describe("server-owned Codex runner selection", () => {
expect(state.getEnvironment).not.toHaveBeenCalled();
expect(state.ssh).not.toHaveBeenCalled();
});
it.each([
[{ platform: "linux", architecture: "x64", libc: "glibc", glibcVersion: "2.39" }, "paperclip_runner"],
[{ platform: "linux", architecture: "arm64", libc: "glibc", glibcVersion: "3.0" }, "paperclip_runner"],
[{ platform: "linux", architecture: "arm64", libc: "glibc", glibcVersion: "2.38" }, "codex_local"],
[{ platform: "linux", architecture: "x64", libc: "glibc" }, "codex_local"],
[{ platform: "linux", architecture: "arm64", libc: "musl" }, "codex_local"],
[{ platform: "linux", architecture: "unknown", libc: "glibc", glibcVersion: "2.39" }, "codex_local"],
])("uses cached selected-image measurements for sandbox creation/discovery (%j)", async (facts, expected) => {
state.getEnvironment.mockResolvedValue({ id: "sandbox", driver: "sandbox", status: "active", config: { provider: "daytona" } });
state.sandboxQualification.mockResolvedValue(facts);
const selectedTarget = await resolveAgentRunnerTargetForCompany({} as never, "company", "sandbox");
expect(agentRunnerAvailability("codex_local", selectedTarget).defaultRunner).toBe(expected === "paperclip_runner" ? "paperclip" : "legacy");
const result = await resolveNewAgentRunnerForCompany({} as never, "company", { adapterType: "codex_local", defaultEnvironmentId: "sandbox" });
expect(result.adapterType).toBe(expected);
expect(state.ssh).not.toHaveBeenCalled();
expect(state.resolveEnvironment).not.toHaveBeenCalled();
if (expected === "codex_local") await expect(resolveNewAgentRunnerForCompany({} as never, "company", { adapterType: "codex_local", runner: "paperclip", defaultEnvironmentId: "sandbox" })).rejects.toMatchObject({ status: 422 });
});
it("does not infer a sandbox image architecture from its driver", async () => {
state.getEnvironment.mockResolvedValue({ id: "sandbox", driver: "sandbox", status: "active", config: { provider: "daytona" } });
const result = await resolveNewAgentRunnerForCompany({} as never, "company", { adapterType: "codex_local", defaultEnvironmentId: "sandbox" });
expect(result.adapterType).toBe("paperclip_runner"); expect(state.ssh).not.toHaveBeenCalled();
expect(result.adapterType).toBe("codex_local"); expect(state.ssh).not.toHaveBeenCalled();
expect(state.sandboxQualification).toHaveBeenCalledWith({}, expect.objectContaining({ id: "sandbox" }), "company");
await expect(resolveNewAgentRunnerForCompany({} as never, "company", { adapterType: "codex_local", runner: "paperclip", defaultEnvironmentId: "sandbox" })).rejects.toMatchObject({ status: 422, details: { code: "agent_runner_unavailable" } });
});
});
+11 -2
View File
@@ -25,6 +25,8 @@ export function agentRunnerAvailability(harness: string, target: RunnerTarget =
&& !!findActiveServerAdapter(harness) && !!findActiveServerAdapter("paperclip_runner")
&& !hasActiveAdapterOverride(harness) && !hasActiveAdapterOverride("paperclip_runner")
&& isEnvironmentDriverSupportedForAdapter("paperclip_runner", target.driver)
&& (target.driver !== "sandbox" || Boolean(target.platform && target.architecture
&& (target.platform !== "linux" || (target.libc === "glibc" && target.glibcVersion))))
&& (!target.platform || paperclipRunnerSupportsPlatform(harness, target.platform, target.architecture ?? "", target.libc, target.glibcVersion));
return { supportedRunners: supported ? ["paperclip", "legacy"] : ["legacy"], defaultRunner: supported ? "paperclip" : "legacy" };
}
@@ -55,7 +57,9 @@ 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.defaultRunner !== "paperclip") throw unprocessable(input.target?.driver === "sandbox"
? "Paperclip Runner is unavailable for this sandbox. Choose an immutable OCI image digest and run its environment connection probe to qualify the OS, architecture and libc, or select Legacy runner in Advanced."
: "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) };
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));
@@ -112,7 +116,12 @@ export async function resolveAgentRunnerTargetForCompany(db: Db, companyId: stri
}
const driver = environment?.driver ?? "local";
let target: RunnerTarget = driver === "local" ? localAgentRunnerTarget()
: { driver }; // Sandbox platform/artifact identity is verified by selected-runtime setup; do not guess an image's architecture.
: { driver };
if (driver === "sandbox" && environment) {
const { readSandboxPlatformQualification } = await import("./environment-probe.js");
const qualified = await readSandboxPlatformQualification(db, environment, companyId);
if (qualified) target = { driver, ...qualified };
}
if (driver === "ssh" && environment && agentRunnerAvailability("codex_local", target).defaultRunner === "paperclip") {
const parsed = await resolveEnvironmentDriverConfigForRuntime(db, companyId, environment);
if (parsed.driver !== "ssh") throw unprocessable("The selected SSH environment is unavailable.");
@@ -1,6 +1,6 @@
// @vitest-environment jsdom
import { createRoot } from "react-dom/client";
import { act } from "react";
import { act, useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { ReactNode } from "react";
@@ -9,6 +9,9 @@ import { beforeAll, describe, expect, it } from "vitest";
import { TooltipProvider } from "@/components/ui/tooltip";
import { CodexLocalConfigFields } from "./config-fields";
import { buildCodexRunnerConfig } from "./config-fields";
import { defaultCreateValues } from "@/components/agent-config-defaults";
import { resolveCodexLocalCliPolicy } from "@paperclipai/adapter-codex-local";
beforeAll(() => { Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }); });
@@ -51,6 +54,47 @@ async function renderRunner(config: Record<string, unknown>, expand?: string, in
}
describe("Paperclip Runner Codex configuration", () => {
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);
const root = createRoot(container);
let saved: typeof defaultCreateValues = { ...defaultCreateValues, adapterType: "codex_local", runner: "auto", extraArgs };
function Fixture() {
const [values, setValues] = useState(saved);
saved = values;
return <CodexLocalConfigFields mode="create" isCreate adapterType="codex_local" values={values}
set={(patch) => setValues(current => ({ ...current, ...patch }))} config={{}}
eff={(_group, _field, original) => original} mark={() => undefined} models={[]} hideInstructionsFile />;
}
try {
await act(async () => root.render(<QueryClientProvider client={new QueryClient()}><TooltipProvider><Fixture /></TooltipProvider></QueryClientProvider>));
const toggle = container.querySelector<HTMLButtonElement>('[aria-label="Bypass sandbox"]');
expect(toggle).toBeDefined();
expect(toggle?.getAttribute("aria-checked")).toBe(String(resolveCodexLocalCliPolicy(buildCodexRunnerConfig(saved)).bypass));
await act(async () => toggle!.click());
const config = buildCodexRunnerConfig(saved);
expect(saved.runner).toBe("auto");
expect(config.dangerouslyBypassApprovalsAndSandbox).toBe(extraArgs !== "");
expect(toggle?.getAttribute("aria-checked")).toBe(String(resolveCodexLocalCliPolicy(config).bypass));
} finally {
await act(async () => root.unmount());
container.remove();
}
});
it.each([
{},
{ dangerouslyBypassApprovalsAndSandbox: false, dangerouslyBypassSandbox: true },
{ dangerouslyBypassSandbox: false },
{ extraArgs: ["--sandbox", "read-only"] },
])("shows the actual saved Legacy CLI policy while editing %j", async config => {
await renderMarkup(<TooltipProvider><CodexLocalConfigFields mode="edit" isCreate={false}
adapterType="codex_local" values={null} set={null} config={config}
eff={(_group, _field, original) => original} mark={() => undefined} models={[]} hideInstructionsFile />
</TooltipProvider>, undefined, container => {
expect(container.querySelector('[aria-label="Bypass sandbox"]')?.getAttribute("aria-checked"))
.toBe(String(resolveCodexLocalCliPolicy(config).bypass));
});
});
it("edits the legacy execution engine through the shared keyboard picker", async () => {
const container = document.createElement("div");
document.body.appendChild(container);
+30 -10
View File
@@ -3,6 +3,8 @@ import { AdapterMark } from "../../components/AdapterMark";
import { DotRunnerConnection } from "../../components/DotRunnerConnection";
import { configFieldsForSection } from "../config-sections";
import type { AdapterConfigFieldsProps } from "../types";
import type { CreateConfigValues } from "@paperclipai/adapter-utils";
import { buildCodexLocalConfig } from "@paperclipai/adapter-codex-local/ui";
import {
Field,
ToggleField,
@@ -17,6 +19,7 @@ import {
CODEX_LOCAL_FAST_MODE_SUPPORTED_MODELS,
isCodexLocalFastModeSupported,
isCodexLocalManualModel,
resolveCodexLocalCliPolicy,
} from "@paperclipai/adapter-codex-local";
import {
PAPERCLIP_RUNNER_IDLE_TIMEOUT_DEFAULT_MS,
@@ -47,6 +50,23 @@ const runnerHarnessOptions = [
{ value: "grok", label: "Grok Build", adapter: "grok_local" },
];
/** Keep explicit Legacy policy even when the server resolves an automatic choice. */
export function buildCodexRunnerConfig(values: CreateConfigValues): Record<string, unknown> {
const config = buildCodexLocalConfig(values);
if (values.codexLegacySandboxBypass !== undefined) {
config.dangerouslyBypassApprovalsAndSandbox = values.codexLegacySandboxBypass;
} else if (values.runner !== "legacy") {
// An untouched false form default is not an explicit native permission policy.
delete config.dangerouslyBypassApprovalsAndSandbox;
}
return {
...config,
...(values.codexPermissionMode !== undefined ? { codexPermissionMode: values.codexPermissionMode } : {}),
...(values.paperclipRunnerLifecycleMode !== undefined ? { lifecycleMode: values.paperclipRunnerLifecycleMode } : {}),
...(values.paperclipRunnerIdleTimeoutMs !== undefined ? { idleTimeoutMs: values.paperclipRunnerIdleTimeoutMs } : {}),
};
}
export function CodexLocalConfigFields({
section,
companyId,
@@ -149,9 +169,13 @@ export function CodexLocalConfigFields({
const engine =
rawEngine === "acp" || rawEngine === "cli" ? rawEngine : "auto";
const acpSelected = engine === "acp";
const bypassEnabled =
config.dangerouslyBypassApprovalsAndSandbox === true ||
config.dangerouslyBypassSandbox === true;
const bypassEnabled = resolveCodexLocalCliPolicy({
...config,
dangerouslyBypassApprovalsAndSandbox: eff("adapterConfig", "dangerouslyBypassApprovalsAndSandbox", config.dangerouslyBypassApprovalsAndSandbox),
dangerouslyBypassSandbox: eff("adapterConfig", "dangerouslyBypassSandbox", config.dangerouslyBypassSandbox),
extraArgs: eff("adapterConfig", "extraArgs", config.extraArgs),
args: eff("adapterConfig", "args", config.args),
}).bypass;
const fastModeEnabled = isCreate
? Boolean(values!.fastMode)
: eff("adapterConfig", "fastMode", Boolean(config.fastMode));
@@ -753,16 +777,12 @@ export function CodexLocalConfigFields({
hint={help.dangerouslyBypassSandbox}
checked={
isCreate
? values!.dangerouslyBypassSandbox
: eff(
"adapterConfig",
"dangerouslyBypassApprovalsAndSandbox",
bypassEnabled,
)
? resolveCodexLocalCliPolicy(buildCodexRunnerConfig(values!)).bypass
: bypassEnabled
}
onChange={(v) =>
isCreate
? set!({ dangerouslyBypassSandbox: v })
? set!({ dangerouslyBypassSandbox: v, codexLegacySandboxBypass: v })
: mark(
"adapterConfig",
"dangerouslyBypassApprovalsAndSandbox",
+2 -13
View File
@@ -1,22 +1,11 @@
import type { UIAdapterModule } from "../types";
import { parseCodexStdoutLine } from "@paperclipai/adapter-codex-local/ui";
import { CodexLocalConfigFields } from "./config-fields";
import { buildCodexLocalConfig } from "@paperclipai/adapter-codex-local/ui";
import { CodexLocalConfigFields, buildCodexRunnerConfig } from "./config-fields";
export const codexLocalUIAdapter: UIAdapterModule = {
type: "codex_local",
label: "Codex",
parseStdoutLine: parseCodexStdoutLine,
ConfigFields: CodexLocalConfigFields,
buildAdapterConfig: (values) => {
const config = buildCodexLocalConfig(values);
// Legacy permission flags cannot describe the native provider policy.
if (values.runner !== "legacy") delete config.dangerouslyBypassApprovalsAndSandbox;
return {
...config,
...(values.codexPermissionMode !== undefined ? { codexPermissionMode: values.codexPermissionMode } : {}),
...(values.paperclipRunnerLifecycleMode !== undefined ? { lifecycleMode: values.paperclipRunnerLifecycleMode } : {}),
...(values.paperclipRunnerIdleTimeoutMs !== undefined ? { idleTimeoutMs: values.paperclipRunnerIdleTimeoutMs } : {}),
};
},
buildAdapterConfig: buildCodexRunnerConfig,
};
@@ -660,6 +660,39 @@ async function flushUntil(check: () => boolean, timeoutMs = 4000) {
describe("AgentConfigForm environment selector", () => {
let roots: Root[] = [];
it("keeps an explicit Codex draft policy until a harness reset clears it", async () => {
mockEnvironmentsApi.list.mockResolvedValue([]);
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } });
let current = { ...defaultCreateValues, adapterType: "codex_local", runner: "auto" as const,
dangerouslyBypassSandbox: false, codexLegacySandboxBypass: false };
function Fixture() {
const [values, setValues] = useState<typeof defaultCreateValues>(current);
current = values as typeof current;
return <AgentConfigForm mode="create" values={values}
onChange={patch => setValues(prev => ({ ...prev, ...patch }))} showAdapterTypeField hidePromptTemplate />;
}
try {
await act(async () => root.render(<QueryClientProvider client={queryClient}><ToastProvider><TooltipProvider>
<Fixture />
</TooltipProvider></ToastProvider></QueryClientProvider>));
await flushReact();
expect(current.codexLegacySandboxBypass).toBe(false);
for (const harness of ["claude_local", "codex_local"]) {
await clickElement(container.querySelector('[aria-label="Harness"]'));
await clickElement(document.querySelector(`[role="option"][data-value="${harness}"]`));
await flushReact();
expect(current.adapterType).toBe(harness);
expect(current).toHaveProperty("codexLegacySandboxBypass", undefined);
}
} finally {
await act(async () => root.unmount());
container.remove();
}
});
it.each(["codex_local", "paperclip_runner"])("keeps a saved %s runner during unrelated edits", async adapterType => {
const adapterConfig = { model: "gpt-5.6-sol", ...(adapterType === "paperclip_runner" ? { provider: "codex", reasoningEffort: "high" } : { modelReasoningEffort: "high" }) };
const result = await renderForm([], { adapterType, adapterConfig }, { showAdapterTypeField: true });
@@ -12,6 +12,7 @@ export const defaultCreateValues: CreateConfigValues = {
search: false,
fastMode: false,
dangerouslyBypassSandbox: false,
codexLegacySandboxBypass: undefined,
command: "",
args: "",
extraArgs: "",
@@ -3,8 +3,17 @@ import { describe, expect, it } from "vitest";
import { buildNewAgentHirePayload } from "./new-agent-hire-payload";
import { defaultCreateValues } from "../components/agent-config-defaults";
import { codexLocalUIAdapter } from "../adapters/codex-local";
import { resolveCodexLocalCliPolicy } from "@paperclipai/adapter-codex-local";
describe("buildNewAgentHirePayload", () => {
it.each([false, true])("preserves explicit automatic Legacy sandbox choice %s through the actual CLI policy", (bypass) => {
const values = { ...defaultCreateValues, adapterType: "codex_local", runner: "auto" as const, dangerouslyBypassSandbox: bypass, codexLegacySandboxBypass: bypass };
const payload = buildNewAgentHirePayload({ name: "Codex", effectiveRole: "general", configValues: values, adapterConfig: codexLocalUIAdapter.buildAdapterConfig(values) });
expect(payload.runner).toBe("auto");
expect(payload.adapterConfig.dangerouslyBypassApprovalsAndSandbox).toBe(bypass);
expect(resolveCodexLocalCliPolicy(payload.adapterConfig).bypass).toBe(bypass);
expect(payload.adapterConfig).not.toHaveProperty("codexLegacySandboxBypass");
});
it.each([undefined, "auto", "paperclip", "legacy"] as const)("keeps runner %s separate from Codex execution settings", (runner) => {
const values = { ...defaultCreateValues, adapterType: "codex_local", runner, model: "gpt-5.6-sol", thinkingEffort: "high" };
const payload = buildNewAgentHirePayload({ name: "Codex", effectiveRole: "general", configValues: values, adapterConfig: codexLocalUIAdapter.buildAdapterConfig(values) });
+32
View File
@@ -1269,6 +1269,38 @@ describe("CompanyImport", () => {
expect(preview.manifest.agents[0]!.adapterConfig).toEqual(savedSourceConfig);
});
it.each([
{ dangerouslyBypassApprovalsAndSandbox: false },
{ dangerouslyBypassSandbox: false },
{ dangerouslyBypassApprovalsAndSandbox: false, dangerouslyBypassSandbox: true },
])("keeps saved restrictive Legacy policy during an automatic import model edit (%j)", async policy => {
mockOptionalCompany.mockReturnValue({ selectedCompanyId: "company-1" });
mockAdaptersApi.list.mockResolvedValue([
{ type: "codex_local", disabled: false, supportedRunners: ["legacy"], defaultRunner: "legacy" },
]);
const preview = buildPreviewResult();
preview.manifest.agents[0]!.adapterType = "codex_local";
preview.manifest.agents[0]!.adapterConfig = { model: "gpt-5.6-sol", ...policy };
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"));
expect(container.querySelector('[aria-label="Bypass sandbox"]')?.getAttribute("aria-checked")).toBe("false");
await act(async () => container.querySelector<HTMLButtonElement>('[aria-label="Model"]')!.click());
const defaultModel = [...document.querySelectorAll<HTMLButtonElement>('[role="option"]')].find(option => option.textContent?.trim() === "Default");
expect(defaultModel).toBeTruthy();
await act(async () => defaultModel!.click());
await clickButton(text => text.startsWith("Import 3 file"));
await settle();
expect(lastImportMeta().adapterOverrides?.coder).toMatchObject({
adapterType: "codex_local", adapterConfig: { dangerouslyBypassApprovalsAndSandbox: false },
});
expect(lastImportMeta().adapterOverrides?.coder?.adapterConfig).not.toHaveProperty("model");
expect(lastImportMeta().adapterOverrides?.coder?.adapterConfig).not.toHaveProperty("codexLegacySandboxBypass");
});
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();
+1
View File
@@ -585,6 +585,7 @@ function importAdapterValues(
...(sourceConfig && choice === "codex_local" ? {
model: typeof sourceConfig.model === "string" ? sourceConfig.model : "",
thinkingEffort: String(sourceConfig.reasoningEffort ?? sourceConfig.modelReasoningEffort ?? ""),
...(typeof savedBypass === "boolean" ? { codexLegacySandboxBypass: savedBypass } : {}),
// Runner-only imports preserve server-translated policy; display that policy too.
dangerouslyBypassSandbox: typeof savedBypass === "boolean" ? savedBypass
: nativeCodex