diff --git a/doc/plans/2026-10-06-default-agent-runner.md b/doc/plans/2026-10-06-default-agent-runner.md index 0ff7292e61..199012a25a 100644 --- a/doc/plans/2026-10-06-default-agent-runner.md +++ b/doc/plans/2026-10-06-default-agent-runner.md @@ -4,7 +4,7 @@ Updated 2026-10-12. ## Latest checkpoint: ARM qualification and master integration -The native runtime, protocol, Codex adapter/driver, selection code, and provider +The Rust daemon, protocol, Codex driver, creation policy, and provider requirements remain unchanged from qualified source `a200495cc7488f4d59da570407e516cf702e5af8`. Its verified 18-package archive retains that literal identity. All four hosted daemon producers, Linux x64 clean @@ -53,7 +53,32 @@ The last verified preview restart serves `2026.1005.0+352.git.0690801f13` at configurations and both historical native runs. The downloaded Apple Silicon daemon requires no source build or binary override. -Next action: require fresh CI/review on the integrated candidate, refresh the +Fresh review at `665c155` reports three valid findings. Remote sandbox setup and +execution now share the existing actual-target OS/architecture resolver rather +than selecting the Mac controller's daemon. Thirteen new baseline failures +reproduce the issue; 124 target/setup/backend and retained-artifact/version +controls pass after the fix, with server typecheck. Header, digest, preinstalled +artifact, and remote build-metadata verification stay strict. Foreign artifacts +are read/hashed on the controller and executed through the remote launcher. +Local and non-Codex paths remain unchanged. Receipt: +`/private/tmp/codex-default-sandbox-target-fix-receipt.json`. + +Unchanged saved setup choices now bypass only new-selection resolution, retaining +later secret, company/environment, authentication, and runtime validation. The +actual disabled legacy baseline returned 422 before its probe. Treatment passes +175 existing controls plus a native-provider switch negative, with server +typecheck. New, explicit, draft, and changed-harness choices keep the disabled +adapter policy. Receipt: `/private/tmp/pr2-disabled-saved-codex-owned-receipt.json`. + +The actual served Codex guide reproduces stale Mac/ARM legacy text at `665c155`. +It now uses the shared GNU libc floor, lists qualified Linux/Mac architectures, +distinguishes legacy fields, and preserves existing-runner and failure semantics. +Adapter typecheck passes; the updated served guide needs final-preview proof. +These corrections update server setup/execution and guide source. The qualified +`a200495` package startup and historical task evidence retain their literal +component boundaries; no new managed live proof or paid turn is claimed. + +Next action: require fresh CI/review on the corrected integrated candidate, refresh the matched preview, and publish final conclusions in the canonical PR's evidence index. Human code-owner approval, managed staging onboarding, and actual post-merge Cloud composition remain outstanding. No merge or production diff --git a/packages/adapters/codex-local/src/index.ts b/packages/adapters/codex-local/src/index.ts index c9d54904d7..38640d46f4 100644 --- a/packages/adapters/codex-local/src/index.ts +++ b/packages/adapters/codex-local/src/index.ts @@ -1,6 +1,7 @@ import { PAPERCLIP_RUNNER_DEFAULT_MODELS, } from "@paperclipai/adapter-utils"; +import { PAPERCLIP_RUNNER_MINIMUM_GLIBC_VERSION } from "@paperclipai/shared"; export const type = "codex_local"; export const label = "Codex"; @@ -212,7 +213,9 @@ export const models = [ export const agentConfigurationDoc = `# codex_local agent configuration -New agents select this Codex harness with adapterType: codex_local. On qualified Linux x64 targets, automatic selection stores adapterType: paperclip_runner with provider: codex. macOS, Windows, and Linux ARM retain legacy defaults until their runner artifacts are qualified. Other harness defaults are unchanged. Set the request-level runner: legacy to use this adapter's execution and custom settings. Existing agents and reviewed pending hires retain their saved runner; runtime errors never silently switch runners. +New agents select the Codex harness with adapterType: codex_local and an optional request-level runner: auto, paperclip, or legacy. Automatic selection uses Paperclip Runner on qualified Linux x64/ARM64 targets with GNU libc ${PAPERCLIP_RUNNER_MINIMUM_GLIBC_VERSION} or newer and macOS arm64/x64 targets. It stores adapterType: paperclip_runner with provider: codex. Windows, 32-bit ARM, Alpine/musl, and older or unknown Linux libc retain automatic legacy selection. Qualification and artifact checks use the selected execution environment, including its architecture, rather than the controller's platform. Other harness defaults are unchanged. + +Set runner: legacy to retain this adapter's execution and custom settings. Explicit runner: paperclip requires a qualified target. Existing agents and reviewed pending hires retain their saved runner; authentication, model, dependency, and runtime errors never silently switch runners. The fields below describe legacy codex_local execution. Native creation translates supported settings and rejects unsupported settings with the explicit Legacy runner option. Adapter: codex_local diff --git a/server/src/__tests__/agent-adapter-validation-routes.test.ts b/server/src/__tests__/agent-adapter-validation-routes.test.ts index a834223c89..d48b1e8080 100644 --- a/server/src/__tests__/agent-adapter-validation-routes.test.ts +++ b/server/src/__tests__/agent-adapter-validation-routes.test.ts @@ -657,6 +657,78 @@ describe("agent routes adapter validation", () => { expect(res.status, JSON.stringify(res.body)).toBe(200); }); + it.each(["pass", "fail"] as const)("tests saved legacy Codex on a disabled adapter and retains its %s runtime verdict", async (status) => { + const adapterConfig = { model: "gpt-5.4", extraArgs: ["--legacy-setting"], env: { CODEX_HOME: "/saved-codex-home" } }; + mockAgentService.getById.mockResolvedValue({ ...(await mockAgentService.getById()), adapterConfig }); + mockAdapterPluginStore.getDisabledAdapterTypes.mockReturnValue(["codex_local"]); + const probe = vi.fn(async () => ({ adapterType: "codex_local", status, testedAt: new Date(0).toISOString(), + checks: status === "fail" ? [{ code: "adapter_auth_missing", level: "error" as const, message: "Sign in to the saved Codex account." }] : [] })); + const { registerServerAdapter } = await import("../adapters/index.js"); + registerServerAdapter({ ...externalAdapter, type: "codex_local", testEnvironment: probe }); + try { + const app = await createApp(); + const res = await requestApp(app, baseUrl => request(baseUrl) + .post("/api/companies/company-1/adapters/codex_local/test-environment") + .send({ agentId: "11111111-1111-4111-8111-111111111111", adapterConfig: { + ...adapterConfig, env: { CODEX_HOME: { type: "plain", value: "***REDACTED***" } }, + } })); + expect(res.status, JSON.stringify(res.body)).toBe(200); + expect(res.body).toMatchObject({ adapterType: "codex_local", status }); + expect(probe).toHaveBeenCalledOnce(); + expect(probe).toHaveBeenCalledWith(expect.objectContaining({ adapterType: "codex_local", config: adapterConfig })); + expect(mockSecretService.normalizeAdapterConfigForPersistence).toHaveBeenCalledWith("company-1", adapterConfig, expect.objectContaining({ adapterType: "codex_local" })); + expect(mockAgentService.create).not.toHaveBeenCalled(); + expect(mockAgentService.update).not.toHaveBeenCalled(); + } finally { await unregisterTestAdapter("codex_local"); } + }); + + it.each(["draft", "explicit legacy", "explicit auto", "explicit native", "changed harness"] as const)("rejects a disabled Codex setup selection for %s", async (mode) => { + mockAdapterPluginStore.getDisabledAdapterTypes.mockReturnValue(["codex_local"]); + if (mode === "changed harness") mockAgentService.getById.mockResolvedValue({ ...(await mockAgentService.getById()), adapterType: "external_test" }); + const probe = vi.fn(); + const { registerServerAdapter } = await import("../adapters/index.js"); + registerServerAdapter({ ...externalAdapter, type: "codex_local", testEnvironment: probe }); + try { + const runner = mode === "explicit legacy" ? "legacy" : mode === "explicit auto" ? "auto" : mode === "explicit native" ? "paperclip" : undefined; + const app = await createApp(); + const res = await requestApp(app, baseUrl => request(baseUrl) + .post("/api/companies/company-1/adapters/codex_local/test-environment") + .send({ adapterConfig: {}, ...(mode === "draft" ? {} : { agentId: "11111111-1111-4111-8111-111111111111" }), ...(runner ? { runner } : {}) })); + expect(res.status, JSON.stringify(res.body)).toBe(422); + expect(res.body.details).toMatchObject({ code: "agent_runner_unavailable" }); + expect(probe).not.toHaveBeenCalled(); + expect(mockSecretService.normalizeAdapterConfigForPersistence).not.toHaveBeenCalled(); + } finally { await unregisterTestAdapter("codex_local"); } + }); + + it("still rejects new Codex creation when its adapter is disabled", async () => { + mockAdapterPluginStore.getDisabledAdapterTypes.mockReturnValue(["codex_local"]); + const app = await createApp(); + const res = await requestApp(app, baseUrl => request(baseUrl) + .post("/api/companies/company-1/agents").send({ name: "New Codex", adapterType: "codex_local" })); + expect(res.status, JSON.stringify(res.body)).toBe(422); + expect(res.body.details).toMatchObject({ code: "agent_runner_unavailable" }); + expect(mockAgentService.create).not.toHaveBeenCalled(); + }); + + it("rechecks selection policy when a saved native adapter switches its underlying harness", async () => { + mockAgentService.getById.mockResolvedValue({ ...(await mockAgentService.getById()), adapterType: "paperclip_runner", adapterConfig: { provider: "codex" } }); + mockInstanceSettingsService.getExperimental.mockResolvedValue({ enableNativeRunner: false }); + const probe = vi.fn(); + const { registerServerAdapter } = await import("../adapters/index.js"); + registerServerAdapter({ ...externalAdapter, type: "paperclip_runner", testEnvironment: probe }); + try { + const app = await createApp(); + const res = await requestApp(app, baseUrl => request(baseUrl) + .post("/api/companies/company-1/adapters/paperclip_runner/test-environment") + .send({ agentId: "11111111-1111-4111-8111-111111111111", adapterConfig: { provider: "acpx", acpxAgent: "claude" } })); + expect(res.status, JSON.stringify(res.body)).toBe(422); + expect(res.body.details).toMatchObject({ code: "paperclip_runner_rollout_disabled" }); + expect(probe).not.toHaveBeenCalled(); + expect(mockSecretService.normalizeAdapterConfigForPersistence).not.toHaveBeenCalled(); + } finally { await unregisterTestAdapter("paperclip_runner"); } + }); + it("still creates an agent on an adapter that is registered and enabled", async () => { const { registerServerAdapter } = await import("../adapters/index.js"); registerServerAdapter(externalAdapter); diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index 4672e4b8ea..4d1b6ec2c3 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -3142,15 +3142,21 @@ export function agentRoutes( ? restoreRedactedAgentEnv(inputAdapterConfig, savedAgent.adapterConfig) : inputAdapterConfig; } - const selection = await resolveNewAgentRunnerForCompany(db, companyId, { - adapterType: type, adapterConfig: adapterConfigForTest, - runner: req.body.runner ?? (savedAgent && savedAgent.adapterType === type ? (type === "paperclip_runner" ? undefined : "legacy") : undefined), - defaultEnvironmentId: requestedEnvironmentId, - }); + const retainsSavedExecution = savedAgent && req.body.runner === undefined + && savedAgent.adapterType === type + && agentHarnessType(type, adapterConfigForTest) === agentHarnessType(savedAgent.adapterType, savedAgent.adapterConfig); + // Preserve saved execution; the adapter probe below still validates runtime readiness. + const selection = retainsSavedExecution + ? { adapterType: type, adapterConfig: adapterConfigForTest } + : await resolveNewAgentRunnerForCompany(db, companyId, { + adapterType: type, adapterConfig: adapterConfigForTest, + runner: req.body.runner, + defaultEnvironmentId: requestedEnvironmentId, + }); type = selection.adapterType; adapterConfigForTest = selection.adapterConfig; const adapter = requireServerAdapter(type); - if (!savedAgent || req.body.runner !== undefined || type !== savedAgent.adapterType) await assertSelectableAdapterType(type, adapterConfigForTest); + if (!retainsSavedExecution) await assertSelectableAdapterType(type, adapterConfigForTest); if (poolBinding?.mode === "router") { await validateManagedAgentBinding(req, companyId, savedAgentId ?? "", type, adapterConfigForTest, poolBinding, requestedEnvironmentId, false, !savedAgentId); res.json({ adapterType: type, status: "warn", testedAt: new Date().toISOString(), checks: [{ code: "ai_connection_pool_task_test_required", level: "warn", message: "Pool configuration is available. Run a task to verify the selected account and harness; this preview does not allocate a task or contact a provider." }] }); diff --git a/server/src/services/native-runtime/native-codex-runner.test.ts b/server/src/services/native-runtime/native-codex-runner.test.ts index c93d1b739e..2648d68b9e 100644 --- a/server/src/services/native-runtime/native-codex-runner.test.ts +++ b/server/src/services/native-runtime/native-codex-runner.test.ts @@ -12,7 +12,7 @@ import { resolveRemotePaperclipRunnerBinary, } from "./native-codex-runner.js"; -afterEach(() => vi.restoreAllMocks()); +afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); describe("installed runner selection", () => { it("uses the runner's platform-aware packaged selection", async () => { @@ -46,7 +46,7 @@ describe("installed runner selection", () => { }); }); -describe("selected SSH daemon target", () => { +describe("selected remote daemon target", () => { function header(target: string) { const bytes = Buffer.alloc(64); if (target.startsWith("darwin")) { @@ -72,6 +72,21 @@ describe("selected SSH daemon target", () => { expect(target.execute).toHaveBeenCalledExactlyOnceWith({ command: "sh", args: ["-c", "uname -s; uname -m"], cwd: "/selected-workspace", bypassSession: true, timeoutMs: 10_000 }); } finally { await rm(root, { recursive: true, force: true }); } }); + it.each(["arm64", "x64"].flatMap(controllerArch => ["arm64", "x64"].map(targetArch => ({ controllerArch, targetArch }))))( + "selects the actual Linux $targetArch sandbox bytes on a Mac $controllerArch controller", async ({ controllerArch, targetArch }) => { + const root = await mkdtemp(join(tmpdir(), "paperclip-sandbox-daemon-")); + const binary = join(root, "paperclip-runnerd"); + try { + vi.stubGlobal("process", { ...process, platform: "darwin", arch: controllerArch }); + await writeFile(binary, header(`linux-${targetArch}`)); + const select = vi.spyOn(runner, "defaultCapabilityRunnerdBinary").mockReturnValue(binary); + const target = remote(`Linux\n${targetArch === "arm64" ? "aarch64" : "x86_64"}\n`); + expect(await resolveRemotePaperclipRunnerBinary({ runner: target, remoteCwd: "/sandbox-workspace" })).toBe(binary); + expect(select).toHaveBeenCalledExactlyOnceWith("linux", targetArch); + expect(target.execute).toHaveBeenCalledExactlyOnceWith({ command: "sh", args: ["-c", "uname -s; uname -m"], cwd: "/sandbox-workspace", bypassSession: true, timeoutMs: 10_000 }); + } finally { await rm(root, { recursive: true, force: true }); } + }, + ); it.each(["matching", "wrong-architecture", "malformed", "missing"])("verifies an explicit controller-owned remote artifact (%s)", async scenario => { const root = await mkdtemp(join(tmpdir(), "paperclip-ssh-daemon-")); const binary = join(root, "configured-runnerd"); diff --git a/server/src/services/native-runtime/native-codex-runner.ts b/server/src/services/native-runtime/native-codex-runner.ts index 01edfbbe3c..962b7fb30e 100644 --- a/server/src/services/native-runtime/native-codex-runner.ts +++ b/server/src/services/native-runtime/native-codex-runner.ts @@ -208,7 +208,7 @@ export function resolvePaperclipRunnerBinary( ); } -/** Select controller-owned bytes for the selected SSH host, never its controller. */ +/** Select controller-owned bytes for the actual remote host, never its controller. */ export async function resolveRemotePaperclipRunnerBinary(input: { runner: CommandManagedRuntimeRunner; remoteCwd: string; @@ -220,7 +220,7 @@ export async function resolveRemotePaperclipRunnerBinary(input: { const platform = os === "Linux" ? "linux" : os === "Darwin" ? "darwin" : null; const architecture = arch === "x86_64" ? "x64" : arch === "aarch64" || arch === "arm64" ? "arm64" : null; if (probe.exitCode !== 0 || probe.timedOut || !platform || !architecture) { - throw new Error("runner_remote_artifact_platform_mismatch: could not identify a supported remote OS and architecture; check the SSH host and retry"); + throw new Error("runner_remote_artifact_platform_mismatch: could not identify a supported remote OS and architecture; check the selected execution environment and retry"); } const configured = input.configuredPath?.trim(); if (configured && !isAbsolute(configured)) throw new Error("PAPERCLIP_RUNNER_REMOTE_BINARY_PATH must be an absolute path"); diff --git a/server/src/services/native-runtime/native-session-executor.test.ts b/server/src/services/native-runtime/native-session-executor.test.ts index 824b7ba82b..d49205ae65 100644 --- a/server/src/services/native-runtime/native-session-executor.test.ts +++ b/server/src/services/native-runtime/native-session-executor.test.ts @@ -12218,10 +12218,8 @@ describe("runnerd provider runtime wiring", () => { // The mocked remote executes metadata probes; artifact staging only needs bytes. // Keep this regression independent of a locally compiled Rust runner binary. const controllerArtifact = join(isolatedStateDirectory, "paperclip-runnerd"); - if (needsReplacement || retained || image === "preinstalled-exact") { - await writeFile(controllerArtifact, "fixture runner artifact"); - state.resolveRunnerBinary.mockReturnValueOnce(controllerArtifact); - } + await writeFile(controllerArtifact, "fixture runner artifact"); + state.resolveRemoteRunnerBinary.mockResolvedValue(controllerArtifact); const onLog = vi.fn(async () => undefined); const syncIn = vi.fn(async () => undefined); const remoteExecute = vi.fn( @@ -12430,21 +12428,27 @@ describe("runnerd provider runtime wiring", () => { }); - it.each(["explicit", "linux-arm64", "linux-x64"])("binds an SSH launch to the selected controller-owned runner artifact (%s)", async mode => { + it.each(["ssh", "sandbox"].flatMap(transport => ["explicit", "linux-arm64", "linux-x64", "missing", "mismatch"].map(mode => ({ transport, mode })))) + ("binds a $transport launch to the selected controller-owned runner artifact ($mode)", async ({ transport, mode }) => { const remoteCwd = "/home/daytona/paperclip-workspace"; const controllerArtifact = `/controller/artifacts/${mode}/paperclip-runnerd`; - state.resolveRemoteRunnerBinary.mockResolvedValue(controllerArtifact); + const explicit = mode === "explicit" || mode === "missing" || mode === "mismatch"; + const error = mode === "missing" ? "runner_remote_artifact_unavailable" : mode === "mismatch" ? "runner_remote_artifact_platform_mismatch" : null; + if (error) state.resolveRemoteRunnerBinary.mockRejectedValue(new Error(error)); + else state.resolveRemoteRunnerBinary.mockResolvedValue(controllerArtifact); const remoteExecution = { ...execution, binding: { ...execution.binding, runId: "run-remote-runner-artifact" }, workspace: { ...execution.workspace, cwd: "/host/paperclip-workspace" }, } as NativeExecutionInputV1; - await createRunnerdBackend({ + const backend = createRunnerdBackend({ db: leaseDb(remoteExecution), execution: remoteExecution, runnerInstanceId: "runner", - runnerExecutionTarget: { + runnerExecutionTarget: transport === "sandbox" ? { + kind: "remote", transport: "sandbox", providerKey: "test", remoteCwd, runner: { execute: vi.fn() }, + } : { kind: "remote", transport: "ssh", remoteCwd, @@ -12459,15 +12463,25 @@ describe("runnerd provider runtime wiring", () => { strictHostKeyChecking: true, }, }, - ...(mode === "explicit" ? { runnerRemoteBinaryPath: controllerArtifact } : {}), + ...(explicit ? { runnerRemoteBinaryPath: controllerArtifact } : {}), }); + if (error) { + await expect(backend).rejects.toThrow(error); + expect(state.resolveRemoteRunnerBinary).toHaveBeenCalledWith(expect.objectContaining({ remoteCwd, configuredPath: controllerArtifact })); + expect(state.resolveRunnerBinary).not.toHaveBeenCalled(); + expect(state.createBackend).not.toHaveBeenCalled(); + expect(state.createTransport).not.toHaveBeenCalled(); + return; + } + await backend; + state.createTransport.mockClear(); state.createBackend.mock.calls.at(-1)![1].codexTransportFactory!(); expect(state.createTransport).toHaveBeenCalledWith( expect.objectContaining({ runnerBinary: controllerArtifact }), ); - expect(state.resolveRemoteRunnerBinary).toHaveBeenCalledWith(expect.objectContaining({ remoteCwd, configuredPath: mode === "explicit" ? controllerArtifact : undefined })); + expect(state.resolveRemoteRunnerBinary).toHaveBeenCalledWith(expect.objectContaining({ remoteCwd, configuredPath: explicit ? controllerArtifact : undefined })); expect(state.resolveRunnerBinary).not.toHaveBeenCalled(); }); diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index 0dedc9812d..b0fb8b56c9 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -11551,7 +11551,7 @@ async function createRunnerdBackendWithinSessionClaim( // filesystem; the remote launcher separately owns the sandbox command path. // When an explicit remote artifact is configured, prepareRemoteRunner stages // these exact bytes at remoteBinary before launch. - const targetMatchedCodexArtifact = remoteTarget?.transport === "ssh" && input.execution.provider.kind === "codex"; + const targetMatchedCodexArtifact = remoteTarget !== null && input.execution.provider.kind === "codex"; const controllerRunnerBinary = targetMatchedCodexArtifact ? await resolveRemotePaperclipRunnerBinary({ runner: remoteCommandRunner!, remoteCwd: remoteTarget.remoteCwd, configuredPath: input.runnerRemoteBinaryPath }) : remoteTarget diff --git a/server/src/services/native-runtime/setup-readiness.test.ts b/server/src/services/native-runtime/setup-readiness.test.ts index 9727571278..8a4d2dcf84 100644 --- a/server/src/services/native-runtime/setup-readiness.test.ts +++ b/server/src/services/native-runtime/setup-readiness.test.ts @@ -35,6 +35,7 @@ describe("Codex selected native account verification", () => { vi.stubEnv("PAPERCLIP_RUNNER_REMOTE_BINARY_PATH", ""); }); afterEach(async () => { + vi.unstubAllGlobals(); vi.unstubAllEnvs(); for (const [input] of nativeProbe.mock.calls) if (input.runtimeDirectory) await rm(input.runtimeDirectory, { recursive: true, force: true }); }); @@ -238,7 +239,12 @@ describe("Codex selected native account verification", () => { if (confirmed) await expect(stat(root)).rejects.toThrow(); else expect((await stat(root)).isDirectory()).toBe(true); }); - it("uses the same preparation and controller PRP for a sandbox, without a guest provider pack", async () => { + it.each(["arm64", "x64"].flatMap(controllerArch => ["arm64", "x64"].flatMap(targetArch => [false, true].map(explicit => ({ controllerArch, targetArch, explicit }))))) + ("uses target $targetArch artifacts and controller PRP from Mac $controllerArch for sandbox setup (override=$explicit)", async ({ controllerArch, targetArch, explicit }) => { + vi.stubGlobal("process", { ...process, platform: "darwin", arch: controllerArch }); + const selectedBinary = `/controller/artifacts/linux-${targetArch}/paperclip-runnerd`; + selectedRemoteRunner.mockResolvedValue(selectedBinary); + if (explicit) vi.stubEnv("PAPERCLIP_RUNNER_REMOTE_BINARY_PATH", selectedBinary); nativeProbe.mockImplementation(async input => { const registration = await input.transportOptions.controlPlaneRegistration({}); expect(registration.connection.mode).toBe("connect"); @@ -248,10 +254,23 @@ describe("Codex selected native account verification", () => { const target = { ...context.executionTarget, runner: { execute: sshExecute } }; expect((await testNativeRunnerAuthentication({ ...context, executionTarget: target }, "codex", "gpt-6.1-sol")).status).toBe("pass"); expect(sshRunner).not.toHaveBeenCalled(); - expect(nativeArtifacts).toHaveBeenCalledWith(expect.objectContaining({ target, runner: target.runner })); + expect(selectedRemoteRunner).toHaveBeenCalledExactlyOnceWith({ runner: target.runner, remoteCwd: target.remoteCwd, configuredPath: explicit ? selectedBinary : null }); + expect(selectedRunner).not.toHaveBeenCalled(); + expect(nativeArtifacts).toHaveBeenCalledWith(expect.objectContaining({ target, runner: target.runner, controllerRunnerBinary: selectedBinary, targetPlatformArtifactVerified: true })); + expect(nativeProbe.mock.calls[0][0].transportOptions.runnerBinary).toBe(selectedBinary); expect(nativeArtifacts.mock.results[0].value.prepare).toHaveBeenCalledWith("dial_wss"); expect(execute).not.toHaveBeenCalled(); }); + it.each(["runner_remote_artifact_unavailable", "runner_remote_artifact_platform_mismatch"])("fails sandbox %s before setup or a controller fallback", async code => { + selectedRemoteRunner.mockRejectedValue(new Error(code)); + const target = { ...context.executionTarget, runner: { execute: sshExecute } }; + const result = await testNativeRunnerAuthentication({ ...context, executionTarget: target }, "codex", "gpt-6.1-sol"); + expect(result.status).toBe("fail"); + expect(result.checks[0].message).toContain(code); + expect(selectedRunner).not.toHaveBeenCalled(); expect(nativeArtifacts).not.toHaveBeenCalled(); + expect(nativeProbe).not.toHaveBeenCalled(); expect(sshExecute).not.toHaveBeenCalled(); + expect(execute).not.toHaveBeenCalled(); + }); it("prepares listener artifacts before acquiring sandbox ingress and owns its release", async () => { const order: string[] = []; const close = vi.fn(); diff --git a/server/src/services/native-runtime/setup-readiness.ts b/server/src/services/native-runtime/setup-readiness.ts index c7afbc45ab..8cffbc9c13 100644 --- a/server/src/services/native-runtime/setup-readiness.ts +++ b/server/src/services/native-runtime/setup-readiness.ts @@ -63,11 +63,9 @@ async function probeRemoteNativeCodex(options: { const remoteCodexNpmSpec = process.env.PAPERCLIP_RUNNER_REMOTE_CODEX_NPM_SPEC?.trim() || null; const remoteCodexBinary = remoteCodexNpmSpec ? posix.join(remoteRoot, "harnesses", "codex", "node_modules", ".bin", "codex") : posix.join(remoteRoot, "bin", "codex"); const explicitRunner = process.env.PAPERCLIP_RUNNER_REMOTE_BINARY_PATH?.trim() || null; - const controllerRunnerBinary = target.transport === "ssh" - ? await resolveRemotePaperclipRunnerBinary({ runner, remoteCwd: target.remoteCwd, configuredPath: explicitRunner }) - : explicitRunner || resolvePaperclipRunnerBinary(); + const controllerRunnerBinary = await resolveRemotePaperclipRunnerBinary({ runner, remoteCwd: target.remoteCwd, configuredPath: explicitRunner }); const artifacts = createRemoteNativeArtifactPreparation({ target, runner, remoteBinary, controllerRunnerBinary, remoteRuntimeRoot: remoteRoot, remoteCodexBinary, - targetPlatformArtifactVerified: target.transport === "ssh", + targetPlatformArtifactVerified: true, runnerRemoteBinaryPath: explicitRunner, runnerRemoteCodexPath: process.env.PAPERCLIP_RUNNER_REMOTE_CODEX_PATH?.trim(), runnerRemoteCodexNpmSpec: remoteCodexNpmSpec, model: options.model }); const requireSuccess = (result: { timedOut: boolean; exitCode: number | null }, message: string) => { if (result.timedOut || result.exitCode !== 0) throw new Error(message);