Verify agent harnesses through bounded computer probe owners

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-10 08:31:36 -05:00
1 parent ebb60670c1
commit d29e2a8bfe
8 files changed
+70 -12

No files matched your search

@@ -361,7 +361,11 @@ export function agentEnvironmentTestService(db: Db, pluginWorkerManager?: Plugin
executionTarget: target,
environmentName: environment.name,
fallbackChecks: [],
sandboxIdentityCheck: buildSandboxIdentityCheck({
sandboxIdentityCheck: environment.driver === "computer" ? {
code: "computer_test_identity", level: "info",
message: `Environment test ran on "${environment.name}" with a bounded computer owner.`,
detail: `paperclipLeaseId=${leaseRecord.lease.id}; ownerId=${leaseRecord.lease.providerLeaseId}`,
} : buildSandboxIdentityCheck({
environmentName: environment.name,
lease: leaseRecord.lease,
}),
@@ -433,7 +437,7 @@ export function agentEnvironmentTestService(db: Db, pluginWorkerManager?: Plugin
// The environment drivers the adapter Test route accepts. A local, SSH, or
// sandbox environment can host a probe; a plugin environment cannot.
const ADAPTER_TEST_ALLOWED_ENVIRONMENT_DRIVERS = ["local", "ssh", "sandbox"];
const ADAPTER_TEST_ALLOWED_ENVIRONMENT_DRIVERS = ["local", "ssh", "sandbox", "computer"];
// The fail-closed tenant-binding guard for the adapter Test route. A caller
// may name any instance environment by id, so the route must reject an
@@ -0,0 +1,24 @@
import { describe, expect, it, vi } from "vitest";
import { createComputerEnvironmentDriver } from "./computer-environment-driver.js";
const state = vi.hoisted(() => {
const binding = { owner: { computerId: "computer", ownerId: "owner", generation: 1 },
listenerPort: 43127, remoteCwd: "/remote/workspace", placementId: "placement", agentHome: "/remote/home" };
return { admit: vi.fn(async () => binding), admitProbe: vi.fn(async () => binding),
retire: vi.fn(async () => ({ retired: true })), acquireLease: vi.fn(async (input: unknown) => input) };
});
vi.mock("../modules/computers/index.js", () => ({ computerService: () => state }));
vi.mock("./environments.js", () => ({ environmentService: () => state }));
vi.mock("./instance-settings.js", () => ({ instanceSettingsService: () => ({ getExperimental: async () => ({ enableBoatEnvironments: true }) }) }));
describe("computer environment probe ownership", () => {
it("admits a bounded probe without fabricating a heartbeat run", async () => {
const db = { update: () => ({ set: () => ({ where: async () => undefined }) }) };
const driver = createComputerEnvironmentDriver(db as never);
await driver.acquireRunLease({ companyId: "company", agentId: "agent", heartbeatRunId: null,
environment: { id: "environment", config: {} }, adapterType: "paperclip_runner" } as never);
expect(state.admit).not.toHaveBeenCalled();
expect(state.admitProbe).toHaveBeenCalledWith(expect.objectContaining({ companyId: "company", environmentId: "environment", agentId: "agent", probeId: expect.any(String) }));
expect(state.acquireLease).toHaveBeenCalledWith(expect.objectContaining({ heartbeatRunId: null, providerLeaseId: "owner", metadata: expect.objectContaining({ driver: "computer", computerOwner: { computerId: "computer", ownerId: "owner", generation: 1 } }) }));
});
});
@@ -3,7 +3,7 @@ import { environmentLeases } from "@paperclipai/db";
import { remoteTerminationReceipt } from "./remote-execution-termination.js";
import { instanceSettingsService } from "./instance-settings.js";
import { resolvePaperclipRunnerIdleTimeoutMs } from "@paperclipai/adapter-utils";
import { createHash } from "node:crypto";
import { createHash, randomUUID } from "node:crypto";
import type { Db } from "@paperclipai/db";
import type { EnvironmentLease } from "@paperclipai/shared";
import type { AdapterComputerExecutionTarget, EffectiveExecutionCapabilities } from "@paperclipai/adapter-utils/execution-target";
@@ -45,11 +45,13 @@ export function createComputerEnvironmentDriver(db: Db): EnvironmentRuntimeDrive
driver: "computer",
async acquireRunLease(input) {
if (!(await instanceSettingsService(db).getExperimental()).enableBoatEnvironments) throw new Error("boat_environments_disabled");
if (!input.agentId || !input.heartbeatRunId) throw new Error("computer_agent_run_required");
if (!input.agentId) throw new Error("computer_agent_required");
const idleTimeoutMs = resolvePaperclipRunnerIdleTimeoutMs(input.environment.config.runnerIdleTimeoutMs);
const binding = await computers.admit({ companyId: input.companyId, environmentId: input.environment.id,
const binding = input.heartbeatRunId ? await computers.admit({ companyId: input.companyId, environmentId: input.environment.id,
agentId: input.agentId, runId: input.heartbeatRunId, idleTimeoutMs,
sessionKey: createHash("sha256").update(JSON.stringify([input.agentId, input.executionWorkspaceId ?? input.issueId, input.adapterType])).digest("hex") });
sessionKey: createHash("sha256").update(JSON.stringify([input.agentId, input.executionWorkspaceId ?? input.issueId, input.adapterType])).digest("hex") })
: await computers.admitProbe({ companyId: input.companyId, environmentId: input.environment.id,
agentId: input.agentId, probeId: randomUUID(), idleTimeoutMs });
try {
// The durable owner advanced generation; prior retained bookkeeping no
// longer owns the process and must not be reaped as an allocation.