diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index e9d2c94b9fa..8eb659935dc 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -557,6 +557,18 @@ or dependency postinstall script is required. This also works when installation scripts are disabled. The published server declares the same pinned dependency as the source Runner package. +For native OpenCode and Cursor on Daytona, a complete installed Paperclip release +also selects its packaged Linux daemon and provider-pack identity automatically. +Use the matching Paperclip Daytona image: the controller verifies the image's +pack manifest, executable hashes, bridge tree, and pinned versions before launch. +The controller's operating system does not determine the remote daemon target. + +A plain Daytona sandbox image does not contain this provider pack. Custom images +and source-development launchers must supply a verified Linux provider pack and +daemon through the existing remote artifact overrides; see +[the remote E2E setup](../tests/runner-e2e/README.md#match-the-local-controller-package-to-the-daytona-image). +An explicit override remains authoritative and fails closed if invalid. + ## One-Command Local Run For a first-time local install, you can bootstrap and run in one command: 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 4280e1566c2..6ae27897a98 100644 --- a/server/src/services/native-runtime/native-session-executor.test.ts +++ b/server/src/services/native-runtime/native-session-executor.test.ts @@ -47,7 +47,7 @@ import { type NativeExecutionInput, type PrpEvent, } from "@paperclipai/paperclip-runner"; -import { createHash, generateKeyPairSync } from "node:crypto"; +import { createHash, generateKeyPairSync, randomUUID } from "node:crypto"; import { DatabaseSync } from "node:sqlite"; import { nativeSha256 } from "./canonical.js"; import { agentDirectoryWorkingCopyService } from "../agent-directory-working-copies.js"; @@ -203,6 +203,8 @@ const state = vi.hoisted(() => ({ async (): Promise | null> => null, ), assertCurrentWakeCommentsRead: vi.fn(async () => undefined), + bundledPackManifestPath: null as string | null, + bundledRunnerBinary: null as string | null, resolveRunnerBinary: vi.fn(() => "/tmp/paperclip-runnerd"), release: null as null | (() => void), })); @@ -231,6 +233,8 @@ vi.mock("../../vendor/paperclip-runner/index.js", async (importOriginal) => { >(); return { ...original, + bundledRemoteProviderPackManifestPath: () => state.bundledPackManifestPath ?? original.bundledRemoteProviderPackManifestPath(), + bundledRemoteRunnerBinary: () => state.bundledRunnerBinary ?? original.bundledRemoteRunnerBinary(), createNativeSessionBackend: state.createBackend, createRunnerdCodexTransport: state.createTransport, executeNativeSession: state.execute, @@ -365,6 +369,8 @@ import { } from "./native-session-executor.js"; beforeEach(() => { + state.bundledPackManifestPath = null; + state.bundledRunnerBinary = null; state.createAssignedMcpTools.mockReset(); state.resolveRunnerBinary.mockReset().mockReturnValue("/tmp/paperclip-runnerd"); state.resolveCurrentWakeCommentsBinding.mockReset().mockResolvedValue(null); @@ -1121,6 +1127,63 @@ describe("remote provider pack manifest", () => { return JSON.stringify(value); }; + it.each(["opencode", "cursor"] as const)("uses the bundled Linux image authority for remote %s without controller overrides", async provider => { + const root = await mkdtemp(join(tmpdir(), "paperclip-bundled-image-")); + const digest = "sha256:" + "a".repeat(64); + const artifacts = Object.fromEntries(Object.entries({ + grokLauncher: "dist/providers/grok/launcher.cjs", nodeCommand: "node_modules/node/bin/node", + productionLock: "pnpm-lock.yaml", opencodeCommand: "node_modules/.bin/opencode", + opencodeExecutable: "node_modules/opencode-ai/bin/opencode.exe", + opencodeProxy: "dist/cli/opencode-app-server-proxy.cjs", acpxSidecar: "dist/cli/acpx-runtime-sidecar.cjs", + }).map(([key, path]) => [key, { path, sha256: digest }])); + const payload = { + target: { platform: "linux", architecture: "x64" }, runnerSourceRevision: "a".repeat(40), + pins: { nodeMinimum: "24.11.0", codex: QUALIFIED_ACPX_PROFILES.codex.agentRuntimeVersion, + opencode: "1.18.34", acpx: "0.13.1", grok: QUALIFIED_ACPX_PROFILES.grok.agentRuntimeVersion, + claudeAcp: QUALIFIED_ACPX_PROFILES.claude.agentServerVersion, codexAcp: QUALIFIED_ACPX_PROFILES.codex.agentServerVersion }, + acpxProfileDigests: Object.fromEntries(["grok", "claude", "codex"].map(agent => + [agent, QUALIFIED_ACPX_PROFILES[agent as "grok" | "claude" | "codex"].commandDigest])), + distDigest: digest, artifacts, + bridgeDigest: "sha256:" + createHash("sha256").update(digest + "\n" + digest + "\n" + digest).digest("hex"), + }; + const manifest = { schema: "paperclip-runner/remote-provider-pack/v1", payload, + digest: "sha256:" + createHash("sha256").update(canonical(payload)).digest("hex") }; + state.bundledPackManifestPath = join(root, "provider-pack.json"); + state.bundledRunnerBinary = join(root, "linux-paperclip-runnerd"); + await writeFile(state.bundledPackManifestPath, JSON.stringify(manifest)); + await writeFile(state.bundledRunnerBinary, "verified Linux daemon fixture"); + const remoteExecution = { ...execution, provider: provider === "opencode" + ? { kind: "opencode", model: null } : { kind: "acpx", agent: "cursor", model: null }, + session: { ...execution.session, driverKind: provider === "opencode" ? "opencode_server" : "acpx_runtime" }, + } as unknown as NativeExecutionInputV1; + const syncIn = vi.fn(async () => undefined); + const execute = vi.fn(async (command: { command: string; args?: string[] }) => { + if (command.args?.[0] === "--build-metadata") throw new Error("reached-staged-Linux-daemon-verification"); + return { exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "" }; + }); + try { + await createRunnerdBackend({ db: leaseDb(remoteExecution), execution: remoteExecution, + runnerInstanceId: "bundled-image-runner", runnerIngressAuthorized: true, + runnerExecutionTarget: { kind: "remote", transport: "sandbox", remoteCwd: "/workspace", + environmentId: "environment", leaseId: "lease", providerKey: "daytona", + effectiveCapabilities: { runnerWebSocketIngress: true }, runner: { execute, syncIn } } as never }); + state.createTransport.mockClear(); + state.createBackend.mock.calls.at(-1)![1].codexTransportFactory!(); + const transport = state.createTransport.mock.calls.at(-1)![0] as RunnerTransportOptions & { + controlPlaneRegistration: (authority: unknown) => Promise; + }; + expect(transport.runnerBinary).toBe(state.bundledRunnerBinary); + await expect(transport.controlPlaneRegistration({})).rejects.toThrow("reached-staged-Linux-daemon-verification"); + expect(syncIn).toHaveBeenCalledWith([expect.objectContaining({ files: [expect.objectContaining({ + sourcePath: state.bundledRunnerBinary, targetPath: "/workspace/.paperclip-runtime/paperclip-runner/bin/paperclip-runnerd", + })] })]); + expect(execute.mock.calls.some(([call]) => call.args?.[1] === "uname -s; uname -m")).toBe(false); + } finally { + state.bundledPackManifestPath = null; state.bundledRunnerBinary = null; + await rm(root, { recursive: true, force: true }); + } + }); + it("accepts a fully digested pack and rejects artifact tampering", async () => { const root = await mkdtemp(join(tmpdir(), "paperclip-provider-pack-")); await mkdir(join(root, "dist", "cli"), { recursive: true }); @@ -12914,8 +12977,9 @@ describe("runnerd provider runtime wiring", () => { ["opencode", { kind: "opencode", model: null }, "opencode_server"], ["acpx", { kind: "acpx", agent: "codex", model: null }, "acpx_runtime"], ])( - "requires the build-owned provider pack before launching remote %s", + "requires a build-owned provider pack identity before launching remote %s", async (providerKind, provider, driverKind) => { + state.bundledPackManifestPath = join(tmpdir(), randomUUID(), "provider-pack.json"); const remoteCwd = "/home/daytona/paperclip-workspace"; const remoteProviderExecution = { ...execution, @@ -12953,8 +13017,9 @@ describe("runnerd provider runtime wiring", () => { }, }, }), - ).rejects.toThrow( - "runner_remote_provider_artifact_incompatible: configure PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH", + ).rejects.toThrow(providerKind === "opencode" + ? "runner_remote_provider_artifact_incompatible: provider-pack.json is unreadable" + : "runner_remote_provider_artifact_incompatible: configure PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH", ); expect(state.createBackend).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 b5e5f298359..0ce2526c65b 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -11520,9 +11520,10 @@ async function createRunnerdBackendWithinSessionClaim( const configuredProviderPackRoot = input.runnerRemoteProviderPackPath?.trim() || remotePiCompanion?.providerPack || null; let expectedProviderPackManifest: RemoteProviderPackManifest | null = null; - const useBundledCursorImageAssets = requiresRemoteProviderPack && !configuredProviderPackRoot && - input.execution.provider.kind === "acpx" && input.execution.provider.agent === "cursor"; - if (useBundledCursorImageAssets) { + const useBundledRemoteImageAssets = requiresRemoteProviderPack && !configuredProviderPackRoot && + (input.execution.provider.kind === "opencode" || + (input.execution.provider.kind === "acpx" && input.execution.provider.agent === "cursor")); + if (useBundledRemoteImageAssets) { expectedProviderPackManifest = readBundledRemoteProviderPackManifest(); } else if (requiresRemoteProviderPack) { if ( @@ -11551,7 +11552,7 @@ async function createRunnerdBackendWithinSessionClaim( // When an explicit remote artifact is configured, prepareRemoteRunner stages // these exact bytes at remoteBinary before launch. const controllerRunnerBinary = remoteTarget - ? input.runnerRemoteBinaryPath?.trim() || remotePiCompanion?.runnerBinary || (useBundledCursorImageAssets ? bundledRemoteRunnerBinary() : resolvePaperclipRunnerBinary()) + ? input.runnerRemoteBinaryPath?.trim() || remotePiCompanion?.runnerBinary || (useBundledRemoteImageAssets ? bundledRemoteRunnerBinary() : resolvePaperclipRunnerBinary()) : resolvePaperclipRunnerBinary(); const explicitRemoteCodex = input.runnerRemoteCodexPath?.trim() || null; const remoteCodexNpmSpec = input.runnerRemoteCodexNpmSpec?.trim() || null; @@ -11930,7 +11931,7 @@ async function createRunnerdBackendWithinSessionClaim( if (!existsSync(sourceBinary)) { throw new Error("runner_remote_artifact_unavailable"); } - if (!explicitRemoteBinary && !remotePiCompanion) { + if (!explicitRemoteBinary && !remotePiCompanion && !useBundledRemoteImageAssets) { const platform = await remoteCommandRunner.execute({ command: "sh", args: ["-c", "uname -s; uname -m"],