mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 04:02:18 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Native Codex agents use the saved agent settings to prepare each provider launch. > - Onboarding saves web search as off, but native configuration conversion removes that setting. > - Codex 0.160.0 includes a hosted web-search tool by default, even in an authentication probe that asks it not to use tools. > - A gateway that permits only priced function tools can reject that request before budget admission. > - This change preserves the explicit restriction through native execution and disables web search for authentication probes. ## Linked Issues or Issue Description **What happened?** A native Codex authentication probe can include hosted web search. Normal task execution also loses a saved `search: false` setting. A restricted Responses gateway can reject these requests before it makes a model call. Related repairs: Refs #17445, Refs #17529. The roadmap and related PRs were checked; no duplicate repair was found. **Expected behavior** Authentication probes check the selected account and model without hosted web search. A saved search-off setting remains effective in native task launches, prepared launches, and session recovery. Existing behavior for an absent setting remains unchanged. **Steps to reproduce** 1. Create a Codex agent with search off and choose Paperclip Runner. 2. Use a Responses gateway that rejects hosted tools. 3. Inspect the request from the native authentication probe or task launch. Before this fix, the pinned Codex SDK includes `web_search` despite the saved setting. ## What Changed - Preserve an explicit search-off setting when native agent configuration is built and converted. - Carry the restriction in the durable native input and provider launch requirements. - Apply the restriction to authentication probes and normal launch paths, including custom arguments and recovery. - Reject prepared launches that discard the required restriction. Keep gateway billing rules unchanged. - Admit only the restrictive literal value in the strict plugin task API. Exercise the real plugin setup path. - Add regression coverage and document the policy. - Add the immutable Copilot v60 source identity required by the changed shared files. Preserve v59 and all original live evidence. Copilot authentication probes keep their prior launch requirements. ## Verification - Regression cases were run against the unfixed source and failed at the configuration and launch boundaries. - An isolated capture using the real Codex 0.160.0 app-server reproduced hosted-tool rejection. Disabling web search reached a mocked budget reservation. The helper and child processes permitted only loopback network access and used synthetic credentials. It made zero upstream calls. - Focused checks passed: 284 server cases, 61 native form cases, 198 backend/security/launch/parser cases, and 36 authentication/launch cases. A separate complete transport/launch/parser run passed 393 cases. These groups overlap. - Head `617b98f` passed workspace typecheck/build and 28/28 plugin contract and actual Runner probe tests with no skips. Final rebased head `20ff0c1` passed all current-head CI checks, including full isolated test lanes, and fresh Greptile and Paperclip reviews at 5/5. - The additive Copilot identity update passed 134 compatibility/policy/protocol checks and nine authentication probe checks. The real pinned Codex SDK capture was repeated after the final policy change: all five cases passed with zero upstream calls. - Two local full-suite attempts stopped after native PostgreSQL loader failures. Seven local dependency aliases were restored, and an isolated cluster initialized successfully. A third run later encountered host PostgreSQL semaphore exhaustion and was interrupted before source-profile regeneration. Its zero exit code after SIGINT is explicitly not a full-suite pass. Final full-suite verification runs in isolated CI; the local attempts remain recorded. - The first CI run exposed a missing strict plugin API field. Its five failures were reproduced locally, then passed after repair. The next run passed every underlying job except the source-identity guard. That guard now binds an additive identity with v59 retained. Final-head CI and both fresh reviews passed; addressed review threads are resolved. - The offline capture runs on Darwin ARM64. It does not prove the contents of a prior unretained Linux request or complete Cloud onboarding acceptance. ## Risks - The restriction must survive prepared launches and resumed sessions. Regression tests cover those boundaries. - Existing absent settings retain their prior behavior. This change does not add native search-on support or loosen gateway admission. - Older durable inputs without the optional restriction must remain valid. - Copilot now uses profile v60 for exact launch admission. Historical v59 inputs remain readable. The new declaration adds no live Copilot qualification and does not change its executable or admission flags. - A deployed Cloud acceptance run is still required after this source repair. ## Model Used - OpenAI GPT-6 through Codex, with reasoning, code execution, and delegated review. The exact backend model identifier and context-window size are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
650 lines
58 KiB
TypeScript
650 lines
58 KiB
TypeScript
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(), kubernetesEnvironment: vi.fn(), ensureKubernetesEnvironment: vi.fn(), ssh: vi.fn(), resolveEnvironment: vi.fn(), sandboxQualification: vi.fn(), pluginByKey: 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.kubernetesEnvironment, ensureKubernetesEnvironment: state.ensureKubernetesEnvironment }) }));
|
|
vi.mock("./environment-probe.js", () => ({ readSandboxPlatformQualification: state.sandboxQualification }));
|
|
vi.mock("./environment-config.js", async () => ({
|
|
parseEnvironmentDriverConfig: (await vi.importActual<typeof import("./environment-config.js")>("./environment-config.js")).parseEnvironmentDriverConfig,
|
|
resolveEnvironmentDriverConfigForRuntime: state.resolveEnvironment,
|
|
}));
|
|
vi.mock("./plugin-registry.js", () => ({ pluginRegistryService: () => ({ getByKey: state.pluginByKey }) }));
|
|
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, resolveCodexExecutionEngine } from "@paperclipai/adapter-codex-local/server";
|
|
import { secretService } from "./secrets.js";
|
|
import * as localPlatform 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" }];
|
|
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform")!;
|
|
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.kubernetesEnvironment.mockReset().mockResolvedValue(null); state.ensureKubernetesEnvironment.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 };
|
|
expect(agentRunnerAvailability("codex_local", target)).toEqual({ supportedRunners: ["legacy"], defaultRunner: "legacy" });
|
|
const input = { adapterType: "codex_local", adapterConfig: { extraArgs: ["--sandbox", "read-only"] }, target };
|
|
expect(resolveNewAgentRunner(input)).toEqual({ adapterType: input.adapterType, adapterConfig: input.adapterConfig });
|
|
expect(() => resolveNewAgentRunner({ ...input, runner: "paperclip" })).toThrow("unavailable");
|
|
const saved = { adapterType: "paperclip_runner", adapterConfig: { provider: "codex", model: "gpt-5.6-sol" }, target };
|
|
expect(resolveNewAgentRunner(saved)).toEqual({ adapterType: saved.adapterType, adapterConfig: saved.adapterConfig });
|
|
expect(resolveNewAgentRunner({ ...saved, runner: "paperclip" })).toEqual({ adapterType: saved.adapterType, adapterConfig: saved.adapterConfig });
|
|
expect(agentRunnerAvailability("codex_local", { ...target, libc: "glibc" }).defaultRunner).toBe("paperclip");
|
|
}
|
|
});
|
|
it.each([[undefined, false], ["2.38", false], ["2.39", true], ["3.0", true], [" 2.39 ", true], ["malformed", false], ["", false]] as const)("uses the measured local libc version (%s) for creation", async (glibcVersionRuntime, native) => {
|
|
Object.defineProperty(process, "platform", { ...originalPlatform, value: "linux" });
|
|
Object.defineProperty(process, "arch", { ...originalArchitecture, value: "arm64" });
|
|
vi.spyOn(localPlatform, "localAgentRunnerTarget").mockReturnValue({ driver: "local", platform: "linux", architecture: "arm64", libc: glibcVersionRuntime ? "glibc" : "unknown", ...(glibcVersionRuntime ? { glibcVersion: glibcVersionRuntime.trim() } : {}) });
|
|
const localTarget = localPlatform.localAgentRunnerTarget();
|
|
expect(localTarget).toEqual({ driver: "local", platform: "linux", architecture: "arm64", libc: glibcVersionRuntime ? "glibc" : "unknown", ...(glibcVersionRuntime ? { glibcVersion: glibcVersionRuntime.trim() } : {}) });
|
|
expect(agentRunnerAvailability("codex_local").defaultRunner).toBe(native ? "paperclip" : "legacy");
|
|
const saved = await resolveNewAgentRunnerForCompany({} as never, "company", { adapterType: "codex_local" });
|
|
expect(saved.adapterType).toBe(native ? "paperclip_runner" : "codex_local");
|
|
});
|
|
it.each([
|
|
["2.38", false], ["2.39", true], ["2.40", true], ["3.0", true], ["2.9", false],
|
|
["glibc 2.39", true], [" 2.39 ", true], ["2.39.1", true], ["02.039", true],
|
|
["2.39ubuntu1", false], ["unknown", false], ["", false], ["2", false], ["2.-39", false],
|
|
["999999999999999999999999.0", false],
|
|
])("qualifies the measured GNU/Linux daemon ABI for glibc %s (%s)", (glibcVersion, native) => {
|
|
expect(PAPERCLIP_RUNNER_MINIMUM_GLIBC_VERSION).toBe("2.39");
|
|
for (const architecture of ["x64", "arm64"]) {
|
|
const target = { driver: "local", platform: "linux", architecture, libc: "glibc" as const, glibcVersion: String(glibcVersion) };
|
|
expect(paperclipRunnerSupportsPlatform("codex_local", target.platform, architecture, target.libc, target.glibcVersion)).toBe(native);
|
|
expect(agentRunnerAvailability("codex_local", target).defaultRunner).toBe(native ? "paperclip" : "legacy");
|
|
expect(resolveNewAgentRunner({ adapterType: "codex_local", target }).adapterType).toBe(native ? "paperclip_runner" : "codex_local");
|
|
if (!native) expect(() => resolveNewAgentRunner({ adapterType: "codex_local", runner: "paperclip", target })).toThrow("unavailable");
|
|
const saved = { adapterType: "paperclip_runner", adapterConfig: { provider: "codex", model: "gpt-5.6-sol" }, target, runner: "paperclip" as const };
|
|
expect(resolveNewAgentRunner(saved)).toEqual({ adapterType: saved.adapterType, adapterConfig: saved.adapterConfig });
|
|
}
|
|
});
|
|
it("keeps an unidentified local Linux libc legacy if the platform probe is unavailable", () => {
|
|
Object.defineProperty(process, "platform", { ...originalPlatform, value: "linux" });
|
|
vi.spyOn(localPlatform, "localAgentRunnerTarget").mockReturnValue({ driver: "local", platform: "linux", architecture: "x64", libc: "unknown" });
|
|
expect(localPlatform.localAgentRunnerTarget()).toMatchObject({ libc: "unknown" });
|
|
expect(agentRunnerAvailability("codex_local").defaultRunner).toBe("legacy");
|
|
});
|
|
it.each(qualifiedTargets)("uses native Codex automatically with compatible configuration and is idempotent on $platform/$architecture", target => {
|
|
const input = { adapterType: "codex_local", target, adapterConfig: { model: "gpt-5.6-sol", modelReasoningEffort: "high", env: { OPENAI_API_KEY: { type: "secret_ref", secretId: "secret" } }, cwd: "/workspace", instructionsFilePath: "/workspace/AGENTS.md", workspaceStrategy: { type: "git_worktree" }, timeoutSec: 60, search: false, fastMode: false, dangerouslyBypassApprovalsAndSandbox: true } };
|
|
const resolved = resolveNewAgentRunner(input);
|
|
expect(resolved).toMatchObject({ adapterType: "paperclip_runner", adapterConfig: { provider: "codex", codexPermissionMode: "never", modelReasoningEffort: "high", env: input.adapterConfig.env, workspaceStrategy: input.adapterConfig.workspaceStrategy, timeoutSec: 60 } });
|
|
expect(resolved.adapterConfig).toHaveProperty("search", false);
|
|
expect(resolveNewAgentRunner(resolved)).toEqual(resolved);
|
|
expect(input.adapterConfig).toHaveProperty("search", false);
|
|
});
|
|
it.each(["grok_local", "cursor", "process", "external"])("keeps %s default unchanged", adapterType => {
|
|
expect(resolveNewAgentRunner({ adapterType, target, adapterConfig: { custom: "preserved" } })).toEqual({ adapterType, adapterConfig: { custom: "preserved" } });
|
|
});
|
|
it.each(qualifiedTargets.filter(target => !(target.platform === "linux" && target.architecture === "arm64")))("defaults Pi to native on $platform/$architecture and round trips exact thinking", target => {
|
|
const adapterConfig = { model: "openrouter/anthropic/claude-sonnet-4.6", thinking: "high", command: "pi", env: { OPENROUTER_API_KEY: { type: "secret_ref", secretId: "connection" } }, cwd: "/workspace" };
|
|
const selected = resolveNewAgentRunner({ adapterType: "pi_local", adapterConfig, target });
|
|
expect(agentRunnerAvailability("pi_local", target).defaultRunner).toBe("paperclip");
|
|
expect(selected).toEqual({ adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "pi", model: adapterConfig.model, piThinkingLevel: "high", acpxPermissionMode: "approve-all", lifecycleMode: "per_turn", cwd: adapterConfig.cwd, env: adapterConfig.env } });
|
|
expect(resolveNewAgentRunner(selected)).toEqual(selected);
|
|
expect(resolveNewAgentRunner({ ...selected, runner: "legacy" })).toEqual({ adapterType: "pi_local", adapterConfig: { model: adapterConfig.model, thinking: "high", cwd: adapterConfig.cwd, env: adapterConfig.env } });
|
|
});
|
|
it.each(["minimal", "medium", "xhigh"])("keeps unsupported Pi thinking %s explicit", thinking => {
|
|
const input = { adapterType: "pi_local", target, adapterConfig: { model: "custom/model", thinking } };
|
|
expect(() => resolveNewAgentRunner(input)).toThrow("Legacy runner");
|
|
expect(resolveNewAgentRunner({ ...input, runner: "legacy" })).toEqual({ adapterType: input.adapterType, adapterConfig: input.adapterConfig });
|
|
});
|
|
it.each([{ command: "custom-pi" }, { args: ["--extension", "custom.ts"] }, { extraArgs: "--no-tools" }, { stdoutLogMode: "raw" }, { thinking: "low", piThinkingLevel: "high" }])("rejects unsupported Pi configuration %j", extra => {
|
|
expect(() => resolveNewAgentRunner({ adapterType: "pi_local", target, adapterConfig: { model: "custom/model", ...extra } })).toThrow("Legacy runner");
|
|
});
|
|
it.each([undefined, "", "model", "provider/"])("requires a Pi provider/model outside inactive setup: %s", model => {
|
|
expect(() => resolveNewAgentRunner({ adapterType: "pi_local", target, adapterConfig: { model } })).toThrow("provider/model");
|
|
});
|
|
it("allows an inactive Pi draft before model selection and preserves saved native policy", () => {
|
|
expect(resolveNewAgentRunner({ adapterType: "pi_local", target, setupOnly: true })).toMatchObject({ adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "pi", model: "", piThinkingLevel: "low" } });
|
|
const saved = { adapterType: "paperclip_runner", target, adapterConfig: { provider: "acpx", acpxAgent: "pi", model: "custom/model", piThinkingLevel: "max", acpxPermissionMode: "approve-reads", lifecycleMode: "warm", idleTimeoutMs: 1234 } };
|
|
expect(resolveNewAgentRunner(saved)).toEqual({ adapterType: saved.adapterType, adapterConfig: saved.adapterConfig });
|
|
expect(() => resolveNewAgentRunner({ ...saved, runner: "legacy" })).toThrow("Legacy Pi cannot honor");
|
|
});
|
|
it.each([{ driver: "local", platform: "win32", architecture: "x64" }, { driver: "local", platform: "linux", architecture: "arm64", libc: "glibc" as const, glibcVersion: "2.39" }, { driver: "ssh", platform: "linux", architecture: "arm64", libc: "glibc" as const, glibcVersion: "2.39" }, { driver: "sandbox" }, { driver: "local", platform: "linux", architecture: "x64", libc: "musl" as const }])("keeps Pi legacy on unqualified $driver/$platform targets", target => {
|
|
const input = { adapterType: "pi_local", adapterConfig: { model: "custom/model", thinking: "medium" }, target };
|
|
expect(agentRunnerAvailability("pi_local", target).defaultRunner).toBe("legacy");
|
|
expect(resolveNewAgentRunner(input)).toEqual({ adapterType: input.adapterType, adapterConfig: input.adapterConfig });
|
|
expect(() => resolveNewAgentRunner({ ...input, runner: "paperclip" })).toThrow("unavailable");
|
|
});
|
|
it.each(["minimal", "medium", "xhigh"])("honors an explicit Legacy Pi thinking edit to %s when resuming a native draft", thinking => {
|
|
expect(resolveNewAgentRunner({ adapterType: "paperclip_runner", runner: "legacy", adapterConfig: { provider: "acpx", acpxAgent: "pi", model: "custom/model", piThinkingLevel: "max", thinking } })).toEqual({ adapterType: "pi_local", adapterConfig: { model: "custom/model", thinking } });
|
|
});
|
|
it("honors disabled and overridden Pi adapters", () => {
|
|
const input = { adapterType: "pi_local", target, adapterConfig: { model: "custom/model" } };
|
|
state.overridden.add("pi_local");
|
|
expect(resolveNewAgentRunner(input).adapterType).toBe("pi_local");
|
|
state.overridden.clear(); state.disabled = ["paperclip_runner"];
|
|
expect(resolveNewAgentRunner(input).adapterType).toBe("pi_local");
|
|
state.disabled = ["pi_local"];
|
|
expect(() => resolveNewAgentRunner(input)).toThrow("Pi is disabled");
|
|
});
|
|
it.each(qualifiedTargets.filter(target => !(target.platform === "linux" && target.architecture === "arm64")))("defaults OpenCode to its native provider on $platform/$architecture", target => {
|
|
const adapterConfig = { model: "openrouter/anthropic/claude-sonnet-4.6", dangerouslySkipPermissions: true, command: "opencode", env: { OPENROUTER_API_KEY: { type: "secret_ref", secretId: "connection" } }, cwd: "/workspace", instructionsFilePath: "/workspace/AGENTS.md" };
|
|
const selected = resolveNewAgentRunner({ adapterType: "opencode_local", adapterConfig, target });
|
|
expect(agentRunnerAvailability("opencode_local", target).defaultRunner).toBe("paperclip");
|
|
expect(selected).toEqual({ adapterType: "paperclip_runner", adapterConfig: { provider: "opencode", model: adapterConfig.model, env: adapterConfig.env, cwd: adapterConfig.cwd, instructionsFilePath: adapterConfig.instructionsFilePath, opencodePermissionMode: "allow", lifecycleMode: "per_turn" } });
|
|
expect(resolveNewAgentRunner(selected)).toEqual(selected);
|
|
expect(resolveNewAgentRunner({ ...selected, runner: "legacy" })).toMatchObject({ adapterType: "opencode_local", adapterConfig: { model: adapterConfig.model, dangerouslySkipPermissions: true } });
|
|
});
|
|
it.each([{ driver: "local", platform: "win32", architecture: "x64" }, { driver: "local", platform: "linux", architecture: "arm64", libc: "glibc" as const, glibcVersion: "2.39" }, { driver: "ssh", platform: "linux", architecture: "arm64", libc: "glibc" as const, glibcVersion: "2.39" }, { driver: "sandbox", platform: "linux", architecture: "arm64", libc: "glibc" as const, glibcVersion: "2.39" }, { driver: "local", platform: "linux", architecture: "arm64", libc: "musl" as const }, { driver: "local", platform: "linux", architecture: "x64", libc: "glibc" as const, glibcVersion: "2.38" }, { driver: "sandbox" }])("keeps OpenCode legacy on unqualified $driver/$platform targets", target => {
|
|
const input = { adapterType: "opencode_local", adapterConfig: { model: "custom/model", command: "custom-opencode" }, target };
|
|
expect(agentRunnerAvailability("opencode_local", target)).toEqual({ supportedRunners: ["legacy"], defaultRunner: "legacy" });
|
|
expect(resolveNewAgentRunner(input)).toEqual({ adapterType: input.adapterType, adapterConfig: input.adapterConfig });
|
|
expect(() => resolveNewAgentRunner({ ...input, runner: "paperclip" })).toThrow("unavailable");
|
|
});
|
|
it.each([{ command: "custom-opencode" }, { variant: "high" }, { extraArgs: ["--agent", "plan"] }, { args: ["--attach", "http://localhost"] }, { dangerouslySkipPermissions: false }])("rejects unsupported OpenCode settings instead of discarding them: %j", adapterConfig => {
|
|
const input = { adapterType: "opencode_local", adapterConfig: { model: "custom/model", ...adapterConfig }, target };
|
|
expect(() => resolveNewAgentRunner(input)).toThrow("Legacy runner");
|
|
expect(resolveNewAgentRunner({ ...input, runner: "legacy" })).toEqual({ adapterType: input.adapterType, adapterConfig: input.adapterConfig });
|
|
});
|
|
it.each([undefined, "", "model", "provider/"])("requires an explicit OpenCode provider/model: %s", model => {
|
|
expect(() => resolveNewAgentRunner({ adapterType: "opencode_local", adapterConfig: { model }, target })).toThrow("provider/model");
|
|
});
|
|
it("stores an inactive native OpenCode setup draft before model selection, but cannot hire it", () => {
|
|
const input = { adapterType: "opencode_local", target };
|
|
expect(resolveNewAgentRunner({ ...input, setupOnly: true })).toEqual({ adapterType: "paperclip_runner", adapterConfig: { provider: "opencode", model: "", opencodePermissionMode: "allow", lifecycleMode: "per_turn" } });
|
|
expect(() => resolveNewAgentRunner(input)).toThrow("provider/model");
|
|
});
|
|
it("honors native OpenCode permissions and refuses to discard them on return to legacy", () => {
|
|
const selected = resolveNewAgentRunner({ adapterType: "opencode_local", adapterConfig: { model: "custom/model", opencodePermissionMode: "ask" }, target });
|
|
expect(selected.adapterConfig.opencodePermissionMode).toBe("ask");
|
|
expect(() => resolveNewAgentRunner({ ...selected, runner: "legacy" })).toThrow("opencodePermissionMode");
|
|
});
|
|
it("resolves OpenCode against the company sandbox qualification and preserves disabled/overridden adapters", async () => {
|
|
const input = { adapterType: "opencode_local", adapterConfig: { model: "custom/model" } };
|
|
state.settings = { defaultEnvironmentId: "sandbox" };
|
|
state.getEnvironment.mockResolvedValue({ id: "sandbox", driver: "sandbox", status: "active", config: { provider: "daytona" } });
|
|
expect((await resolveNewAgentRunnerForCompany({} as never, "company", input)).adapterType).toBe("opencode_local");
|
|
state.sandboxQualification.mockResolvedValue({ platform: "linux", architecture: "x64", libc: "glibc", glibcVersion: "2.39" });
|
|
expect((await resolveNewAgentRunnerForCompany({} as never, "company", input)).adapterType).toBe("paperclip_runner");
|
|
state.overridden.add("opencode_local");
|
|
expect(resolveNewAgentRunner({ ...input, target }).adapterType).toBe("opencode_local");
|
|
state.overridden.clear(); state.disabled = ["paperclip_runner"];
|
|
expect(resolveNewAgentRunner({ ...input, target }).adapterType).toBe("opencode_local");
|
|
state.disabled = ["opencode_local"];
|
|
expect(() => resolveNewAgentRunner({ ...input, target })).toThrow("OpenCode is disabled");
|
|
});
|
|
it.each(qualifiedTargets)("allows an explicit legacy runner and native to legacy round trip on $platform/$architecture", target => {
|
|
expect(resolveNewAgentRunner({ adapterType: "codex_local", target, runner: "legacy", adapterConfig: { extraArgs: ["--search"] } }).adapterType).toBe("codex_local");
|
|
expect(resolveNewAgentRunner({ adapterType: "paperclip_runner", runner: "legacy", adapterConfig: { provider: "codex", model: "gpt-5.6-sol", lifecycleMode: "per_turn", codexPermissionMode: "never", modelReasoningEffort: "high" } })).toEqual({ adapterType: "codex_local", adapterConfig: { model: "gpt-5.6-sol", modelReasoningEffort: "high", dangerouslyBypassApprovalsAndSandbox: true } });
|
|
});
|
|
it("preserves explicit legacy permission policy alongside compatible native lifecycle defaults", () => {
|
|
expect(resolveNewAgentRunner({ adapterType: "codex_local", runner: "legacy", adapterConfig: { dangerouslyBypassApprovalsAndSandbox: false, lifecycleMode: "per_turn" } }))
|
|
.toEqual({ adapterType: "codex_local", adapterConfig: { dangerouslyBypassApprovalsAndSandbox: false } });
|
|
expect(resolveNewAgentRunner({ adapterType: "paperclip_runner", runner: "legacy", adapterConfig: { provider: "codex", codexPermissionMode: "never", lifecycleMode: "per_turn", dangerouslyBypassApprovalsAndSandbox: false } }))
|
|
.toEqual({ adapterType: "codex_local", adapterConfig: { dangerouslyBypassApprovalsAndSandbox: false } });
|
|
});
|
|
it.each([{ platform: "win32", architecture: "x64" }, { platform: "linux", architecture: "arm" }, { platform: "darwin", architecture: "ia32" }])("keeps unqualified $platform/$architecture targets legacy while preserving explicit native profiles", ({ platform, architecture }) => {
|
|
const input = { adapterType: "codex_local", target: { driver: "local", platform, architecture } };
|
|
expect(resolveNewAgentRunner(input).adapterType).toBe("codex_local");
|
|
expect(() => resolveNewAgentRunner({ ...input, runner: "paperclip" })).toThrow("unavailable");
|
|
const saved = { adapterType: "paperclip_runner", adapterConfig: { provider: "codex", model: "gpt-5.6-sol" }, target: input.target };
|
|
expect(resolveNewAgentRunner(saved)).toEqual({ adapterType: saved.adapterType, adapterConfig: saved.adapterConfig });
|
|
expect(resolveNewAgentRunner({ ...saved, runner: "paperclip" })).toEqual({ adapterType: saved.adapterType, adapterConfig: saved.adapterConfig });
|
|
});
|
|
it.each(qualifiedTargets)("automatically keeps unsupported targets and active external overrides legacy on $platform/$architecture", target => {
|
|
const input = { adapterType: "codex_local", target: { driver: "local", platform: "win32", architecture: "x64" } };
|
|
expect(resolveNewAgentRunner(input).adapterType).toBe("codex_local");
|
|
expect(() => resolveNewAgentRunner({ ...input, runner: "paperclip" })).toThrow("unavailable");
|
|
state.overridden.add("codex_local");
|
|
expect(agentRunnerAvailability("codex_local", target).defaultRunner).toBe("legacy");
|
|
expect(resolveNewAgentRunner({ adapterType: "codex_local", target }).adapterType).toBe("codex_local");
|
|
state.overridden.clear();
|
|
state.overridden.add("paperclip_runner");
|
|
expect(resolveNewAgentRunner({ adapterType: "codex_local", target }).adapterType).toBe("codex_local");
|
|
});
|
|
it.each(qualifiedTargets)("does not select a disabled runner on $platform/$architecture", target => {
|
|
state.disabled = ["paperclip_runner"];
|
|
expect(resolveNewAgentRunner({ adapterType: "codex_local", target }).adapterType).toBe("codex_local");
|
|
expect(() => resolveNewAgentRunner({ adapterType: "codex_local", target, runner: "paperclip" })).toThrow("unavailable");
|
|
state.disabled = ["codex_local"];
|
|
expect(() => resolveNewAgentRunner({ adapterType: "codex_local", target })).toThrow("Codex is disabled");
|
|
});
|
|
it.each(["extraArgs", "agentCommand", "networkScope", "filesystemScope", "search"])("rejects incompatible %s without changing runner", field => {
|
|
expect(() => resolveNewAgentRunner({ adapterType: "codex_local", target, adapterConfig: { [field]: true } })).toThrow("Legacy runner");
|
|
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.each([undefined, "acp"])("rejects effective Legacy ACP network denial for engine=%s before native selection", async engine => {
|
|
const adapterConfig = { ...(engine ? { engine } : {}), env: { PAPERCLIP_CODEX_ACP_NETWORK_ACCESS: "false" } };
|
|
expect(resolveCodexExecutionEngine(adapterConfig).engine).toBe("acp");
|
|
expect(buildCodexAcpConfig(adapterConfig)).toMatchObject({ env: { PAPERCLIP_CODEX_ACP_NETWORK_ACCESS: "false" } });
|
|
const { codexNetworkAccess } = await import(new URL(
|
|
"../../../packages/paperclip-runner/src/drivers/codex/codex-security-config.ts", import.meta.url,
|
|
).href);
|
|
expect(codexNetworkAccess({ PAPERCLIP_RUNNER_NETWORK_ACCESS: "enabled", PAPERCLIP_CODEX_ACP_NETWORK_ACCESS: "false" })).toBe(true);
|
|
for (const runner of [undefined, "auto", "paperclip"] as const) {
|
|
expect(() => resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target, runner }))
|
|
.toThrow(expect.objectContaining({ status: 422, details: { code: "agent_runner_config_incompatible", fields: ["env.PAPERCLIP_CODEX_ACP_NETWORK_ACCESS"] } }));
|
|
}
|
|
expect(resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target, runner: "legacy" }))
|
|
.toEqual({ adapterType: "codex_local", adapterConfig });
|
|
expect(adapterConfig.env).toEqual({ PAPERCLIP_CODEX_ACP_NETWORK_ACCESS: "false" });
|
|
});
|
|
|
|
it.each([
|
|
{ type: "plain", value: "false" },
|
|
{ type: "secret_ref", secretId: "fixture-policy", version: "latest" },
|
|
{ type: "user_secret_ref", key: "fixture-policy", version: "latest" },
|
|
])("does not discard a bound ACP network restriction or guess its value (%j)", async binding => {
|
|
const adapterConfig = { engine: "acp", env: { PAPERCLIP_CODEX_ACP_NETWORK_ACCESS: binding } };
|
|
if (binding.type === "plain") {
|
|
const resolved = await secretService({} as never).resolveEnvBindings("fixture-company", adapterConfig.env);
|
|
expect(buildCodexAcpConfig({ ...adapterConfig, env: resolved.env }))
|
|
.toMatchObject({ env: { PAPERCLIP_CODEX_ACP_NETWORK_ACCESS: "false" } });
|
|
}
|
|
expect(() => resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target }))
|
|
.toThrow(expect.objectContaining({ status: 422, details: { code: "agent_runner_config_incompatible", fields: ["env.PAPERCLIP_CODEX_ACP_NETWORK_ACCESS"] } }));
|
|
expect(resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target, runner: "legacy" }))
|
|
.toEqual({ adapterType: "codex_local", adapterConfig });
|
|
});
|
|
|
|
it.each([
|
|
{ extraArgs: ["-c", "sandbox_workspace_write.network_access=true"] },
|
|
{ extraArgs: [], args: ["-c", "sandbox_workspace_write.network_access=true"] },
|
|
])("retains actionable argument errors when Legacy arguments override a network flag (%j)", args => {
|
|
const adapterConfig = { engine: "acp", ...args, env: { PAPERCLIP_CODEX_ACP_NETWORK_ACCESS: "false" } };
|
|
expect(buildCodexAcpConfig(adapterConfig)).toMatchObject({ env: { PAPERCLIP_CODEX_ACP_NETWORK_ACCESS: "true" } });
|
|
expect(() => resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target }))
|
|
.toThrow(expect.objectContaining({ status: 422, details: { code: "agent_runner_config_incompatible", fields: [args.extraArgs.length ? "extraArgs" : "args"] } }));
|
|
});
|
|
|
|
it.each(["true", "False", " false ", { type: "plain", value: "true" }])("keeps compatible ACP environment settings (%j) and native idempotence", value => {
|
|
const adapterConfig = { engine: "acp", env: { PAPERCLIP_CODEX_ACP_NETWORK_ACCESS: value, OPENAI_API_KEY: { type: "secret_ref", secretId: "fixture-api-key" } } };
|
|
const literal = typeof value === "string" ? value : value.value;
|
|
expect(buildCodexAcpConfig({ ...adapterConfig, env: { PAPERCLIP_CODEX_ACP_NETWORK_ACCESS: literal } }))
|
|
.toMatchObject({ env: { PAPERCLIP_CODEX_ACP_NETWORK_ACCESS: "true" } });
|
|
const resolved = resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target });
|
|
expect(resolved.adapterType).toBe("paperclip_runner");
|
|
expect(resolved.adapterConfig.env).toEqual(adapterConfig.env);
|
|
expect(resolveNewAgentRunner(resolved)).toEqual(resolved);
|
|
});
|
|
|
|
it("keeps a saved native profile and an explicitly selected Legacy CLI's unused ACP flag unchanged", () => {
|
|
const adapterConfig = { engine: "cli", env: { PAPERCLIP_CODEX_ACP_NETWORK_ACCESS: "false" } };
|
|
expect(resolveCodexExecutionEngine(adapterConfig).engine).toBe("cli");
|
|
expect(resolveNewAgentRunner({ adapterType: "codex_local", adapterConfig, target }).adapterType).toBe("paperclip_runner");
|
|
const saved = { adapterType: "paperclip_runner", adapterConfig: { provider: "codex", env: adapterConfig.env } };
|
|
expect(resolveNewAgentRunner(saved)).toEqual(saved);
|
|
});
|
|
|
|
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 });
|
|
});
|
|
it("advertises Legacy for a forced Kubernetes target before lazy environment creation", async () => {
|
|
state.settings = { general: { executionMode: "kubernetes" } };
|
|
const selected = await resolveAgentRunnerTargetForCompany({} as never, "company");
|
|
expect(selected).toEqual({ driver: "sandbox" });
|
|
expect(agentRunnerAvailability("codex_local", selected)).toEqual({ supportedRunners: ["legacy"], defaultRunner: "legacy" });
|
|
expect(state.kubernetesEnvironment).toHaveBeenCalledWith("company");
|
|
expect(state.ensureKubernetesEnvironment).not.toHaveBeenCalled();
|
|
expect(state.resolveEnvironment).not.toHaveBeenCalled();
|
|
expect(state.sandboxQualification).not.toHaveBeenCalled();
|
|
expect(state.ssh).not.toHaveBeenCalled();
|
|
});
|
|
it.each([undefined, "auto"] as const)("preserves Codex %s creation before a forced Kubernetes environment exists", async runner => {
|
|
state.settings = { general: { executionMode: "kubernetes" } };
|
|
const adapterConfig = { model: "gpt-5.6-sol", extraArgs: ["--search"], env: { OPENAI_API_KEY: { type: "secret_ref", secretId: "synthetic-secret" } } };
|
|
const resolved = await resolveNewAgentRunnerForCompany({} as never, "company", { adapterType: "codex_local", adapterConfig, runner });
|
|
expect(resolved).toEqual({ adapterType: "codex_local", adapterConfig });
|
|
expect(state.ensureKubernetesEnvironment).not.toHaveBeenCalled();
|
|
expect(state.resolveEnvironment).not.toHaveBeenCalled();
|
|
expect(state.sandboxQualification).not.toHaveBeenCalled();
|
|
expect(state.ssh).not.toHaveBeenCalled();
|
|
});
|
|
it("rejects explicit Paperclip execution on an unqualified forced Kubernetes target without provisioning", async () => {
|
|
state.settings = { general: { executionMode: "kubernetes" } };
|
|
await expect(resolveNewAgentRunnerForCompany({} as never, "company", { adapterType: "codex_local", runner: "paperclip" }))
|
|
.rejects.toMatchObject({ status: 422, message: expect.stringContaining("Legacy runner"), details: { code: "agent_runner_unavailable" } });
|
|
expect(state.ensureKubernetesEnvironment).not.toHaveBeenCalled();
|
|
expect(state.resolveEnvironment).not.toHaveBeenCalled();
|
|
expect(state.sandboxQualification).not.toHaveBeenCalled();
|
|
expect(state.ssh).not.toHaveBeenCalled();
|
|
});
|
|
it("preserves explicit Legacy and recorded native execution without reading the forced Kubernetes target", async () => {
|
|
state.settings = { general: { executionMode: "kubernetes" } };
|
|
for (const input of [
|
|
{ adapterType: "codex_local", runner: "legacy" as const, adapterConfig: { extraArgs: ["--search"] } },
|
|
{ adapterType: "paperclip_runner", adapterConfig: { provider: "codex", model: "gpt-5.6-sol" } },
|
|
]) {
|
|
expect(await resolveNewAgentRunnerForCompany({} as never, "company", input))
|
|
.toEqual({ adapterType: input.adapterType, adapterConfig: input.adapterConfig });
|
|
}
|
|
expect(state.kubernetesEnvironment).not.toHaveBeenCalled();
|
|
expect(state.ensureKubernetesEnvironment).not.toHaveBeenCalled();
|
|
});
|
|
it("retains company checks for an existing forced Kubernetes environment", async () => {
|
|
state.settings = { general: { executionMode: "kubernetes" } };
|
|
state.kubernetesEnvironment.mockResolvedValue({ id: "kubernetes", driver: "sandbox", status: "active", config: { provider: "kubernetes" } });
|
|
state.bindings.mockResolvedValue(["another-company"]);
|
|
await expect(resolveNewAgentRunnerForCompany({} as never, "company", { adapterType: "codex_local" }))
|
|
.rejects.toMatchObject({ status: 403, details: { code: "environment_company_mismatch" } });
|
|
expect(state.ensureKubernetesEnvironment).not.toHaveBeenCalled();
|
|
expect(state.sandboxQualification).not.toHaveBeenCalled();
|
|
expect(state.resolveEnvironment).not.toHaveBeenCalled();
|
|
});
|
|
it("rejects a foreign environment before resolving credentials or probing its platform", async () => {
|
|
state.getEnvironment.mockResolvedValue({ id: "foreign", driver: "ssh", status: "active", config: {} });
|
|
state.bindings.mockResolvedValue(["another-company"]);
|
|
await expect(resolveNewAgentRunnerForCompany({} as never, "company", { adapterType: "codex_local", defaultEnvironmentId: "foreign" })).rejects.toMatchObject({ status: 403, details: { code: "environment_company_mismatch" } });
|
|
expect(state.resolveEnvironment).not.toHaveBeenCalled(); expect(state.ssh).not.toHaveBeenCalled();
|
|
});
|
|
it("uses dispatch's instance default for a null agent override and checks the selected SSH platform", async () => {
|
|
state.settings = { defaultEnvironmentId: "default" };
|
|
state.getEnvironment.mockResolvedValue({ id: "default", driver: "ssh", status: "active", config: {} });
|
|
state.ssh.mockResolvedValue({ stdout: "Linux\naarch64\n" });
|
|
const result = await resolveNewAgentRunnerForCompany({} as never, "company", { adapterType: "codex_local", defaultEnvironmentId: null });
|
|
expect(state.getEnvironment).toHaveBeenCalledWith("default"); expect(state.bindings).toHaveBeenCalledWith("default");
|
|
expect(result.adapterType).toBe("paperclip_runner");
|
|
});
|
|
it.each([
|
|
["Linux\nx86_64\n", "paperclip", "paperclip_runner"],
|
|
["Darwin\narm64\n", "paperclip", "paperclip_runner"],
|
|
["Darwin\nx86_64\n", "paperclip", "paperclip_runner"],
|
|
["Linux\naarch64\n", "paperclip", "paperclip_runner"],
|
|
["Linux\narm64\n", "paperclip", "paperclip_runner"],
|
|
["Linux\nx86_64\nglibc\n", "paperclip", "paperclip_runner"],
|
|
["Linux\naarch64\nglibc\n", "paperclip", "paperclip_runner"],
|
|
["Linux\nx86_64\nglibc\nglibc 2.38\n", "legacy", "codex_local"],
|
|
["Linux\naarch64\nglibc\nglibc 2.38\n", "legacy", "codex_local"],
|
|
["Linux\nx86_64\nglibc\nglibc 2.39\n", "paperclip", "paperclip_runner"],
|
|
["Linux\naarch64\nglibc\nglibc 3.0\n", "paperclip", "paperclip_runner"],
|
|
["Linux\nx86_64\nglibc\nmalformed\n", "legacy", "codex_local"],
|
|
["Linux\nx86_64\nmusl\n", "legacy", "codex_local"],
|
|
["Linux\naarch64\nmusl\n", "legacy", "codex_local"],
|
|
["Linux\nx86_64\nunknown\n", "legacy", "codex_local"],
|
|
["Linux\narmv7l\n", "legacy", "codex_local"],
|
|
])("uses the same selected SSH target for discovery and saving (%s)", async (stdout, expectedRunner, expectedAdapter) => {
|
|
state.getEnvironment.mockResolvedValue({ id: "selected", driver: "ssh", status: "active", config: {} });
|
|
state.ssh.mockResolvedValue({ stdout });
|
|
const discoveredTarget = await resolveAgentRunnerTargetForCompany({} as never, "company", "selected");
|
|
expect(agentRunnerAvailability("codex_local", discoveredTarget).defaultRunner).toBe(expectedRunner);
|
|
const saved = await resolveNewAgentRunnerForCompany({} as never, "company", { adapterType: "codex_local", defaultEnvironmentId: "selected" });
|
|
expect(saved.adapterType).toBe(expectedAdapter);
|
|
expect(state.ssh).toHaveBeenCalledTimes(2);
|
|
expect(state.ssh).toHaveBeenCalledWith({}, AGENT_RUNNER_SSH_PLATFORM_COMMAND, { timeoutMs: 10_000 });
|
|
});
|
|
it("preserves recorded explicit native execution without rechecking creation defaults", async () => {
|
|
const recorded = { adapterType: "paperclip_runner", adapterConfig: { provider: "codex", model: "gpt-5.6-sol" }, runner: "paperclip" as const };
|
|
expect(await resolveNewAgentRunnerForCompany({} as never, "company", recorded)).toEqual({ adapterType: recorded.adapterType, adapterConfig: recorded.adapterConfig });
|
|
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("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" } });
|
|
});
|
|
});
|
|
|
|
describe("Codex creation runner contract", () => {
|
|
it("graduates qualified harnesses and retains saved provider identity", () => {
|
|
expect(paperclipRunnerProfileForHarness("codex_local")).toEqual({ provider: "codex" });
|
|
expect(paperclipRunnerProfileForHarness("claude_local")).toEqual({ provider: "acpx", acpxAgent: "claude" });
|
|
expect(paperclipRunnerProfileForHarness("opencode_local")).toEqual({ provider: "opencode" });
|
|
expect(paperclipRunnerProfileForHarness("pi_local")).toEqual({ provider: "acpx", acpxAgent: "pi" });
|
|
for (const harness of ["grok_local", "cursor", "process"]) expect(paperclipRunnerProfileForHarness(harness)).toBeUndefined();
|
|
expect(agentHarnessType("paperclip_runner", { provider: "codex" })).toBe("codex_local");
|
|
expect(agentHarnessType("paperclip_runner", { provider: "acpx", acpxAgent: "claude" })).toBe("claude_local");
|
|
expect(agentHarnessType("paperclip_runner", { provider: "unknown" })).toBe("unknown");
|
|
expect(agentHarnessType("paperclip_runner", { provider: "acpx", acpxAgent: "unknown" })).toBe("acpx:unknown");
|
|
expect(agentRunner("codex_local")).toBe("legacy");
|
|
expect(agentRunner("paperclip_runner")).toBe("paperclip");
|
|
});
|
|
it("qualifies only the Codex daemon targets shipped in public server packages", () => {
|
|
for (const { platform, architecture } of qualifiedTargets) expect(paperclipRunnerSupportsPlatform("codex_local", platform, architecture)).toBe(true);
|
|
for (const architecture of ["x64", "arm64"]) {
|
|
expect(paperclipRunnerSupportsPlatform("codex_local", "linux", architecture, "glibc")).toBe(true);
|
|
expect(paperclipRunnerSupportsPlatform("codex_local", "linux", architecture, "musl")).toBe(false);
|
|
expect(paperclipRunnerSupportsPlatform("codex_local", "linux", architecture, "unknown")).toBe(false);
|
|
}
|
|
for (const [platform, architecture] of [["darwin", "ia32"], ["linux", "arm"], ["win32", "x64"], ["win32", "arm64"], ["freebsd", "x64"]]) expect(paperclipRunnerSupportsPlatform("codex_local", platform, architecture)).toBe(false);
|
|
expect(paperclipRunnerSupportsPlatform("claude_local", "darwin", "arm64")).toBe(true);
|
|
});
|
|
it("keeps auto out of saved update defaults and validates request intent", () => {
|
|
expect(createAgentSchema.parse({ name: "Codex", adapterType: "codex_local" }).runner).toBeUndefined();
|
|
expect(updateAgentSchema.parse({ title: "same execution" })).not.toHaveProperty("runner");
|
|
expect(testAdapterEnvironmentSchema.parse({ runner: "legacy" }).runner).toBe("legacy");
|
|
expect(createAgentSchema.safeParse({ name: "Codex", adapterType: "codex_local", runner: "other" }).success).toBe(false);
|
|
});
|
|
});
|
|
|
|
|
|
describe("Runner-only plugin environments", () => {
|
|
const pluginEnvironment = { id: "computer", driver: "plugin", status: "active", config: { pluginKey: "acme.computers", driverKey: "tasks", driverConfig: {} } };
|
|
const plugin = (driver: Record<string, unknown>, capabilities = ["environment.drivers.register"]) => ({
|
|
id: "plugin-id", status: "ready", manifestJson: { capabilities, environmentDrivers: [{ driverKey: "tasks", displayName: "Tasks", configSchema: {}, ...driver }] },
|
|
});
|
|
beforeEach(() => {
|
|
state.disabled = []; state.overridden.clear(); state.settings = {};
|
|
state.getEnvironment.mockReset().mockResolvedValue(pluginEnvironment); state.bindings.mockReset().mockResolvedValue([]);
|
|
state.kubernetesEnvironment.mockReset().mockResolvedValue(null); state.ssh.mockReset(); state.sandboxQualification.mockReset();
|
|
state.pluginByKey.mockReset().mockResolvedValue(plugin({ supportsTasks: true }));
|
|
});
|
|
it("offers only Paperclip Runner for Codex and Claude without a platform", async () => {
|
|
const target = await resolveAgentRunnerTargetForCompany({} as never, "company", "computer");
|
|
expect(target).toEqual({ driver: "plugin", runnerTasks: true });
|
|
expect(state.pluginByKey).toHaveBeenCalledWith("acme.computers");
|
|
for (const harness of ["codex_local", "claude_local"]) {
|
|
expect(agentRunnerAvailability(harness, target)).toEqual({ supportedRunners: ["paperclip"], defaultRunner: "paperclip" });
|
|
}
|
|
expect(agentRunnerAvailability("gemini_local", target)).toEqual({ supportedRunners: ["legacy"], defaultRunner: "legacy" });
|
|
expect(state.ssh).not.toHaveBeenCalled();
|
|
});
|
|
it("saves a Claude agent on the plugin environment as Paperclip Runner", async () => {
|
|
const saved = await resolveNewAgentRunnerForCompany({} as never, "company", { adapterType: "claude_local", defaultEnvironmentId: "computer" });
|
|
expect(saved).toMatchObject({ adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "claude" } });
|
|
const explicit = await resolveNewAgentRunnerForCompany({} as never, "company", { adapterType: "claude_local", runner: "paperclip", defaultEnvironmentId: "computer" });
|
|
expect(explicit.adapterType).toBe("paperclip_runner");
|
|
});
|
|
it.each([
|
|
["a driver without Runner tasks", plugin({})],
|
|
["a different driver key", { ...plugin({ supportsTasks: true }), manifestJson: { capabilities: ["environment.drivers.register"], environmentDrivers: [{ driverKey: "other", supportsTasks: true }] } }],
|
|
["a plugin without the driver capability", plugin({ supportsTasks: true }, [])],
|
|
["an unknown plugin", null],
|
|
])("keeps the existing plugin target for %s", async (_label, record) => {
|
|
state.pluginByKey.mockResolvedValue(record);
|
|
const target = await resolveAgentRunnerTargetForCompany({} as never, "company", "computer");
|
|
expect(target).toEqual({ driver: "plugin" });
|
|
expect(agentRunnerAvailability("claude_local", target)).toEqual({ supportedRunners: ["legacy"], defaultRunner: "legacy" });
|
|
});
|
|
it("ignores a malformed plugin environment config", async () => {
|
|
state.getEnvironment.mockResolvedValue({ ...pluginEnvironment, config: { pluginKey: "acme.computers" } });
|
|
expect(await resolveAgentRunnerTargetForCompany({} as never, "company", "computer")).toEqual({ driver: "plugin" });
|
|
expect(state.pluginByKey).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("Claude runner defaults", () => {
|
|
const mac = { driver: "local", platform: "darwin", architecture: "arm64" };
|
|
beforeEach(() => { state.disabled = []; state.overridden.clear(); });
|
|
it.each(qualifiedTargets)("defaults Claude to the qualified $platform $architecture profile", target => {
|
|
expect(agentRunnerAvailability("claude_local", target)).toEqual({ supportedRunners: ["paperclip", "legacy"], defaultRunner: "paperclip" });
|
|
expect(resolveNewAgentRunner({ adapterType: "claude_local", target })).toEqual({
|
|
adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "claude", model: "claude-opus-5-5", acpxPermissionMode: "approve-all", lifecycleMode: "per_turn" },
|
|
});
|
|
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");
|
|
}
|
|
expect(agentRunnerAvailability("claude_local", { driver: "local", platform: "linux", architecture, libc: "glibc", glibcVersion: "2.17" }).defaultRunner).toBe("legacy");
|
|
});
|
|
it("saves native Claude with Opus 5.5, preserving credentials and shared settings", () => {
|
|
const config = { env: { ANTHROPIC_API_KEY: { type: "secret_ref", secretId: "key" } }, instructionsFilePath: "/workspace/AGENTS.md", timeoutSec: 180, dangerouslySkipPermissions: true };
|
|
const result = resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: config });
|
|
expect(result).toEqual({ adapterType: "paperclip_runner", adapterConfig: { env: config.env, instructionsFilePath: config.instructionsFilePath, timeoutSec: 180, provider: "acpx", acpxAgent: "claude", model: "claude-opus-5-5", acpxPermissionMode: "approve-all", lifecycleMode: "per_turn" } });
|
|
expect(resolveNewAgentRunner(result)).toEqual(result);
|
|
expect(config.dangerouslySkipPermissions).toBe(true);
|
|
});
|
|
it("preserves explicit models and Legacy choices", () => {
|
|
const legacy = { adapterType: "claude_local", target: mac, runner: "legacy" as const, adapterConfig: { model: "custom", chrome: true } };
|
|
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 } });
|
|
expect(native.adapterConfig.acpxPermissionMode).toBe(mode);
|
|
expect(native.adapterConfig[key]).toBeUndefined();
|
|
}
|
|
expect(() => resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: { [key]: "deny-all", acpxPermissionMode: "approve-all" } })).toThrow("cannot honor");
|
|
expect(() => resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: { [key]: "unrecognized" } })).toThrow("Legacy runner");
|
|
});
|
|
it("retains saved native Claude and allows an explicit compatible Legacy switch", () => {
|
|
const saved = { adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "claude", model: "custom", acpxPermissionMode: "approve-all", lifecycleMode: "per_turn" } };
|
|
expect(resolveNewAgentRunner(saved)).toEqual(saved);
|
|
expect(resolveNewAgentRunner({ ...saved, runner: "legacy" })).toEqual({ adapterType: "claude_local", adapterConfig: { model: "custom", dangerouslySkipPermissions: true } });
|
|
expect(() => resolveNewAgentRunner({ ...saved, runner: "legacy", adapterConfig: { ...saved.adapterConfig, acpxPermissionMode: "deny-all" } })).toThrow("cannot honor");
|
|
});
|
|
it.each(["maxTurnsPerRun", "chrome", "warmHandleIdleMs", "acpWarmHandleIdleMs"])("warns and ignores obsolete Claude %s on conversion and native saves", field => {
|
|
for (const adapterType of ["claude_local", "paperclip_runner"]) {
|
|
const onConfigurationWarnings = vi.fn();
|
|
const result = resolveNewAgentRunner({ adapterType, target: mac, runner: "paperclip",
|
|
adapterConfig: { provider: "acpx", acpxAgent: "claude", model: "claude-opus-5-5", [field]: 1000 }, onConfigurationWarnings });
|
|
expect(result.adapterType).toBe("paperclip_runner");
|
|
expect(result.adapterConfig).not.toHaveProperty(field);
|
|
expect(onConfigurationWarnings).toHaveBeenCalledWith([expect.objectContaining({ field, code: "runner_option_ignored" })]);
|
|
}
|
|
});
|
|
it.each(["agentCommand", "networkScope"])("rejects unsupported %s without fallback", key => {
|
|
expect(() => resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: { [key]: "configured" } })).toThrow("Legacy runner");
|
|
});
|
|
it.each(["low", "medium", "high", "xhigh", "max"])("translates Claude effort %s and preserves it on an explicit Legacy switch", effort => {
|
|
const native = resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: { effort } });
|
|
expect(native.adapterConfig.modelReasoningEffort).toBe(effort);
|
|
expect(native.adapterConfig.effort).toBeUndefined();
|
|
expect(resolveNewAgentRunner({ ...native, runner: "legacy" }).adapterConfig).toMatchObject({ effort });
|
|
});
|
|
it("rejects invalid effort and unknown platform defaults", () => {
|
|
expect(() => resolveNewAgentRunner({ adapterType: "claude_local", target: mac, adapterConfig: { effort: "ultra" } })).toThrow("Claude effort");
|
|
expect(agentRunnerAvailability("claude_local", { driver: "sandbox" }).defaultRunner).toBe("legacy");
|
|
});
|
|
it("keeps unqualified targets and overrides legacy", () => {
|
|
expect(agentRunnerAvailability("claude_local", { driver: "local", platform: "win32", architecture: "x64" }).defaultRunner).toBe("legacy");
|
|
state.overridden.add("claude_local");
|
|
expect(agentRunnerAvailability("claude_local", mac).defaultRunner).toBe("legacy");
|
|
state.overridden.clear(); state.disabled = ["claude_local"];
|
|
expect(() => resolveNewAgentRunner({ adapterType: "claude_local", target: mac })).toThrow("Claude is disabled");
|
|
});
|
|
});
|