mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 02:44:50 +01:00
fix(runner): precompile Phase 7 validators for strict CSP
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
4e625a5dc2
commit
da0d32d74a
5 files changed
+88
-39
No files matched your search
@@ -31,7 +31,7 @@
|
||||
"build:rust": "cargo build --manifest-path runner/Cargo.toml --locked --workspace",
|
||||
"build:browser": "vite build --config vite.config.ts",
|
||||
"build:phase5": "vite build --config vite.phase5.config.ts",
|
||||
"build:phase7": "vite build --config vite.phase7.config.ts",
|
||||
"build:phase7": "vite build --config vite.phase7.config.ts && node scripts/check-phase7-csp-bundle.mjs",
|
||||
"typecheck": "pnpm run typecheck:typescript && pnpm run typecheck:rust && pnpm run typecheck:browser",
|
||||
"typecheck:typescript": "pnpm run check:protocol-types && tsc -p tsconfig.json --noEmit",
|
||||
"typecheck:rust": "cargo check --manifest-path runner/Cargo.toml --locked --workspace",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { readFile, readdir } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const assetsDirectory = resolve(packageRoot, "dist-phase7/assets");
|
||||
const bundleNames = (await readdir(assetsDirectory)).filter((name) => name.endsWith(".js"));
|
||||
|
||||
if (bundleNames.length === 0) {
|
||||
throw new Error("Phase 7 build produced no JavaScript bundle");
|
||||
}
|
||||
|
||||
const forbidden = [
|
||||
["new Function", "dynamic Function construction"],
|
||||
["Error compiling schema, function code:", "AJV runtime schema compilation"],
|
||||
];
|
||||
|
||||
for (const name of bundleNames) {
|
||||
const source = await readFile(resolve(assetsDirectory, name), "utf8");
|
||||
for (const [needle, description] of forbidden) {
|
||||
if (source.includes(needle)) {
|
||||
throw new Error(`${name} contains ${description}, which strict Phase 7I CSP blocks`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write("Phase 7 bundle is compatible with the strict Phase 7I CSP.\n");
|
||||
@@ -2,9 +2,16 @@ import { readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import standaloneCode from "ajv/dist/standalone/index.js";
|
||||
|
||||
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const schemaDirectory = resolve(packageRoot, "protocol/schemas");
|
||||
const outputPath = resolve(packageRoot, "src/protocol/generated/schema-bundle.ts");
|
||||
const validatorsOutputPath = resolve(
|
||||
packageRoot,
|
||||
"src/protocol/generated/standalone-validators.ts",
|
||||
);
|
||||
const schemaNames = [
|
||||
"identity",
|
||||
"capabilities",
|
||||
@@ -29,15 +36,45 @@ const declarations = schemas
|
||||
const bundle = schemaNames.map((name) => ` ${name}: ${name}Schema,`).join("\n");
|
||||
const generated = `// Generated by scripts/generate-protocol-schema-module.mjs. Do not edit.\n\n${declarations}\n\nexport const prpSchemaBundle = {\n${bundle}\n} as const;\n`;
|
||||
|
||||
const schemaByName = Object.fromEntries(schemas.map(({ name, value }) => [name, value]));
|
||||
const ajv = new Ajv2020({
|
||||
allErrors: true,
|
||||
strict: true,
|
||||
strictRequired: false,
|
||||
code: { esm: true, source: true },
|
||||
formats: {
|
||||
"date-time": /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/,
|
||||
},
|
||||
});
|
||||
for (const { value } of schemas) ajv.addSchema(value);
|
||||
const standaloneValidators = standaloneCode(ajv, {
|
||||
fixtureValidator: schemaByName.fixture.$id,
|
||||
eventValidator: schemaByName.event.$id,
|
||||
resultValidator: schemaByName.result.$id,
|
||||
});
|
||||
const ucs2RuntimePattern = /const (func\d+) = require\("ajv\/dist\/runtime\/ucs2length"\)\.default;/;
|
||||
if (!ucs2RuntimePattern.test(standaloneValidators)) {
|
||||
throw new Error("Ajv standalone output no longer has the expected ucs2length runtime binding");
|
||||
}
|
||||
const validators = standaloneValidators.replace(
|
||||
ucs2RuntimePattern,
|
||||
"const $1 = typeof ucs2LengthModule === \"function\" ? ucs2LengthModule : ucs2LengthModule.default;",
|
||||
);
|
||||
const generatedValidators = `// Generated by scripts/generate-protocol-schema-module.mjs. Do not edit.\n// @ts-nocheck -- Ajv standalone output is JavaScript compiled from the checked-in schemas.\n\nimport ucs2LengthModule from "ajv/dist/runtime/ucs2length.js";\n\n${validators}\n`;
|
||||
|
||||
if (process.argv.includes("--check")) {
|
||||
const current = await readFile(outputPath, "utf8").catch(() => "");
|
||||
if (current !== generated) {
|
||||
process.stderr.write("Generated PRP schema module is stale. Run pnpm generate:protocol-types.\n");
|
||||
const currentValidators = await readFile(validatorsOutputPath, "utf8").catch(() => "");
|
||||
if (current !== generated || currentValidators !== generatedValidators) {
|
||||
process.stderr.write("Generated PRP schema modules are stale. Run pnpm generate:protocol-types.\n");
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
process.stdout.write("Generated PRP schema module matches the JSON Schema sources.\n");
|
||||
process.stdout.write("Generated PRP schema modules match the JSON Schema sources.\n");
|
||||
}
|
||||
} else {
|
||||
await writeFile(outputPath, generated);
|
||||
process.stdout.write("Generated src/protocol/generated/schema-bundle.ts.\n");
|
||||
await Promise.all([
|
||||
writeFile(outputPath, generated),
|
||||
writeFile(validatorsOutputPath, generatedValidators),
|
||||
]);
|
||||
process.stdout.write("Generated PRP schema and standalone validator modules.\n");
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Generated by scripts/generate-protocol-schema-module.mjs. Do not edit.
|
||||
// @ts-nocheck -- Ajv standalone output is JavaScript compiled from the checked-in schemas.
|
||||
|
||||
import ucs2LengthModule from "ajv/dist/runtime/ucs2length.js";
|
||||
|
||||
"use strict";export const fixtureValidator = validate20;const schema31 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://paperclip.dev/schemas/prp/v1/fixture.schema.json","title":"PRP scripted replay fixture","type":"object","required":["schema","fixtureVersion","protocolVersion","name","description","identity","capabilities","commands","events","result"],"properties":{"schema":{"const":"paperclip.prp.fixture.v1"},"fixtureVersion":{"const":1},"protocolVersion":{"const":1},"name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":"string","minLength":1,"maxLength":1000},"identity":{"$ref":"https://paperclip.dev/schemas/prp/v1/identity.schema.json"},"capabilities":{"$ref":"https://paperclip.dev/schemas/prp/v1/capabilities.schema.json"},"commands":{"type":"array","items":{"$ref":"https://paperclip.dev/schemas/prp/v1/command.schema.json"}},"events":{"type":"array","minItems":1,"items":{"$ref":"https://paperclip.dev/schemas/prp/v1/event.schema.json"}},"requests":{"type":"array","items":{"$ref":"https://paperclip.dev/schemas/prp/v1/request.schema.json"}},"result":{"$ref":"https://paperclip.dev/schemas/prp/v1/result.schema.json"}},"additionalProperties":true};const schema41 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://paperclip.dev/schemas/prp/v1/capabilities.schema.json","title":"PRP negotiated capabilities","type":"object","required":["schema","sessionReusePolicy","driver","steer","interrupt","resume","runtimeRequests","structuredResult","typedEvents"],"properties":{"schema":{"const":"paperclip.prp.capabilities.v1"},"sessionReusePolicy":{"enum":["new_per_run","reuse_per_issue","reuse_per_workspace"]},"driver":{"type":"object","required":["kind","version"],"properties":{"kind":{"type":"string","minLength":1,"maxLength":80},"version":{"type":"string","minLength":1,"maxLength":80}},"additionalProperties":true},"steer":{"type":"boolean"},"interrupt":{"type":"boolean"},"resume":{"type":"boolean"},"runtimeRequests":{"type":"boolean"},"structuredResult":{"type":"boolean"},"typedEvents":{"type":"boolean"},"unsupported":{"type":"array","items":{"type":"string","minLength":1},"uniqueItems":true}},"additionalProperties":true};const schema42 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://paperclip.dev/schemas/prp/v1/command.schema.json","title":"PRP runner command","type":"object","required":["schema","commandId","controllerSeq","type","issuedAt","payload"],"properties":{"schema":{"const":"paperclip.prp.command.v1"},"commandId":{"type":"string","minLength":1,"maxLength":160},"controllerSeq":{"type":"integer","minimum":1},"type":{"enum":["run.prepare","session.open","turn.start","turn.steer","turn.interrupt","turn.stop","request.resolve","interaction.receipt","session.snapshot","session.close","run.cancel","runner.drain","runner.shutdown"]},"issuedAt":{"type":"string","format":"date-time"},"deadlineAt":{"type":"string","format":"date-time"},"precondition":{"type":"object","properties":{"runnerState":{"type":"array","items":{"type":"string"}},"runState":{"type":"array","items":{"type":"string"}},"sessionState":{"type":"array","items":{"type":"string"}},"activeTurnId":{"type":["string","null"]}},"additionalProperties":true},"payload":{"type":"object","additionalProperties":true}},"additionalProperties":true};const schema46 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://paperclip.dev/schemas/prp/v1/request.schema.json","title":"PRP request","oneOf":[{"type":"object","required":["schema","requestKind","requestId","type","status","prompt"],"properties":{"schema":{"const":"paperclip.runtime_request.v1"},"requestKind":{"const":"runtime"},"requestId":{"type":"string","minLength":1,"maxLength":160},"type":{"enum":["permission","input"]},"status":{"enum":["pending","resolved","expired","cancelled"]},"prompt":{"type":"string","minLength":1,"maxLength":4000},"choices":{"type":"array","items":{"type":"object","required":["key","label"],"properties":{"key":{"type":"string","minLength":1},"label":{"type":"string","minLength":1}},"additionalProperties":true}}},"additionalProperties":true},{"type":"object","required":["schema","requestKind","requestId","kind","blocking","payload"],"properties":{"schema":{"const":"paperclip.interaction_request.v1"},"requestKind":{"const":"issue_thread"},"requestId":{"type":"string","minLength":1,"maxLength":160},"kind":{"enum":["request_confirmation","request_checkbox_confirmation","request_item_verdicts","ask_user_questions","suggest_tasks"]},"blocking":{"type":"boolean"},"payload":{"type":"object","additionalProperties":true}},"additionalProperties":true}]};const schema45 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://paperclip.dev/schemas/prp/v1/result.schema.json","title":"PRP structured run result","type":"object","required":["schema","reportedWorkDisposition","summary","completionClaim","evidence","verification","attentionRequests","artifacts"],"pLine truncated
|
||||
@@ -1,21 +1,20 @@
|
||||
import {
|
||||
Ajv2020,
|
||||
type ErrorObject,
|
||||
type ValidateFunction,
|
||||
} from "ajv/dist/2020.js";
|
||||
import type { ErrorObject, ValidateFunction } from "ajv/dist/2020.js";
|
||||
import type { FromSchema } from "json-schema-to-ts";
|
||||
|
||||
import {
|
||||
capabilitiesSchema,
|
||||
commandSchema,
|
||||
eventSchema,
|
||||
fixtureSchema,
|
||||
identitySchema,
|
||||
prpSchemaBundle,
|
||||
requestSchema,
|
||||
resultSchema,
|
||||
terminalSchema,
|
||||
} from "./generated/schema-bundle.js";
|
||||
import {
|
||||
eventValidator as standaloneEventValidator,
|
||||
fixtureValidator as standaloneFixtureValidator,
|
||||
resultValidator as standaloneResultValidator,
|
||||
} from "./generated/standalone-validators.js";
|
||||
|
||||
export const PRP_PROTOCOL_NAME = "paperclip.runner";
|
||||
export const PRP_PROTOCOL_VERSION = 1;
|
||||
@@ -69,32 +68,12 @@ export interface ProtocolVersionRange {
|
||||
max: number;
|
||||
}
|
||||
|
||||
const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false });
|
||||
ajv.addFormat("date-time", {
|
||||
type: "string",
|
||||
validate(value: string) {
|
||||
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value);
|
||||
},
|
||||
});
|
||||
for (const schema of Object.values(prpSchemaBundle)) {
|
||||
ajv.addSchema(schema);
|
||||
}
|
||||
|
||||
const registeredFixtureValidator = ajv.getSchema(fixtureSchema.$id);
|
||||
if (registeredFixtureValidator === undefined) {
|
||||
throw new Error("PRP fixture validator was not registered");
|
||||
}
|
||||
const fixtureValidator = registeredFixtureValidator as ValidateFunction<PrpFixture>;
|
||||
const registeredEventValidator = ajv.getSchema(eventSchema.$id);
|
||||
if (registeredEventValidator === undefined) {
|
||||
throw new Error("PRP event validator was not registered");
|
||||
}
|
||||
const eventValidator = registeredEventValidator as ValidateFunction<PrpEvent>;
|
||||
const registeredResultValidator = ajv.getSchema(resultSchema.$id);
|
||||
if (registeredResultValidator === undefined) {
|
||||
throw new Error("PRP structured result validator was not registered");
|
||||
}
|
||||
const resultValidator = registeredResultValidator as ValidateFunction<PrpStructuredRunResult>;
|
||||
// The validators are generated from the same checked-in schemas as the types.
|
||||
// Keeping compilation out of the runtime lets strict CSP deployments retain
|
||||
// `script-src 'self'` without AJV attempting dynamic JavaScript evaluation.
|
||||
const fixtureValidator = standaloneFixtureValidator as ValidateFunction<PrpFixture>;
|
||||
const eventValidator = standaloneEventValidator as ValidateFunction<PrpEvent>;
|
||||
const resultValidator = standaloneResultValidator as ValidateFunction<PrpStructuredRunResult>;
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
|
||||
Reference in new issue
Block a user