Keep computer process capabilities stable across warm admission

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-10 08:31:36 -05:00
1 parent 60b3d32722
commit 52aee03920
7 files changed
+59 -15

No files matched your search

@@ -0,0 +1,9 @@
import type { AdapterCommandBackedExecutionTarget, AdapterExecutionTarget } from "./execution-target.js";
/** Command transport capability; sandbox allocation ownership is unchanged. */
export function adapterExecutionTargetIsCommandBacked(
target: AdapterExecutionTarget | null | undefined,
): target is AdapterCommandBackedExecutionTarget {
return target?.kind === "remote" && (target.transport === "sandbox" || target.transport === "computer");
}
@@ -1,3 +1,5 @@
import { adapterExecutionTargetIsCommandBacked } from "./execution-target-kind.js";
export { adapterExecutionTargetIsCommandBacked } from "./execution-target-kind.js";
import fs from "node:fs/promises";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
@@ -237,6 +239,8 @@ export interface AdapterSandboxExecutionTarget extends AdapterExecutionTargetWor
export interface AdapterComputerExecutionTarget extends Omit<AdapterSandboxExecutionTarget, "transport" | "sandboxLeaseAcquisition"> {
transport: "computer";
listenerPort: number;
/** Native process control survives attempt handoff only for the same process claim. */
processRunner: CommandManagedRuntimeRunner;
resourceAuthority: { kind: "computer-owner"; computerId: string; ownerId: string; generation: number };
fileAuthority: { kind: "remote-persistent"; placementId: string; root: string; agentHome: string };
launch(input: { command: string; args?: string[]; cwd?: string; env?: Record<string, string> }): Promise<unknown>;
@@ -248,12 +252,6 @@ export interface AdapterComputerExecutionTarget extends Omit<AdapterSandboxExecu
export type AdapterCommandBackedExecutionTarget = AdapterSandboxExecutionTarget | AdapterComputerExecutionTarget;
/** Command transport capability; sandbox allocation ownership is unchanged. */
export function adapterExecutionTargetIsCommandBacked(
target: AdapterExecutionTarget | null | undefined,
): target is AdapterCommandBackedExecutionTarget {
return target?.kind === "remote" && (target.transport === "sandbox" || target.transport === "computer");
}
export type AdapterExecutionTarget =
| AdapterLocalExecutionTarget
@@ -218,6 +218,7 @@ it("uses each computer owner's allocated port without changing the sandbox defau
const base = {
kind: "remote" as const, transport: "computer" as const, providerKey: "boat", remoteCwd: "/home/user/project",
leaseId: "attempt-lease", listenerPort: 45101, effectiveCapabilities: capabilities, getRunnerIngressEndpoint,
processRunner: { execute: async () => ({ exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "", pid: null, startedAt: new Date().toISOString() }) },
resourceAuthority: { kind: "computer-owner" as const, computerId: "machine", ownerId: "owner-a", generation: 2 },
fileAuthority: { kind: "remote-persistent" as const, placementId: "placement", root: "/home/user/project", agentHome: "/home/user/agent-a" },
launch: async () => ({}), inspectProcess: async () => ({ running: true, claim: {} }),
@@ -230,7 +231,7 @@ it("uses each computer owner's allocated port without changing the sandbox defau
expect(resolved).toMatchObject({ mode: "provider_ingress", listenPort: port });
expect(getRunnerIngressEndpoint).toHaveBeenLastCalledWith({ leaseId: "attempt-lease", port, path: "/api/runner/v1/connect/run-1" });
}
const { listenerPort, resourceAuthority, fileAuthority, launch, inspectProcess, retainWarm, retire, computerTool, ...common } = base;
const { listenerPort, processRunner, resourceAuthority, fileAuthority, launch, inspectProcess, retainWarm, retire, computerTool, ...common } = base;
const resolved = await resolvePaperclipRunnerTransport({ target: { ...common, transport: "sandbox" }, runId: "sandbox-run",
localConnectUrl: "ws://localhost/unused", runnerIngressAuthorized: true });
expect(resolved).toMatchObject({ mode: "provider_ingress", listenPort: 43127 });
@@ -1,4 +1,5 @@
import { adapterExecutionTargetIsCommandBacked, type AdapterExecutionTarget } from "./execution-target.js";
import type { AdapterExecutionTarget } from "./execution-target.js";
import { adapterExecutionTargetIsCommandBacked } from "./execution-target-kind.js";
export const PAPERCLIP_RUNNER_INGRESS_PORT = 43_127;
export const PAPERCLIP_RUNNER_CONNECT_PATH_PREFIX = "/api/runner/v1/connect";
@@ -76,10 +76,12 @@ export function createComputerEnvironmentDriver(db: Db): EnvironmentRuntimeDrive
return { providerLeaseId: lease.providerLeaseId, state: "stopped" };
},
async realizeWorkspace({ lease, workspace }) {
const request = workspace.metadata?.workspaceRealizationRequest as { source?: { projectId?: string; repoUrl?: string; branchName?: string; strategy?: string }; issueId?: string } | undefined;
const request = workspace.metadata?.workspaceRealizationRequest as { source?: { projectId?: string; repoUrl?: string; repoRef?: string; branchName?: string; strategy?: string }; issueId?: string } | undefined;
const result = await computers.realizeWorkspace({ ...scope(lease), owner: computerOwnerFromLease(lease),
projectId: request?.source?.projectId ?? undefined, taskId: request?.issueId ?? undefined,
repositoryUrl: request?.source?.repoUrl ?? undefined, branch: request?.source?.branchName ?? undefined,
repositoryUrl: request?.source?.repoUrl ?? undefined,
branch: request?.source?.branchName ?? request?.source?.repoRef ?? undefined,
baseRef: request?.source?.repoRef ?? undefined,
mode: request?.source?.strategy === "git_worktree" ? "worktree" : "shared" });
await leases.updateLeaseMetadata(lease.id, { ...lease.metadata, remoteCwd: result.remoteCwd,
fileAuthority: { kind: "remote-persistent", placementId: result.placementId, root: result.remoteCwd, agentHome: result.agentHome } });
@@ -88,7 +90,8 @@ export function createComputerEnvironmentDriver(db: Db): EnvironmentRuntimeDrive
},
async execute({ lease, command, args, cwd, env, stdin, timeoutMs, onLog }) {
const binding = await recover(lease);
return binding.runner.execute({ command, args, cwd: cwd ?? binding.remoteCwd, env, stdin, timeoutMs, onLog });
return binding.runner.execute({ command, args, cwd: cwd ?? binding.remoteCwd, env, stdin, timeoutMs,
onLog: onLog ? async (stream, chunk) => { await onLog(stream, chunk); } : undefined });
},
async resolveCapabilities() { return COMPUTER_CAPABILITIES; },
};
@@ -101,13 +104,13 @@ export async function computerExecutionTarget(db: Db, lease: EnvironmentLease, i
const owner = computerOwnerFromLease(lease);
const binding = await computers.recover({ ...scope, owner });
const ingress = async (path: string): Promise<RunnerIngressEndpoint> => {
const endpoint = await binding.ingress({ path });
const endpoint = await binding.process.ingress({ path });
return { kind: "authenticated_websocket", websocketUrl: endpoint.url,
secretHeaders: Object.entries(endpoint.secretHeaders).map(([name, value]) => ({ name, value })),
generation: `${owner.ownerId}:${owner.generation}`, refresh: () => ingress(path), close: async () => {} };
};
return { kind: "remote", transport: "computer", providerKey: "boat", environmentId: lease.environmentId, leaseId: lease.id,
remoteCwd: binding.remoteCwd, listenerPort: binding.listenerPort, runner: binding.runner, shellCommand: "bash",
remoteCwd: binding.remoteCwd, listenerPort: binding.listenerPort, runner: binding.runner, processRunner: binding.process.runner, shellCommand: "bash",
effectiveCapabilities: COMPUTER_CAPABILITIES, reusableLeaseConfigured: true,
runnerLifecyclePolicy: { mode: "warm", idleTimeoutMs },
resourceAuthority: { kind: "computer-owner", ...owner },
@@ -705,6 +705,29 @@ describe("remote runner launch fingerprint compatibility", () => {
});
describe("remote runner process supervision", () => {
it("launches a computer runner through owned process admission, never detached shell allocation", async () => {
let nonce = "";
const launch = vi.fn(async (request: { args: string[] }) => { nonce = request.args[4]!; return {}; });
const execute = vi.fn(async (request: { args: string[] }) => {
const label = request.args[2];
return { exitCode: label === "paperclip-runner-monitor" ? 3 : 0, timedOut: false, signal: null,
stdout: label === "paperclip-runner-process-identity" ? `${nonce}\n4321\n2026-09-06T00:00:00.000Z\ncomputer-runner\nlinux:abcd-1234:100\n`
: label === "paperclip-runner-diagnostics" ? "stopped" : Buffer.from('{}').toString('base64'), stderr: "" };
});
const launcher = createRemoteRunnerProcessLauncher({
target: { kind: "remote", transport: "computer", remoteCwd: "/home/user/project", launch } as never,
runner: { execute } as never, remoteBinary: "/runtime/runnerd", processIdentityPath: "/runtime/state/identity",
stateDirectory: "/runtime/state", diagnosticsDirectory: "/runtime/logs", runnerInstanceId: "computer-runner",
});
const handle = launcher({ command: "/controller/runnerd", args: ["--runner-id", "computer-runner"], cwd: "/controller", environment: {} });
await handle.completion;
expect(launch).toHaveBeenCalledOnce();
expect(launch.mock.calls[0]![0].args).toContain("/runtime/runnerd");
expect(launch.mock.calls[0]![0].args[1]).not.toContain("nohup");
expect(execute.mock.calls.every(([request]) => request.args[2] !== "paperclip-runner-launch")).toBe(true);
expect(handle.child.pid).toBe(4321);
});
it.each(["delivered", "sandbox_missing", "logging_failed"] as const)(
"detaches runnerd and contains asynchronous signal failures (%s)", async (signalOutcome) => {
let launchNonce = "";
@@ -554,7 +554,7 @@ export async function claimWarmNativeInstructionCopy(input: {
function instructionTargetIdentity(target?: AdapterExecutionTarget | null): string {
return JSON.stringify(target?.kind === "remote" ? {
environmentId: target.environmentId, cwd: target.remoteCwd,
providerLeaseId: target.transport === "sandbox" ? target.sandboxLeaseAcquisition?.providerLeaseId : target.transport === "computer" ? target.resourceAuthority.computerId : target.spec,
providerLeaseId: target.transport === "sandbox" ? target.sandboxLeaseAcquisition?.providerLeaseId : target.transport === "computer" ? [target.resourceAuthority.computerId, target.resourceAuthority.ownerId] : target.spec,
} : { kind: "local", environmentId: target?.environmentId });
}
@@ -567,6 +567,12 @@ export async function reserveWarmNativeInstructionDirectory(input: {
for (const [id, entry] of warmNativeSessions) {
if (entry.companyId !== input.companyId || entry.agentId !== input.agentId || entry.instructionCopy?.runId !== input.previousRunId) continue;
if (entry.busy || entry.preparingRunId) throw new Error("native_session_supervisor_busy");
if (input.target?.kind === "remote" && input.target.transport === "computer" &&
entry.computerTarget?.resourceAuthority.computerId === input.target.resourceAuthority.computerId &&
entry.computerTarget.resourceAuthority.ownerId === input.target.resourceAuthority.ownerId &&
entry.computerTarget.resourceAuthority.generation < input.target.resourceAuthority.generation) {
entry.computerTarget = input.target;
}
if (entry.idleTimer) clearTimeout(entry.idleTimer);
entry.idleTimer = null;
entry.preparingRunId = input.runId;
@@ -8463,6 +8469,9 @@ async function executePaperclipNativeSessionWithinScope(
entry.credentialRunId !== input.execution.binding.runId);
if (
input.refreshTools === true ||
(input.runnerExecutionTarget?.kind === "remote" && input.runnerExecutionTarget.transport === "computer" &&
(entry.computerTarget?.resourceAuthority.computerId !== input.runnerExecutionTarget.resourceAuthority.computerId ||
entry.computerTarget?.resourceAuthority.ownerId !== input.runnerExecutionTarget.resourceAuthority.ownerId)) ||
entry.closeOnReleaseReason !== undefined ||
entry.configDigest !== warmConfigDigest ||
entry.configuredEnvironmentDigest !== configuredEnvironmentDigest ||
@@ -11386,7 +11395,7 @@ async function createRunnerdBackendWithinSessionClaim(
spec: remoteTarget.spec,
defaultCwd: remoteTarget.remoteCwd,
})
: remoteTarget.runner
: remoteTarget.transport === "computer" ? remoteTarget.processRunner : remoteTarget.runner
: null;
if (remoteTarget && !remoteCommandRunner) {
throw new Error(