mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 05:11:49 +01:00
Verify agent harnesses through bounded computer probe owners
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
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.
|
||||
|
||||
Reference in new issue
Block a user