Match task Runner identities to the PRP contract

Accept PRP stable-ID characters and the 160-character limit for Runner identity fields. Type the worker submission fixture and assert its project list reaches the plugin hook.

Verified SDK build, server typecheck, and all 22 focused task and worker tests.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Nicky LeachandPaperclip committed 2026-10-08 08:55:47 -07:00
1 parent 62467a9a5a
commit f6c8fead6c
3 files changed
+23 -4

No files matched your search

@@ -3,6 +3,8 @@ import type { PluginEnvironmentDriverBaseParams, PluginEnvironmentLease } from "
// Provider IDs are opaque; providers validate their own addressing constraints.
const providerId = z.string().min(1);
// PRP identity.schema.json stableId.
const runnerIdentity = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/);
const identifier = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,95}$/);
const secureUrl = z.string().url().refine(value => {
const url = new URL(value);
@@ -19,8 +21,8 @@ export const environmentTaskOperationSchema = z.discriminatedUnion("kind", [
protocolMin: z.number().int().min(1).max(Number.MAX_SAFE_INTEGER),
protocolMax: z.number().int().min(1).max(Number.MAX_SAFE_INTEGER),
harness: identifier,
runnerId: identifier, leaseId: identifier, runId: identifier,
sessionId: identifier, turnId: identifier, itemId: identifier,
runnerId: runnerIdentity, leaseId: runnerIdentity, runId: runnerIdentity,
sessionId: runnerIdentity, turnId: runnerIdentity, itemId: runnerIdentity,
}).strict().refine(value => value.protocolMin <= value.protocolMax, "Invalid PRP version range"),
bootstrapTicket: z.string().min(1).max(65_536),
}).strict(),
@@ -132,6 +132,16 @@ describe("environment task admission", () => {
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("accepts PRP identity characters and length limits", () => {
for (const field of ["runnerId", "leaseId", "runId", "sessionId", "turnId", "itemId"]) {
for (const value of ["session:part.1", "a".repeat(160)]) {
expect(environmentTaskOperationSchema.safeParse({ ...submit, runner: { ...submit.runner, [field]: value } }).success).toBe(true);
}
for (const value of ["a".repeat(161), "../escape", "a/b", ""]) {
expect(environmentTaskOperationSchema.safeParse({ ...submit, runner: { ...submit.runner, [field]: value } }).success).toBe(false);
}
}
});
it("validates the client's inclusive PRP version range", () => {
for (const range of [{ protocolMin: 1, protocolMax: 1 }, { protocolMin: 1, protocolMax: 2 }, { protocolMin: 3, protocolMax: 5 }]) {
expect(environmentTaskOperationSchema.safeParse({ ...submit, runner: { ...submit.runner, ...range } }).success).toBe(true);
@@ -8,6 +8,7 @@ import {
parseMessage,
serializeMessage,
} from "../../../packages/plugins/sdk/src/protocol.js";
import type { PluginEnvironmentTaskParams } from "../../../packages/plugins/sdk/src/environment-tasks.js";
import { definePlugin } from "../../../packages/plugins/sdk/src/define-plugin.js";
import { startWorkerRpcHost } from "../../../packages/plugins/sdk/src/worker-rpc-host.js";
import { pluginManifestV1Schema, type PaperclipPluginManifestV1 } from "@paperclipai/shared";
@@ -121,11 +122,17 @@ describe("plugin environment driver seam", () => {
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" } };
const projectIds = ["10000000-0000-4000-8000-000000000001", "10000000-0000-4000-8000-000000000002"];
const params: PluginEnvironmentTaskParams = { driverKey: "fake-plugin", companyId: "company", environmentId: "environment", config: {},
taskId: "attempt", runId: "run", agentId: "agent", projectIds, lease: { providerLeaseId: "attempt" },
operation: { kind: "submit", projectIds, bootstrapTicket: "transient-test-ticket", runner: {
protocolMin: 1, protocolMax: 2, harness: "codex", runnerId: "runner", leaseId: "lease", runId: "run", sessionId: "session:part.1", turnId: "turn", itemId: "item",
} } };
stdin.write(serializeMessage(createRequest("environmentTask", params, 2)));
await waitForResponses(responses, 2);
expect(responses[1]).toMatchObject({ result: { kind: "accepted", taskId: "attempt" } });
expect(calls[0]).toMatchObject({ projectIds, operation: { kind: "submit", projectIds } });
stdin.write(serializeMessage(createRequest("environmentTask", { ...params, operation: { kind: "status" } }, 3)));
await waitForResponses(responses, 3);
expect(isJsonRpcErrorResponse(responses[2])).toBe(true); // Wrong response kind.