diff --git a/doc/sandbox-work-folders.md b/doc/sandbox-work-folders.md index da0a0e6742..4c2086ce4c 100644 --- a/doc/sandbox-work-folders.md +++ b/doc/sandbox-work-folders.md @@ -84,6 +84,12 @@ overlay. This preserves its index and repository-local state. The legacy outbound merge still saves working files to the host. A missing native sync stamp on a pre-change lease does not make the host copy authoritative. +When an older sandbox image lacks a required runner capability, startup stages +the server-resolved runner artifact, including the vendored binary in packaged +server builds. The uploaded artifact and the controller's runner identity use +the same file. Replacement is atomic and preserves the previous launcher if +staging fails; it does not reset the task's working files or provider session. + Acceptance must resume representative pre-upgrade legacy and native tasks with committed, staged, unstaged, and untracked work, verify their original paths and usable continuation, and exercise their existing restore mechanism after a 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 28f387acd8..98aa7c1a5d 100644 --- a/server/src/services/native-runtime/native-session-executor.test.ts +++ b/server/src/services/native-runtime/native-session-executor.test.ts @@ -6454,6 +6454,50 @@ describe("runnerd provider runtime wiring", () => { ).toBe(false); }); + it.each(["missing", "incompatible"])( + "stages the server-resolved artifact when the image runner is %s", + async (imageRunner) => { + const artifact = join(isolatedStateDirectory, "vendored-runnerd"); + await writeFile(artifact, "server-owned runner bytes", { mode: 0o700 }); + state.resolveRunnerBinary.mockReturnValueOnce(artifact); + const syncIn = vi.fn(async () => { throw new Error("observed-runner-upload"); }); + const remoteExecute = vi.fn(async (command: { command: string; args?: string[] }) => { + const script = command.args?.[1] ?? ""; + let stdout = ""; + if (script.includes("command -v paperclip-runnerd")) { + stdout = imageRunner === "missing" ? "" : "/usr/local/bin/paperclip-runnerd\n"; + } else if (command.args?.[0] === "--build-metadata") { + stdout = "{}"; // An incompatible image must fall back to the app artifact. + } else if (script === "uname -s; uname -m") { + const os = process.platform === "darwin" ? "Darwin" : "Linux"; + const arch = process.arch === "x64" ? "x86_64" : "aarch64"; + stdout = `${os}\n${arch}\n`; + } + return { exitCode: 0, signal: null, timedOut: false, stderr: "", stdout }; + }); + await createRunnerdBackend({ + db: leaseDb(execution), execution, runnerInstanceId: "runner-vendored-artifact", + runnerIngressAuthorized: true, + runnerExecutionTarget: { + kind: "remote", transport: "sandbox", remoteCwd: "/workspace", + environmentId: "environment", leaseId: "lease", providerKey: "daytona", + effectiveCapabilities: { runnerWebSocketIngress: true }, + runner: { execute: remoteExecute, syncIn }, + } as never, + }); + state.createTransport.mockClear(); + state.createBackend.mock.calls.at(-1)![1].codexTransportFactory!(); + const transport = state.createTransport.mock.calls[0]![0] as RunnerTransportOptions & { + controlPlaneRegistration: (authority: unknown) => Promise; + }; + expect(transport.runnerBinary).toBe(artifact); + await expect(transport.controlPlaneRegistration({})).rejects.toThrow("observed-runner-upload"); + expect(syncIn).toHaveBeenCalledWith([ + expect.objectContaining({ files: [expect.objectContaining({ sourcePath: artifact, kind: "file", mode: 0o700 })] }), + ]); + }, + ); + it("binds a remote launch to the configured controller-owned runner artifact", async () => { const remoteCwd = "/home/daytona/paperclip-workspace"; const controllerArtifact = "/controller/artifacts/paperclip-runnerd"; diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index d47352264b..75c7c67977 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -40,7 +40,6 @@ import { acpxRuntimeSessionDirectoryName, createNativeSessionBackend, createRunnerdCodexTransport, - defaultCapabilityRunnerdBinary, executeNativeSession, parseNativeExecutionInput, parsePaperclipQuestionSet, @@ -6708,8 +6707,10 @@ async function createRunnerdBackendWithinSessionClaim( } } if (!usedPreinstalledRunner) { - const sourceBinary = - explicitRemoteBinary ?? defaultCapabilityRunnerdBinary(); + // Use the same server-resolved artifact the transport hashes. The + // package's development fallback does not resolve the vendored layout + // in a built server, even though its bin/paperclip-runnerd is present. + const sourceBinary = controllerRunnerBinary; if (!existsSync(sourceBinary)) { throw new Error("runner_remote_artifact_unavailable"); }