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 8d85ab20b7..350cbbb3b1 100644 --- a/server/src/services/native-runtime/native-session-executor.test.ts +++ b/server/src/services/native-runtime/native-session-executor.test.ts @@ -373,6 +373,7 @@ import { syncRemoteRunnerDirectoryOut, verifyNativeHarnessBackup, shouldRestoreNativeHarnessBackupIntoSandbox, + remoteRunnerStorageRoots, } from "./native-session-executor.js"; beforeEach(() => { @@ -710,6 +711,28 @@ describe("remote runner launch fingerprint compatibility", () => { }); }); +describe("persistent computer runtime storage", () => { + it("isolates agents and sessions sharing one project while keeping warm roots stable", () => { + const target = (agent: string) => ({ kind: "remote", transport: "computer", remoteCwd: "/shared/project", + fileAuthority: { kind: "remote-persistent", root: "/shared/project", placementId: "shared", agentHome: `/home/user/paperclip/company/agents/${agent}` }, + } as never); + const a = remoteRunnerStorageRoots(target("a"), "session-1"); + const b = remoteRunnerStorageRoots(target("b"), "session-1"); + expect(a.runtimeRoot).not.toBe(b.runtimeRoot); + expect(a.runtimeRoot).toContain("/agents/a/.paperclip-runtime/"); + expect(a.sessionRoot).toBe(a.runtimeRoot); + expect(remoteRunnerStorageRoots(target("a"), "session-1")).toEqual(a); + expect(remoteRunnerStorageRoots(target("a"), "session-2").runtimeRoot).not.toBe(a.runtimeRoot); + expect((target("a") as { remoteCwd: string }).remoteCwd).toBe("/shared/project"); + expect(() => remoteRunnerStorageRoots({ kind: "remote", transport: "computer", remoteCwd: "/shared/project" } as never, "session")).toThrow("runner_remote_storage_home_missing"); + }); + it("preserves existing sandbox runtime and session paths", () => { + const roots = remoteRunnerStorageRoots({ kind: "remote", transport: "sandbox", remoteCwd: "/workspace" } as never, "session-1"); + expect(roots.runtimeRoot).toBe("/workspace/.paperclip-runtime/paperclip-runner"); + expect(roots.sessionRoot).toBe(`${roots.runtimeRoot}/sessions/${createHash("sha256").update("session-1").digest("hex")}`); + }); +}); + describe("remote runner process supervision", () => { it("admits a valid computer identity when the supervised RPC takes longer than two seconds", async () => { vi.useFakeTimers(); @@ -13439,6 +13462,7 @@ describe("runnerd provider runtime wiring", () => { runnerIngressAuthorized: true, runnerRemoteCodexNpmSpec: override, runnerExecutionTarget: { kind: "remote", transport: "computer", remoteCwd: "/workspace", + fileAuthority: { kind: "remote-persistent", root: "/workspace", placementId: "workspace", agentHome: "/home/user/agent" }, environmentId: "environment", leaseId: "lease", providerKey: "boat", effectiveCapabilities: { runnerWebSocketIngress: true }, runner: { execute: remoteExecute }, processRunner: { execute: remoteExecute }, @@ -13452,7 +13476,7 @@ describe("runnerd provider runtime wiring", () => { await expect(transport.controlPlaneRegistration({})).rejects.toThrow("reached-private-codex-verification"); expect(remoteExecute).toHaveBeenCalledWith(expect.objectContaining({ command: "npm", - args: ["install", "--prefix", "/workspace/.paperclip-runtime/paperclip-runner/harnesses/codex", "--no-audit", "--no-fund", expected], + args: ["install", "--prefix", `/home/user/agent/.paperclip-runtime/paperclip-runner/sessions/${createHash("sha256").update(execution.session.normalizedSessionId ?? `session-${execution.binding.runId}`).digest("hex")}/harnesses/codex`, "--no-audit", "--no-fund", expected], })); expect(remoteExecute.mock.calls.filter(([command]) => command.command === "npm")).toHaveLength(1); expect(remoteExecute.mock.calls.some(([command]) => command.args?.includes("--global"))).toBe(false); diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index 7bfe19a9ad..ecd9244fe8 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -1697,6 +1697,18 @@ function nativeSessionKey(execution: NativeExecutionInput): string { ); } +/** Persistent computer artifacts belong to an agent session, independent of a shared repo cwd. */ +export function remoteRunnerStorageRoots( + target: Extract, + sessionKey: string, +) { + const home = target.transport === "computer" ? target.fileAuthority?.agentHome : target.remoteCwd; + if (!home || !posix.isAbsolute(home)) throw new Error("runner_remote_storage_home_missing"); + const base = posix.join(home, ".paperclip-runtime", "paperclip-runner"); + const sessionRoot = posix.join(base, "sessions", createHash("sha256").update(sessionKey).digest("hex")); + return { runtimeRoot: target.transport === "computer" ? sessionRoot : base, sessionRoot }; +} + export function nativeSessionWorkspaceScope(execution: { binding: Pick; workspace: NativeExecutionInput["workspace"] }) { if ("access" in execution.workspace) return { kind: "none" as const }; // Projectless local runs use the heartbeat run id as a durable placeholder @@ -11417,10 +11429,10 @@ async function prepareRemoteDotRunner({ input, target, runner, root, identity }: identity: NonNullable[1]>["dotRunnerOptions"]>["identity"]; }) { const runnerBinary = input.runnerRemoteBinaryPath?.trim() || resolvePaperclipRunnerBinary(); - const remoteRoot = posix.join(target.remoteCwd, ".paperclip-runtime", "paperclip-runner"); + const storage = remoteRunnerStorageRoots(target, identity.normalizedSessionId); + const remoteRoot = storage.runtimeRoot; const remoteBinary = posix.join(remoteRoot, "bin", "paperclip-runnerd"); - const runnerStateDirectory = posix.join(remoteRoot, "sessions", - createHash("sha256").update(identity.normalizedSessionId).digest("hex"), "runner"); + const runnerStateDirectory = posix.join(storage.sessionRoot, "runner"); const requiredMode = resolveRemoteRunnerTransportMode({ target, runnerIngressAuthorized: input.runnerIngressAuthorized === true }); const artifact = readRunnerdArtifactBinding(runnerBinary); const prepare = async () => { @@ -11687,13 +11699,10 @@ async function createRunnerdBackendWithinSessionClaim( return { ...backend, descriptor: () => backend.descriptor(), openSession: input => backend.openSession(input), recoverSession: (snapshot, options) => backend.recoverSession!(snapshot, options), bindManagedSession: session => session }; } - const remoteRuntimeRoot = remoteTarget - ? posix.join( - remoteTarget.remoteCwd, - ".paperclip-runtime", - "paperclip-runner", - ) + const remoteStorage = remoteTarget + ? remoteRunnerStorageRoots(remoteTarget, nativeSessionKey(input.execution)) : null; + const remoteRuntimeRoot = remoteStorage?.runtimeRoot ?? null; const requiresRemoteProviderPack = remoteTarget !== null && (input.execution.provider.kind === "opencode" || @@ -11764,12 +11773,7 @@ async function createRunnerdBackendWithinSessionClaim( ) : posix.join(remoteRuntimeRoot, "bin", "codex") : null; - const remoteSessionDigest = createHash("sha256") - .update(nativeSessionKey(input.execution)) - .digest("hex"); - const remoteSessionRoot = remoteRuntimeRoot - ? posix.join(remoteRuntimeRoot, "sessions", remoteSessionDigest) - : null; + const remoteSessionRoot = remoteStorage?.sessionRoot ?? null; const remoteStateDirectory = remoteSessionRoot ? posix.join(remoteSessionRoot, "runner") : undefined;