Integrate scoped legacy runtime files and retirement recovery

Keep the adapter ownership acceptance tests in the stacked PR.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-10 10:56:32 -05:00
commit 18a1e89a08
2 files changed
+56 -4

No files matched your search

@@ -5,6 +5,9 @@ import * as ssh from "./ssh.js";
import * as serverUtils from "./server-utils.js";
import {
cleanupGitHubOperationLaunchers,
adapterExecutionTargetRuntimeRoot,
prepareAdapterExecutionTargetRuntime,
type AdapterComputerExecutionTarget,
prepareGitHubOperationLaunchers,
adapterExecutionTargetUsesManagedHome,
ensureAdapterExecutionTargetRuntimeCommandInstalled,
@@ -437,3 +440,36 @@ describe("GitHub launcher lifecycle", () => {
expect(runner.execute).toHaveBeenCalledTimes(1);
});
});
describe("computer legacy runtime ownership", () => {
it.each(["claude", "codex"])("isolates %s assets and callback roots for owners sharing a checkout", async adapterKey => {
const execute = vi.fn(async () => ({ exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "", pid: 123, startedAt: "2026-10-10T00:00:00Z" }));
const target = (ownerId: string, generation = 1) => ({
kind: "remote", transport: "computer", providerKey: "boat", environmentId: "environment",
leaseId: ownerId, remoteCwd: "/shared/project", listenerPort: 43127,
runner: { execute }, processRunner: { execute },
resourceAuthority: { kind: "computer-owner", computerId: "computer", ownerId, generation },
fileAuthority: { kind: "remote-persistent", placementId: "shared", root: "/shared/project", agentHome: `/homes/${ownerId}` },
launch: vi.fn(), inspectProcess: vi.fn(), retainWarm: vi.fn(), retire: vi.fn(),
computerTool: { command: "cua-driver", args: ["mcp"] },
} satisfies AdapterComputerExecutionTarget);
const prepare = (owner: AdapterComputerExecutionTarget) => prepareAdapterExecutionTargetRuntime({
target: owner, runId: owner.leaseId!, adapterKey, workspaceLocalDir: "/unused-local-workspace", syncWorkspace: false,
});
const first = await prepare(target("agent-one"));
const second = await prepare(target("agent-two"));
expect(first.workspaceRemoteDir).toBe("/shared/project");
expect(second.workspaceRemoteDir).toBe(first.workspaceRemoteDir);
expect(first.runtimeRootDir).toBe(adapterExecutionTargetRuntimeRoot(target("agent-one"), adapterKey));
expect(second.runtimeRootDir).not.toBe(first.runtimeRootDir);
expect(adapterExecutionTargetRuntimeRoot(target("agent-one", 2), adapterKey)).not.toBe(first.runtimeRootDir);
expect((await prepare(target("agent-one"))).runtimeRootDir).toBe(first.runtimeRootDir);
expect(first.runtimeRootDir).toMatch(new RegExp(`^/shared/project/\\.paperclip-runtime/${adapterKey}/owners/[a-f0-9]{64}$`));
});
it("preserves the existing sandbox runtime root", () => {
const target = { kind: "remote", transport: "sandbox", remoteCwd: "/workspace" } as const;
expect(adapterExecutionTargetRuntimeRoot(target, "claude")).toBe("/workspace/.paperclip-runtime/claude");
});
});
+20 -4
View File
@@ -6,7 +6,7 @@ import { promisify } from "node:util";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import { randomBytes, randomUUID } from "node:crypto";
import { createHash, randomBytes, randomUUID } from "node:crypto";
import { githubLauncherSource } from "./github-launcher.js";
import type { SshRemoteExecutionSpec } from "./ssh.js";
import {
@@ -1442,6 +1442,20 @@ export function readAdapterExecutionTarget(input: {
);
}
/** Keep mutable legacy assets separate for owners sharing the same checkout. */
export function adapterExecutionTargetRuntimeRoot(
target: Extract<AdapterExecutionTarget, { kind: "remote" }>,
adapterKey: string,
baseRoot = path.posix.join(target.remoteCwd, ".paperclip-runtime", adapterKey),
): string {
if (target.transport !== "computer") return baseRoot;
const owner = target.resourceAuthority;
const scope = createHash("sha256").update(JSON.stringify([
owner.computerId, owner.ownerId, owner.generation,
])).digest("hex");
return path.posix.join(baseRoot, "owners", scope);
}
export async function prepareAdapterExecutionTargetRuntime(input: {
runId: string;
target: AdapterExecutionTarget | null | undefined;
@@ -1535,7 +1549,9 @@ export async function prepareAdapterExecutionTargetRuntime(input: {
adapterKey: input.adapterKey,
workspaceLocalDir: input.workspaceLocalDir,
workspaceRemoteDir: input.workspaceRemoteDir,
runtimeRootDir: input.runtimeRootDir,
runtimeRootDir: target.transport === "computer"
? adapterExecutionTargetRuntimeRoot(target, input.adapterKey, input.runtimeRootDir)
: input.runtimeRootDir,
syncWorkspace: input.syncWorkspace,
workspaceInboundMode: input.workspaceInboundMode,
workspaceDurableSeed: input.workspaceDurableSeed,
@@ -2033,7 +2049,7 @@ export async function startAdapterExecutionTargetProcessSessionBridge(input: {
? Math.trunc(input.timeoutSec * 1000)
: target.timeoutMs ?? undefined;
const bridgeRuntimeDir = path.posix.join(
input.runtimeRootDir?.trim() || path.posix.join(target.remoteCwd, ".paperclip-runtime", input.adapterKey),
input.runtimeRootDir?.trim() || adapterExecutionTargetRuntimeRoot(target, input.adapterKey),
"process-sessions",
);
const sessionId = randomUUID();
@@ -4402,7 +4418,7 @@ export async function startAdapterExecutionTargetPaperclipBridge(input: {
const runtimeRootDir =
input.runtimeRootDir?.trim().length
? input.runtimeRootDir.trim()
: path.posix.join(target.remoteCwd, ".paperclip-runtime", input.adapterKey);
: adapterExecutionTargetRuntimeRoot(target, input.adapterKey);
const bridgeRuntimeDir = path.posix.join(runtimeRootDir, "paperclip-bridge");
const queueDir = path.posix.join(bridgeRuntimeDir, "queue");
const assetRemoteDir = path.posix.join(bridgeRuntimeDir, "server");