mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 00:53:38 +02:00
fix(runtime): retain anonymous computer launchers across warm turns
This commit is contained in:
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
|
||||
|
||||
Reference in new issue
Block a user