From 73a146e0764503c7ba74d9c44ab22e2b994fbf2a Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 3 Sep 2026 17:40:21 -0500 Subject: [PATCH] fix(runner): separate remote artifact identity from launch path --- .github/workflows/runner-full-stack-e2e.yml | 3 ++ .../native-session-executor.test.ts | 42 +++++++++++++++++++ .../native-runtime/native-session-executor.ts | 10 ++++- tests/runner-e2e/workflow-security.test.ts | 3 ++ 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/.github/workflows/runner-full-stack-e2e.yml b/.github/workflows/runner-full-stack-e2e.yml index 577928ae7d..8ea7b4a56c 100644 --- a/.github/workflows/runner-full-stack-e2e.yml +++ b/.github/workflows/runner-full-stack-e2e.yml @@ -890,6 +890,9 @@ jobs: OPENROUTER_API_KEY: ${{ matrix.credentialName == 'OPENROUTER_API_KEY' && secrets.OPENROUTER_API_KEY || '' }} DAYTONA_API_KEY: ${{ matrix.environmentId == 'daytona' && secrets.DAYTONA_API_KEY || '' }} PAPERCLIP_E2E_DAYTONA_IMAGE: ${{ needs.daytona_image.outputs.image }} + # Bind PRP identity to the exact build-once bytes that the server + # stages into Daytona instead of a separately built image binary. + PAPERCLIP_RUNNER_REMOTE_BINARY_PATH: ${{ github.workspace }}/packages/paperclip-runner/runner/target/debug/paperclip-runnerd PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH: ${{ github.workspace }}/packages/paperclip-runner/provider-pack PAPERCLIP_E2E_CAMPAIGN_ID: gha-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.executionId }} run: pnpm test:e2e:runner -- --id "${{ matrix.executionId }}" 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 f891c06473..f625ca6ae5 100644 --- a/server/src/services/native-runtime/native-session-executor.test.ts +++ b/server/src/services/native-runtime/native-session-executor.test.ts @@ -4618,11 +4618,53 @@ describe("runnerd provider runtime wiring", () => { backendOptions.codexTransportFactory!(); expect(state.createTransport).toHaveBeenCalledWith( expect.objectContaining({ + runnerBinary: "/tmp/paperclip-runnerd", environment: expect.objectContaining({ PAPERCLIP_WORKSPACE_CWD: remoteCwd, }), }), ); + expect(state.createTransport.mock.calls[0]![0].runnerBinary).not.toBe( + `${remoteCwd}/.paperclip-runtime/paperclip-runner/bin/paperclip-runnerd`, + ); + }); + + 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"; + const remoteExecution = { + ...execution, + binding: { ...execution.binding, runId: "run-remote-runner-artifact" }, + workspace: { ...execution.workspace, cwd: "/host/paperclip-workspace" }, + } as NativeExecutionInputV1; + + await createRunnerdBackend({ + db: leaseDb(remoteExecution), + execution: remoteExecution, + runnerInstanceId: "runner", + runnerExecutionTarget: { + kind: "remote", + transport: "ssh", + remoteCwd, + spec: { + host: "runner.internal", + port: 22, + username: "runner", + remoteWorkspacePath: remoteCwd, + remoteCwd, + privateKey: null, + knownHosts: null, + strictHostKeyChecking: true, + }, + }, + runnerRemoteBinaryPath: controllerArtifact, + }); + + state.createTransport.mockClear(); + state.createBackend.mock.calls.at(-1)![1].codexTransportFactory!(); + expect(state.createTransport).toHaveBeenCalledWith( + expect.objectContaining({ runnerBinary: controllerArtifact }), + ); }); it.each([ diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index 0cfebf2404..a6bd4b00cf 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -5320,6 +5320,14 @@ async function createRunnerdBackendWithinSessionClaim( const remoteBinary = remoteRuntimeRoot ? posix.join(remoteRuntimeRoot, "bin", "paperclip-runnerd") : null; + // The transport hashes runnerBinary on the controller before an external + // launcher starts runnerd. Keep that artifact identity in the controller's + // 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 controllerRunnerBinary = remoteTarget + ? input.runnerRemoteBinaryPath?.trim() || resolvePaperclipRunnerBinary() + : resolvePaperclipRunnerBinary(); const explicitRemoteCodex = input.runnerRemoteCodexPath?.trim() || null; const remoteCodexNpmSpec = input.runnerRemoteCodexNpmSpec?.trim() || null; if (explicitRemoteCodex && remoteCodexNpmSpec) { @@ -6675,7 +6683,7 @@ async function createRunnerdBackendWithinSessionClaim( stateDirectory: remoteStateDirectory, }) : undefined, - runnerBinary: remoteBinary ?? resolvePaperclipRunnerBinary(), + runnerBinary: controllerRunnerBinary, codexCommand: remoteCodexBinary ?? undefined, sourceCodexHome: remoteTarget ? resolveSourceCodexHome(input.runnerEnvironment ?? process.env) diff --git a/tests/runner-e2e/workflow-security.test.ts b/tests/runner-e2e/workflow-security.test.ts index cf554d4c34..3ac05e0093 100644 --- a/tests/runner-e2e/workflow-security.test.ts +++ b/tests/runner-e2e/workflow-security.test.ts @@ -376,6 +376,9 @@ describe("public repository paid workflow security", () => { expect(testJob).toContain( "test -x packages/paperclip-runner/runner/target/debug/paperclip-runnerd", ); + expect(testJob).toContain( + "PAPERCLIP_RUNNER_REMOTE_BINARY_PATH: ${{ github.workspace }}/packages/paperclip-runner/runner/target/debug/paperclip-runnerd", + ); expect(testJob).toContain(".payload.runnerSourceRevision == $revision"); expect(workflow).toContain("Qualify local provider Node interpreter"); expect(testJob).toContain(