fix(runtime): retain anonymous computer launchers across warm turns

This commit is contained in:
Dotta committed 2026-10-10 09:39:38 -05:00
1 parent 9abc54f48c
commit c772081310
4 files changed
+18 -8

No files matched your search

@@ -8,10 +8,11 @@ import type { CommandManagedRuntimeRunner } from "@paperclipai/adapter-utils/com
import { describe, expect, it, vi } from "vitest";
import { prepareHeartbeatGitHubLaunchers } from "./heartbeat-github-launchers.js";
const computerTarget = { kind: "remote", transport: "computer", remoteCwd: "/workspace" } as Parameters<typeof prepareHeartbeatGitHubLaunchers>[0]["target"];
const target = { kind: "remote" as const, transport: "sandbox" as const, providerKey: "daytona", remoteCwd: "/workspace" };
describe("heartbeat GitHub launcher lifetime", () => {
it.each([target, null])("defers native managed authorization/staging to the session owner (%j)", async (target) => {
it.each([target, computerTarget, null])("defers native managed authorization/staging to the session owner (%j)", async (target) => {
const prepare = vi.fn();
const mint = vi.fn();
const result = await prepareHeartbeatGitHubLaunchers({
@@ -23,7 +24,7 @@ describe("heartbeat GitHub launcher lifetime", () => {
expect(result.cleanupLocation).toBeNull();
expect(result.env).toMatchObject({ GH_TOKEN: "", PAPERCLIP_GITHUB_BROKER_TOKEN: "" });
});
it("keeps anonymous native sandbox launchers stable without issuing a run capability", async () => {
it.each([target, computerTarget])("keeps anonymous native launchers stable without issuing a run capability (%j)", async (target) => {
const createBrokerToken = vi.fn(() => "run-secret");
const prepareLaunchers = vi.fn(async (input) => input.env);
const inputs = { native: true, githubConfigured: false, agentId: "agent-a", target,
@@ -50,7 +51,7 @@ describe("heartbeat GitHub launcher lifetime", () => {
expect(cleanupLaunchers).toHaveBeenCalledExactlyOnceWith({ runId: "failed-run", target });
});
it("does not remove shared anonymous wrappers when staging a later run fails", async () => {
it.each([target, computerTarget])("does not remove shared anonymous wrappers when staging a later run fails (%j)", async (target) => {
const stagingError = new Error("remote launcher staging failed");
const cleanupLaunchers = vi.fn(async () => undefined);
await expect(prepareHeartbeatGitHubLaunchers({
@@ -64,6 +65,7 @@ describe("heartbeat GitHub launcher lifetime", () => {
it.each([
{ native: false, githubConfigured: true, target },
{ native: false, githubConfigured: false, target },
{ native: false, githubConfigured: false, target: computerTarget },
{ native: true, githubConfigured: false, target: null },
])("preserves run-scoped managed capabilities outside anonymous native sandboxes: %j", async (mode) => {
const createBrokerToken = vi.fn(() => "current-run-secret");
@@ -1,6 +1,6 @@
import { createHash } from "node:crypto";
import { githubBrokerEnvironment } from "@paperclipai/adapter-utils/github-launcher";
import { cleanupGitHubOperationLaunchers, prepareGitHubOperationLaunchers } from "@paperclipai/adapter-utils/execution-target";
import { adapterExecutionTargetIsCommandBacked, cleanupGitHubOperationLaunchers, prepareGitHubOperationLaunchers } from "@paperclipai/adapter-utils/execution-target";
type LauncherInput = Parameters<typeof prepareGitHubOperationLaunchers>[0];
@@ -21,12 +21,12 @@ export async function prepareHeartbeatGitHubLaunchers(
if (input.native && input.githubConfigured) {
return { env: githubBrokerEnvironment(input.env, { url: "", token: "" }), cleanupLocation: null };
}
// An unconfigured sandbox has no managed GitHub identity to broker. Its
// An unconfigured command-backed environment has no managed GitHub identity to broker. Its
// token-free wrappers still isolate image credentials, but may live as long
// as the workspace so a warm provider never inherits a deleted run path.
// Other adapter paths retain their run-scoped capability/retirement rules.
const anonymous = input.native && !input.githubConfigured &&
input.target?.kind === "remote" && input.target.transport === "sandbox";
adapterExecutionTargetIsCommandBacked(input.target);
const location = {
runId: anonymous
? `anonymous-${input.agentId}-${createHash("sha256").update(input.env.PATH ?? "").digest("hex").slice(0, 16)}`
@@ -1,3 +1,4 @@
import { prepareHeartbeatGitHubLaunchers } from "../heartbeat-github-launchers.js";
import { gunzipSync } from "node:zlib";
import { configuredEnvironmentProjection } from "../../vendor/paperclip-runner/index.js";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -7609,9 +7610,15 @@ describe("native warm session supervision", () => {
expect(options.existingSession).toBe(session);
await options.onSession?.(session); return result;
});
const environmentForRun = async (runId: string) => (await prepareHeartbeatGitHubLaunchers({
native: true, githubConfigured: false, agentId: name, runId, target: target as never,
cwd: target.remoteCwd, env: {}, brokerUrl: "http://127.0.0.1:3104",
createBrokerToken: () => { throw new Error("Anonymous computer must not mint per-run authority"); },
}, async (input) => input.env)).env;
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
try {
await executePaperclipNativeSession({ db: leaseDb(current), execution: current, runnerInstanceId: name, runnerExecutionTarget: target as never });
await executePaperclipNativeSession({ db: leaseDb(current), execution: current, runnerInstanceId: name, runnerExecutionTarget: target as never,
runnerEnvironment: await environmentForRun(current.binding.runId) });
generation = 2; // The durable admission committed before local reservation.
await vi.advanceTimersByTimeAsync(60_000);
expect(retire).toHaveBeenCalledOnce();
@@ -7620,6 +7627,7 @@ describe("native warm session supervision", () => {
const next = { ...current, binding: { ...current.binding, runId: `${name}-two` } };
const nextRetire = vi.fn(async () => true);
await executePaperclipNativeSession({ db: leaseDb(next), execution: next, runnerInstanceId: name,
runnerEnvironment: await environmentForRun(next.binding.runId),
runnerExecutionTarget: { ...target, resourceAuthority: { ...target.resourceAuthority, generation: 2 }, retire: nextRetire } as never });
await closeWarmNativeSessionsForRun({ runId: next.binding.runId, reason: "fixture cleanup" });
expect(nextRetire).toHaveBeenCalledOnce();
@@ -12612,7 +12612,7 @@ async function createRunnerdBackendWithinSessionClaim(
timeoutMs: 10_000,
});
if (created.exitCode !== 0 || created.timedOut) {
throw new Error(`runner_remote_directory_staging_failed: exit=${result.exitCode} timedOut=${result.timedOut}${result.stderr.trim() ? ` ${redactSensitiveText(result.stderr).trim().slice(-512)}` : ""}`);
throw new Error(`runner_remote_directory_staging_failed: exit=${created.exitCode} timedOut=${created.timedOut}${created.stderr.trim() ? ` ${redactSensitiveText(created.stderr).trim().slice(-512)}` : ""}`);
}
// Codex launch credentials are intentionally excluded from failover