From f6c8fead6c774222887c9aaef85beac837cfc742 Mon Sep 17 00:00:00 2001 From: Nicky Leach Date: Thu, 8 Oct 2026 08:55:47 -0700 Subject: [PATCH] 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 --- packages/plugins/sdk/src/environment-tasks.ts | 6 ++++-- server/src/__tests__/environment-task-runtime.test.ts | 10 ++++++++++ .../__tests__/plugin-environment-driver-seam.test.ts | 11 +++++++++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/plugins/sdk/src/environment-tasks.ts b/packages/plugins/sdk/src/environment-tasks.ts index b7b2c56cc3e..76ca6b1787f 100644 --- a/packages/plugins/sdk/src/environment-tasks.ts +++ b/packages/plugins/sdk/src/environment-tasks.ts @@ -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(), diff --git a/server/src/__tests__/environment-task-runtime.test.ts b/server/src/__tests__/environment-task-runtime.test.ts index 221ff1be99a..d11cf7650a7 100644 --- a/server/src/__tests__/environment-task-runtime.test.ts +++ b/server/src/__tests__/environment-task-runtime.test.ts @@ -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); diff --git a/server/src/__tests__/plugin-environment-driver-seam.test.ts b/server/src/__tests__/plugin-environment-driver-seam.test.ts index ce9d286f4b3..998cff0e411 100644 --- a/server/src/__tests__/plugin-environment-driver-seam.test.ts +++ b/server/src/__tests__/plugin-environment-driver-seam.test.ts @@ -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.