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.