Add typed task operations to environment plugins

Provide a negotiated task RPC for environment providers that own Runner
admission. Dispatch from persisted company-scoped leases and validate typed
requests and receipts without coupling the host to a provider API.

Cover the worker contract, host-derived identity, capability negotiation,
and failed or mismatched receipts. Document retry and credential handling.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Nicky LeachandPaperclip committed 2026-10-06 18:47:42 -07:00
1 parent faa8e452c7
commit c84d7b35ec
13 files changed
+348

No files matched your search

@@ -0,0 +1,77 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PgDialect } from "drizzle-orm/pg-core";
import type { Db } from "@paperclipai/db";
import { environmentTaskOperationSchema, parseEnvironmentTaskResult } from "@paperclipai/plugin-sdk";
import { executeEnvironmentTask } from "../services/environment-task-runtime.js";
const state = vi.hoisted(() => ({ plugin: {} as any }));
vi.mock("../services/plugin-registry.js", () => ({ pluginRegistryService: () => ({ getById: vi.fn(async () => state.plugin) }) }));
const leaseId = "10000000-0000-4000-8000-000000000001";
const row = () => ({
lease: { id: leaseId, companyId: "company", environmentId: "environment", providerLeaseId: "attempt-1", heartbeatRunId: "run", issueId: "issue", status: "active", expiresAt: null,
metadata: { driver: "plugin", pluginId: "original", driverKey: "tasks" } },
environment: { id: "environment", config: { pluginKey: "test.provider", driverKey: "tasks", driverConfig: {} } },
run: { id: "run", agentId: "agent", status: "running" },
});
function database(value: ReturnType<typeof row> | null = row()) {
const results = [value ? [value] : [], [{ projectId: "project" }]];
const query: any = { from: () => query, innerJoin: () => query, where: vi.fn(() => query), limit: () => Promise.resolve(results.shift()) };
return { db: { select: () => query } as unknown as Db, query };
}
function worker(result: unknown = { kind: "accepted", taskId: "attempt-1" }) {
return { getWorker: vi.fn(() => ({ supportedMethods: ["environmentTask"] })), call: vi.fn(async () => result) };
}
const submit = { kind: "submit" as const, runner: { revision: "a".repeat(40), harness: "codex", runnerId: "runner", leaseId, runId: "run", sessionId: "session", turnId: "turn", itemId: "item" }, bootstrapTicket: "transient-test-ticket" };
beforeEach(() => {
state.plugin = { id: "original", pluginKey: "test.provider", status: "ready", manifestJson: { capabilities: ["environment.drivers.register"], environmentDrivers: [{ driverKey: "tasks", supportsTasks: true }] } };
});
describe("typed environment task admission", () => {
it("dispatches with host-derived scope and a persisted attempt identity", async () => {
const { db, query } = database(); const workers = worker();
await expect(executeEnvironmentTask(db, workers as never, { companyId: "company", leaseId, operation: submit })).resolves.toEqual({ kind: "accepted", taskId: "attempt-1" });
const scope = new PgDialect().sqlToQuery(query.where.mock.calls[0][0]);
expect(scope.sql).toContain('"environment_leases"."company_id" =');
expect(scope.sql).toContain('"environment_leases"."id" =');
expect(scope.params).toEqual([leaseId, "company"]);
expect(workers.call).toHaveBeenCalledWith("original", "environmentTask", expect.objectContaining({
taskId: "attempt-1", companyId: "company", agentId: "agent", projectId: "project", runId: "run", operation: submit,
}), 15_000);
});
it("rejects an absent or cross-company lease before calling a worker", async () => {
const workers = worker();
await expect(executeEnvironmentTask(database(null).db, workers as never, { companyId: "other", leaseId, operation: submit })).rejects.toThrow("lease unavailable");
expect(workers.call).not.toHaveBeenCalled();
});
it("rejects mismatched Runner binding and inactive submission", async () => {
const workers = worker();
await expect(executeEnvironmentTask(database().db, workers as never, { companyId: "company", leaseId, operation: { ...submit, runner: { ...submit.runner, runId: "other" } } })).rejects.toThrow("identity mismatch");
const value = row(); value.lease.status = "released";
await expect(executeEnvironmentTask(database(value).db, workers as never, { companyId: "company", leaseId, operation: submit })).rejects.toThrow("not active");
expect(workers.call).not.toHaveBeenCalled();
});
it("uses the pinned provider for cleanup after environment edits", async () => {
const value = row(); value.lease.status = "released"; value.environment.config.pluginKey = "replacement";
const workers = worker();
await executeEnvironmentTask(database(value).db, workers as never, { companyId: "company", leaseId, operation: { kind: "stop" } });
expect(workers.call).toHaveBeenCalledWith("original", "environmentTask", expect.objectContaining({ config: {}, operation: { kind: "stop" } }), 15_000);
});
it.each(["manifest", "worker"])("requires live %s support", async kind => {
const workers = worker();
if (kind === "manifest") state.plugin.manifestJson.environmentDrivers[0].supportsTasks = false;
else workers.getWorker.mockReturnValue({ supportedMethods: [] });
await expect(executeEnvironmentTask(database().db, workers as never, { companyId: "company", leaseId, operation: submit })).rejects.toThrow("provider unavailable");
expect(workers.call).not.toHaveBeenCalled();
});
it("rejects wrong task receipts and sanitizes worker errors", async () => {
await expect(executeEnvironmentTask(database().db, worker({ kind: "accepted", taskId: "other" }) as never, { companyId: "company", leaseId, operation: submit })).rejects.toThrow("reconcile the same task");
const workers = worker(); workers.call.mockRejectedValue(new Error("private-credential"));
await expect(executeEnvironmentTask(database().db, workers as never, { companyId: "company", leaseId, operation: submit })).rejects.toThrow(/^Environment task operation unavailable; reconcile the same task before retrying$/);
});
it("validates operation and result shape without making acceptance mean readiness", () => {
expect(environmentTaskOperationSchema.safeParse({ ...submit, surprise: true }).success).toBe(false);
expect(environmentTaskOperationSchema.safeParse({ ...submit, runner: { ...submit.runner, connectUrl: "wss://user:secret@example.test/path" } }).success).toBe(false);
expect(() => parseEnvironmentTaskResult({ kind: "status" }, "attempt-1", { kind: "accepted", taskId: "attempt-1" })).toThrow();
expect(parseEnvironmentTaskResult(submit, "attempt-1", { kind: "accepted", taskId: "attempt-1" }).kind).toBe("accepted");
});
});
@@ -94,6 +94,48 @@ describe("plugin environment driver seam", () => {
});
});
it("negotiates and validates typed task RPCs", async () => {
const calls: unknown[] = [];
const plugin = definePlugin({
async setup() {},
async onEnvironmentTask(input) {
calls.push(input);
return { kind: "accepted", taskId: input.taskId };
},
});
const stdin = new PassThrough();
const stdout = new PassThrough();
const host = startWorkerRpcHost({ plugin, stdin, stdout });
const responses: unknown[] = [];
stdout.on("data", chunk => {
for (const line of String(chunk).split("\n").filter(Boolean)) responses.push(parseMessage(line));
});
try {
const manifest = { ...baseManifest, environmentDrivers: [{ ...baseManifest.environmentDrivers![0], supportsTasks: true }] };
expect(pluginManifestV1Schema.parse(manifest).environmentDrivers![0].supportsTasks).toBe(true);
stdin.write(serializeMessage(createRequest("initialize", {
manifest, config: {}, instanceInfo: { instanceId: "instance-1", hostVersion: "1.0.0" }, apiVersion: 1,
}, 1)));
await waitForResponses(responses, 1);
const initialized = responses[0];
expect(isJsonRpcSuccessResponse(initialized)).toBe(true);
if (!isJsonRpcSuccessResponse(initialized)) return;
expect(initialized.result.supportedMethods).toContain("environmentTask");
const params = { driverKey: "fake-plugin", companyId: "company", environmentId: "environment", config: {},
taskId: "attempt", runId: "run", agentId: "agent", projectId: null, lease: { providerLeaseId: "attempt" }, operation: { kind: "stop" } };
stdin.write(serializeMessage(createRequest("environmentTask", params, 2)));
await waitForResponses(responses, 2);
expect(responses[1]).toMatchObject({ result: { kind: "accepted", taskId: "attempt" } });
stdin.write(serializeMessage(createRequest("environmentTask", { ...params, operation: { kind: "status" } }, 3)));
await waitForResponses(responses, 3);
expect(isJsonRpcErrorResponse(responses[2])).toBe(true); // Wrong response kind.
stdin.write(serializeMessage(createRequest("environmentTask", { ...params, operation: { kind: "shell", command: "echo test" } }, 4)));
await waitForResponses(responses, 4);
expect(isJsonRpcErrorResponse(responses[3])).toBe(true);
expect(calls).toHaveLength(2); // Invalid operation did not reach the plugin.
} finally { host.stop(); }
});
it("dispatches environment driver worker hooks and reports support", async () => {
const plugin = definePlugin({
async setup() {},
@@ -1,3 +1,5 @@
import { executeEnvironmentTask } from "./environment-task-runtime.js";
import type { PluginEnvironmentTaskOperation } from "@paperclipai/plugin-sdk";
import { hasStopOnlyCleanup, prepareSandboxStopAndRetain, readStopOnlyCleanup, settleStopOnlyCleanup, stopOnlyCleanupKey } from "./sandbox-stop-and-retain.js";
import { readEnvironmentCreationCleanupError } from "@paperclipai/plugin-sdk";
import { remoteTerminationReceipt } from "./remote-execution-termination.js";
@@ -3870,6 +3872,12 @@ export function environmentRuntimeService(
return resolveSandboxDuplexBridgeInput(experimental);
},
/** Typed task providers own process admission; this does not execute a shell command. */
async task(input: { companyId: string; leaseId: string; operation: PluginEnvironmentTaskOperation }) {
if (!options.pluginWorkerManager) throw new Error("Environment task worker manager unavailable");
return executeEnvironmentTask(db, options.pluginWorkerManager, input);
},
async acquireRunLease(input: {
companyId: string;
environment: Environment;
@@ -0,0 +1,62 @@
import { and, eq } from "drizzle-orm";
import { environmentLeases, environments, heartbeatRuns, issues, type Db } from "@paperclipai/db";
import { environmentTaskOperationSchema, parseEnvironmentTaskResult, type PluginEnvironmentTaskOperation } from "@paperclipai/plugin-sdk";
import { pluginRegistryService } from "./plugin-registry.js";
import type { PluginWorkerManager } from "./plugin-worker-manager.js";
/** Server-only dispatch. The caller supplies an authorized company and a persisted lease,
* never a plugin identity, resource endpoint, or agent/project authority.
* A timeout is ambiguous: retry the same operation against the same lease.
*/
export async function executeEnvironmentTask(db: Db, workers: PluginWorkerManager, input: {
companyId: string;
leaseId: string;
operation: PluginEnvironmentTaskOperation;
}) {
const operation = environmentTaskOperationSchema.parse(input.operation);
const [row] = await db.select({ lease: environmentLeases, environment: environments, run: heartbeatRuns })
.from(environmentLeases)
.innerJoin(environments, eq(environments.id, environmentLeases.environmentId))
.innerJoin(heartbeatRuns, and(eq(heartbeatRuns.id, environmentLeases.heartbeatRunId), eq(heartbeatRuns.companyId, environmentLeases.companyId)))
.where(and(eq(environmentLeases.id, input.leaseId), eq(environmentLeases.companyId, input.companyId))).limit(1);
if (!row || !row.lease.providerLeaseId) throw new Error("Environment task lease unavailable");
const { lease, environment, run } = row;
const taskId = row.lease.providerLeaseId;
if ((operation.kind === "submit" || operation.kind === "connection") &&
(lease.status !== "active" || (lease.expiresAt && lease.expiresAt.getTime() <= Date.now()) ||
run.status !== "running")) throw new Error("Environment task lease is not active");
if (operation.kind === "submit" && (operation.runner.runId !== run.id || operation.runner.leaseId !== lease.id)) {
throw new Error("Environment task runner identity mismatch");
}
const metadata = lease.metadata ?? {};
if (metadata.driver !== "plugin" || typeof metadata.pluginId !== "string" || typeof metadata.driverKey !== "string") {
throw new Error("Environment task lease has no pinned plugin driver");
}
const plugin = await pluginRegistryService(db).getById(metadata.pluginId);
const driver = plugin?.manifestJson.environmentDrivers?.find(value => value.driverKey === metadata.driverKey);
if (!plugin || plugin.status !== "ready" || !driver?.supportsTasks ||
!plugin.manifestJson.capabilities.includes("environment.drivers.register") ||
!workers.getWorker(plugin.id)?.supportedMethods.includes("environmentTask")) {
throw new Error("Environment task provider unavailable");
}
const project = lease.issueId ? await db.select({ projectId: issues.projectId }).from(issues)
.where(and(eq(issues.id, lease.issueId), eq(issues.companyId, input.companyId))).limit(1).then(rows => rows[0]) : null;
if (lease.issueId && !project) throw new Error("Environment task issue unavailable");
// Provider identity comes from the lease. Editing the environment must never
// redirect status or cleanup to a replacement plugin.
const config = environment.config as Record<string, unknown>;
const driverConfig = config.pluginKey === plugin.pluginKey && config.driverKey === metadata.driverKey
? (config.driverConfig as Record<string, unknown> | undefined) ?? {} : {};
try {
const result = await workers.call(plugin.id, "environmentTask", {
driverKey: metadata.driverKey, companyId: input.companyId, environmentId: environment.id,
issueId: lease.issueId, config: driverConfig,
taskId, runId: run.id, agentId: run.agentId, projectId: project?.projectId ?? null,
lease: { providerLeaseId: lease.providerLeaseId, metadata: lease.metadata ?? undefined }, operation,
}, 15_000);
return parseEnvironmentTaskResult(operation, taskId, result);
} catch {
// Worker errors and schema diagnostics can contain request credentials.
throw new Error("Environment task operation unavailable; reconcile the same task before retrying");
}
}