import * as Effect from "effect/Effect"; import { z } from "zod"; import type { Artifact, CatalogEntry } from "./artifact-types.ts"; import { ArtifactUnavailable } from "./deployment-errors.ts"; import { releaseIdentity, sameRelease, type ReleaseIdentity, } from "./installation-metadata.ts"; export const compatibilitySchema = z.strictObject({ applicationSchema: z.literal(1), agentIdentity: z.literal("personal/PersonalAgent/Conversation/Sandbox"), fromArtifacts: z.array(z.string().regex(/^[a-f0-9]{64}$/)).max(32), }); export function verifyUpgradeIdentity(from: ReleaseIdentity, to: Artifact) { return Effect.gen(function* () { if ( sameRelease(from, to.identity) || !to.compatibility.fromArtifacts.includes(from.artifactDigest) ) return yield* new ArtifactUnavailable(); }); } const fileSchema = z.strictObject({ path: z .string() .regex(/^(?:deployment\.json|(?:worker|assets)\/[A-Za-z0-9_./-]+)$/), size: z .number() .int() .nonnegative() .max(25 * 1024 * 1024), sha256: z.string().regex(/^[a-f0-9]{64}$/), mime: z.string().regex(/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/), assetHash: z .string() .regex(/^[a-f0-9]{32}$/) .optional(), }); const deploymentSchema = z.strictObject({ main: z.literal("./worker/index.js"), no_bundle: z.literal(true), compatibility_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), // Retained v0.1.0-dev.1 archives predate public Worker-to-Worker fetch. compatibility_flags: z.union([ z.tuple([z.literal("nodejs_compat")]), z.tuple([ z.literal("nodejs_compat"), z.literal("global_fetch_strictly_public"), ]), ]), assets: z.strictObject({ directory: z.literal("./assets"), binding: z.literal("ASSETS"), }), durable_objects: z.strictObject({ bindings: z .array( z.strictObject({ name: z.enum(["PersonalAgent", "Sandbox"]), class_name: z.enum(["PersonalAgent", "Sandbox"]), }), ) .length(2), }), exports: z.strictObject({ PersonalAgent: z.strictObject({ type: z.literal("durable-object"), storage: z.literal("sqlite"), }), Sandbox: z.strictObject({ type: z.literal("durable-object"), storage: z.literal("sqlite"), }), }), containers: z .array( z.strictObject({ name: z.literal("flarebot-shell"), class_name: z.literal("Sandbox"), image: z .string() .regex( /^docker\.io\/cloudflare\/sandbox:0\.12\.9@sha256:[a-f0-9]{64}$/, ), instance_type: z.literal("lite"), max_instances: z.literal(4), }), ) .length(1), ai: z.strictObject({ binding: z.literal("AI") }), browser: z.strictObject({ binding: z.literal("BROWSER") }), worker_loaders: z .array(z.strictObject({ binding: z.literal("LOADER") })) .length(1), observability: z.strictObject({ enabled: z.literal(false) }), keep_vars: z.literal(true), }); export function digest(bytes: ArrayBuffer) { return Effect.gen(function* () { return [ ...new Uint8Array( yield* Effect.promise(() => crypto.subtle.digest("SHA-256", bytes)), ), ] .map((b) => b.toString(16).padStart(2, "0")) .join(""); }); } const json = (bytes: ArrayBuffer) => JSON.parse(new TextDecoder().decode(bytes)); // Byte verification precedes every external effect. Data modules retain the // original buffers; verification does not accumulate another copy of the release. export function loadArtifact( entry: CatalogEntry, pinned?: ReleaseIdentity, development = false, ) { return Effect.gen(function* () { const identity = releaseIdentity.parse(entry.identity); if ( (entry.development && !development) || (pinned && !sameRelease(identity, pinned)) ) return yield* new ArtifactUnavailable(); const manifestBytes = entry.files["manifest.json"]; if ( !manifestBytes || (yield* digest(manifestBytes)) !== identity.artifactDigest ) return yield* new ArtifactUnavailable(); const manifest = json(manifestBytes); if ( manifest.schemaVersion !== 1 || manifest.kind !== "flarebot-customer-runtime" || manifest.configurationVersion !== 1 || manifest.release !== identity.version || manifest.sourceRevision !== identity.sourceRevision || (manifest.sourceDirty && !development) || typeof manifest.sourceDirty !== "boolean" || manifest.deployment !== "deployment.json" ) return yield* new ArtifactUnavailable(); const files = z.array(fileSchema).min(3).max(256).parse(manifest.files); const paths = new Set(); let total = 0; for (const file of files) { if ( paths.has(file.path) || file.path.split("/").some((p) => !p || p === "." || p === "..") || file.path.endsWith(".map") ) return yield* new ArtifactUnavailable(); paths.add(file.path); total += file.size; const bytes = entry.files[file.path]; if ( !bytes || bytes.byteLength !== file.size || (yield* digest(bytes)) !== file.sha256 || file.path.startsWith("assets/") !== !!file.assetHash ) return yield* new ArtifactUnavailable(); if ( file.path.startsWith("worker/") && (!file.path.endsWith(".js") || file.mime !== "application/javascript") ) return yield* new ArtifactUnavailable(); } if ( files .filter((f) => f.path.startsWith("worker/")) .reduce((sum, f) => sum + f.size, 0) > 16 * 1024 * 1024 || total > 24 * 1024 * 1024 || Object.keys(entry.files).length !== paths.size + 1 || !paths.has("worker/index.js") || !paths.has("deployment.json") ) return yield* new ArtifactUnavailable(); const deployment = deploymentSchema.parse( json(entry.files["deployment.json"]), ); const bindings = deployment.durable_objects.bindings; if ( !bindings.some( (b) => b.name === "PersonalAgent" && b.class_name === "PersonalAgent", ) || !bindings.some((b) => b.name === "Sandbox" && b.class_name === "Sandbox") ) return yield* new ArtifactUnavailable(); if (deployment.containers[0].image !== manifest.shell?.image) return yield* new ArtifactUnavailable(); return { identity, files, bytes: entry.files, deployment, compatibility: compatibilitySchema.parse(manifest.compatibility), }; }).pipe(Effect.catchDefect(() => new ArtifactUnavailable())); }