import { createHash } from 'node:crypto' import { inflateRawSync } from 'node:zlib' const encoder = new TextEncoder() const utf8 = new TextDecoder('utf-8', { fatal: true }) const sha256Pattern = /^[a-f0-9]{64}$/ const semverPattern = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/ const safePathPattern = /^[A-Za-z0-9._/-]+$/ const windowsReservedName = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i export const defaultRunPackLimits = Object.freeze({ maxArchiveBytes: 10 * 1024 * 1024, maxExpandedBytes: 50 * 1024 * 1024, maxFiles: 500, maxFileBytes: 5 * 1024 * 1024, maxManifestFiles: 100, }) export interface RunPackLimits { readonly maxArchiveBytes: number readonly maxExpandedBytes: number readonly maxFiles: number readonly maxFileBytes: number readonly maxManifestFiles: number } export interface RunPackRunMetadata { readonly id: string readonly playbookId: string readonly playbookVersion: string readonly playbookDigest: string readonly repositoryProfileDigest?: string | null readonly renderDigest: string readonly generatedAt: string readonly platformVersion: string } export interface RunPackManifestFile { readonly path: string readonly mediaType: string readonly sizeBytes: number readonly sha256: string } export interface RunPackManifest { readonly apiVersion: 'devrunbook.io/v1alpha1' readonly kind: 'RunPackManifest' readonly run: RunPackRunMetadata readonly files: readonly RunPackManifestFile[] readonly manifestDigest: string } export interface RunPackAdditionalFile { readonly path: string readonly mediaType: string readonly content: string | Uint8Array } export interface CreateRunPackInput { readonly slug: string readonly run: RunPackRunMetadata readonly renderedPrompt: string readonly runbook?: string readonly repositoryContext?: string readonly validation?: string readonly handoffTemplate?: string readonly additionalFiles?: readonly RunPackAdditionalFile[] readonly limits?: Partial } export interface CreatedRunPack { readonly filename: string readonly rootDirectory: string readonly bytes: Uint8Array readonly sha256: string readonly manifest: RunPackManifest readonly taskMarkdown: string } export interface VerifiedRunPack { readonly rootDirectory: string readonly manifest: RunPackManifest readonly files: ReadonlyMap readonly archiveSha256: string } export type RunPackErrorCode = | 'run_pack_input_invalid' | 'run_pack_archive_limit' | 'run_pack_path_unsafe' | 'run_pack_archive_invalid' | 'run_pack_manifest_invalid' | 'run_pack_inventory_mismatch' | 'run_pack_file_integrity_failed' | 'run_pack_manifest_digest_failed' export class RunPackError extends Error { constructor( readonly code: RunPackErrorCode, readonly path: string, message: string, ) { super(`${path}: ${message}`) this.name = 'RunPackError' } } type JsonValue = | null | boolean | number | string | JsonValue[] | { readonly [key: string]: JsonValue } function canonicalJson(value: JsonValue): string { if (value === null || typeof value !== 'object') return JSON.stringify(value) if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]` return `{${Object.keys(value) .sort() .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key]!)}`) .join(',')}}` } function digest(content: Uint8Array | string): string { return createHash('sha256').update(content).digest('hex') } function normalizeText(value: string): string { const normalized = value .replace(/^\uFEFF/u, '') .normalize('NFC') .replace(/\r\n?/g, '\n') .split('\n') .map((line) => line.replace(/[\t ]+$/u, '')) .join('\n') .replace(/\n*$/u, '') return `${normalized}\n` } function mergeLimits(overrides?: Partial): RunPackLimits { const maxFiles = overrides?.maxFiles ?? defaultRunPackLimits.maxFiles const limits = { ...defaultRunPackLimits, ...overrides, maxFiles, maxManifestFiles: overrides?.maxManifestFiles ?? Math.min(defaultRunPackLimits.maxManifestFiles, maxFiles - 1), } for (const [name, value] of Object.entries(limits)) { if (!Number.isSafeInteger(value) || value <= 0) { throw new RunPackError( 'run_pack_input_invalid', `limits.${name}`, 'must be a positive safe integer', ) } } if (limits.maxManifestFiles > limits.maxFiles - 1) { throw new RunPackError( 'run_pack_input_invalid', 'limits.maxManifestFiles', 'must leave room for manifest.json within maxFiles', ) } return limits } function assertSafePath(value: string, label: string): void { if ( value.length === 0 || value.length > 500 || !safePathPattern.test(value) || value.startsWith('/') || value.endsWith('/') || value.includes('//') || value.includes('\\') || value.includes('\0') ) { throw new RunPackError( 'run_pack_path_unsafe', label, 'must be a normalized relative ASCII file path', ) } for (const segment of value.split('/')) { if ( segment === '.' || segment === '..' || segment.endsWith('.') || segment.endsWith(' ') || windowsReservedName.test(segment) ) { throw new RunPackError( 'run_pack_path_unsafe', label, `contains unsafe path segment ${JSON.stringify(segment)}`, ) } } } function sanitizeNamePart(value: string, fallback: string): string { const result = value .normalize('NFKD') .replace(/[^A-Za-z0-9-]+/g, '-') .replace(/^-+|-+$/g, '') .replace(/-+/g, '-') .slice(0, 80) const safe = result.length > 0 ? result : fallback return windowsReservedName.test(safe) ? `run-${safe}` : safe } function assertDigest(value: unknown, label: string): asserts value is string { if (typeof value !== 'string' || !sha256Pattern.test(value)) { throw new RunPackError( 'run_pack_manifest_invalid', label, 'must be a lowercase SHA-256 digest', ) } } function assertRunMetadata(run: RunPackRunMetadata): void { if ( typeof run.id !== 'string' || run.id.length < 8 || run.id.length > 100 || Array.from(run.id).some((character) => character.charCodeAt(0) < 0x20) ) { throw new RunPackError('run_pack_input_invalid', 'run.id', 'is invalid') } if ( typeof run.playbookId !== 'string' || run.playbookId.length < 3 || run.playbookId.length > 120 ) { throw new RunPackError( 'run_pack_input_invalid', 'run.playbookId', 'is invalid', ) } if ( typeof run.playbookVersion !== 'string' || !semverPattern.test(run.playbookVersion) ) { throw new RunPackError( 'run_pack_input_invalid', 'run.playbookVersion', 'must be a semantic version', ) } assertDigest(run.playbookDigest, 'run.playbookDigest') assertDigest(run.renderDigest, 'run.renderDigest') if ( run.repositoryProfileDigest !== undefined && run.repositoryProfileDigest !== null ) { assertDigest(run.repositoryProfileDigest, 'run.repositoryProfileDigest') } if ( typeof run.generatedAt !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/u.test( run.generatedAt, ) || !Number.isFinite(Date.parse(run.generatedAt)) ) { throw new RunPackError( 'run_pack_input_invalid', 'run.generatedAt', 'must be an RFC 3339 UTC timestamp', ) } if ( typeof run.platformVersion !== 'string' || run.platformVersion.length < 1 || run.platformVersion.length > 80 ) { throw new RunPackError( 'run_pack_input_invalid', 'run.platformVersion', 'is invalid', ) } } export function createTaskMarkdown( run: RunPackRunMetadata, renderedPrompt: string, ): string { assertRunMetadata(run) const prompt = normalizeText(renderedPrompt) if (digest(prompt) !== run.renderDigest) { throw new RunPackError( 'run_pack_input_invalid', 'renderedPrompt', 'normalized prompt bytes do not match run.renderDigest', ) } const metadata = JSON.stringify(taskMetadata(run), null, 2) return normalizeText( `\n\n## Run metadata\n\n\`\`\`json\n${metadata}\n\`\`\`\n\n${prompt}`, ) } function taskMetadata( run: RunPackRunMetadata, ): Readonly> { return { generatedAt: run.generatedAt, platformVersion: run.platformVersion, playbookDigest: run.playbookDigest, playbookId: run.playbookId, playbookVersion: run.playbookVersion, renderDigest: run.renderDigest, repositoryProfileDigest: run.repositoryProfileDigest ?? null, runId: run.id, } } function defaultRunbook(run: RunPackRunMetadata): string { return normalizeText( `# DevRunbook Run Pack\n\nUse \`TASK.md\` as the authoritative generated task. ` + `Consult \`REPOSITORY_CONTEXT.md\`, \`VALIDATION.md\`, and ` + `\`HANDOFF_TEMPLATE.md\` only for their named purposes.\n\n` + `Run ID: \`${run.id.replace(/`/g, '')}\`\n`, ) } const defaultValidation = normalizeText( '# Validation\n\nFollow the validation contract in `TASK.md`. Record the exact commands run, their outcomes, and any skipped checks with reasons.\n', ) const defaultHandoff = normalizeText( '# Handoff\n\n## Outcome\n\n## Changed files\n\n## Validation evidence\n\n## Risks and limitations\n\n## Unresolved items\n\n## Recommended follow-up\n', ) interface PayloadFile { readonly path: string readonly mediaType: string readonly bytes: Uint8Array } function bytesFor(content: string | Uint8Array): Uint8Array { return typeof content === 'string' ? encoder.encode(normalizeText(content)) : content.slice() } function compareUtf8(left: string, right: string): number { return Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8')) } function addPayloadFile( files: PayloadFile[], seen: Set, file: RunPackAdditionalFile, limits: RunPackLimits, ): void { assertSafePath(file.path, `files.${file.path}`) if (file.path.toLowerCase() === 'manifest.json') { throw new RunPackError( 'run_pack_input_invalid', `files.${file.path}`, 'manifest.json is generated by the platform', ) } const collisionKey = file.path.toLowerCase() if (seen.has(collisionKey)) { throw new RunPackError( 'run_pack_input_invalid', `files.${file.path}`, 'duplicates another file on a case-insensitive filesystem', ) } if ( file.mediaType.length < 3 || file.mediaType.length > 120 || /[\r\n]/u.test(file.mediaType) ) { throw new RunPackError( 'run_pack_input_invalid', `files.${file.path}.mediaType`, 'is invalid', ) } const bytes = bytesFor(file.content) if (bytes.byteLength > limits.maxFileBytes) { throw new RunPackError( 'run_pack_archive_limit', `files.${file.path}`, `exceeds the ${limits.maxFileBytes} byte single-file limit`, ) } seen.add(collisionKey) files.push({ path: file.path, mediaType: file.mediaType, bytes }) } function manifestWithoutDigest( run: RunPackRunMetadata, files: readonly RunPackManifestFile[], ): Omit { return { apiVersion: 'devrunbook.io/v1alpha1', kind: 'RunPackManifest', run: { id: run.id, playbookId: run.playbookId, playbookVersion: run.playbookVersion, playbookDigest: run.playbookDigest, repositoryProfileDigest: run.repositoryProfileDigest ?? null, renderDigest: run.renderDigest, generatedAt: run.generatedAt, platformVersion: run.platformVersion, }, files, } } export function createRunPack(input: CreateRunPackInput): CreatedRunPack { const limits = mergeLimits(input.limits) assertRunMetadata(input.run) const taskMarkdown = createTaskMarkdown(input.run, input.renderedPrompt) const files: PayloadFile[] = [] const seen = new Set() const standard: RunPackAdditionalFile[] = [ { path: 'RUNBOOK.md', mediaType: 'text/markdown; charset=utf-8', content: input.runbook ?? defaultRunbook(input.run), }, { path: 'TASK.md', mediaType: 'text/markdown; charset=utf-8', content: taskMarkdown, }, ...(input.repositoryContext === undefined ? [] : [ { path: 'REPOSITORY_CONTEXT.md', mediaType: 'text/markdown; charset=utf-8', content: input.repositoryContext, }, ]), { path: 'VALIDATION.md', mediaType: 'text/markdown; charset=utf-8', content: input.validation ?? defaultValidation, }, { path: 'HANDOFF_TEMPLATE.md', mediaType: 'text/markdown; charset=utf-8', content: input.handoffTemplate ?? defaultHandoff, }, ] for (const file of [...standard, ...(input.additionalFiles ?? [])]) { addPayloadFile(files, seen, file, limits) } if (files.length > limits.maxManifestFiles) { throw new RunPackError( 'run_pack_archive_limit', 'files', `exceeds the ${limits.maxManifestFiles} manifest-file limit`, ) } const expandedBytes = files.reduce( (total, file) => total + file.bytes.byteLength, 0, ) if (expandedBytes > limits.maxExpandedBytes) { throw new RunPackError( 'run_pack_archive_limit', 'files', `exceeds the ${limits.maxExpandedBytes} byte expanded limit`, ) } files.sort((left, right) => compareUtf8(left.path, right.path)) const manifestFiles: RunPackManifestFile[] = files.map((file) => ({ path: file.path, mediaType: file.mediaType, sizeBytes: file.bytes.byteLength, sha256: digest(file.bytes), })) const unsigned = manifestWithoutDigest(input.run, manifestFiles) const manifest: RunPackManifest = { ...unsigned, manifestDigest: digest(canonicalJson(unsigned as unknown as JsonValue)), } const manifestBytes = encoder.encode(`${JSON.stringify(manifest, null, 2)}\n`) if (manifestBytes.byteLength > limits.maxFileBytes) { throw new RunPackError( 'run_pack_archive_limit', 'manifest.json', 'exceeds the single-file limit', ) } if (expandedBytes + manifestBytes.byteLength > limits.maxExpandedBytes) { throw new RunPackError( 'run_pack_archive_limit', 'archive', `exceeds the ${limits.maxExpandedBytes} byte expanded limit`, ) } const rootDirectory = `DevRunbook-${sanitizeNamePart(input.slug, 'run')}-${sanitizeNamePart(input.run.id.slice(0, 12), 'task')}` assertSafePath(`${rootDirectory}/TASK.md`, 'rootDirectory') const entries = [ ...files.map((file) => ({ path: `${rootDirectory}/${file.path}`, bytes: file.bytes, })), { path: `${rootDirectory}/manifest.json`, bytes: manifestBytes }, ].sort((left, right) => compareUtf8(left.path, right.path)) const archive = encodeDeterministicZip(entries) if (archive.byteLength > limits.maxArchiveBytes) { throw new RunPackError( 'run_pack_archive_limit', 'archive', `exceeds the ${limits.maxArchiveBytes} byte compressed limit`, ) } return Object.freeze({ filename: `${rootDirectory}.zip`, rootDirectory, bytes: archive, sha256: digest(archive), manifest: Object.freeze(manifest), taskMarkdown, }) } const crcTable = Uint32Array.from({ length: 256 }, (_, index) => { let value = index for (let bit = 0; bit < 8; bit += 1) { value = (value & 1) === 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1 } return value >>> 0 }) function crc32(bytes: Uint8Array): number { let crc = 0xffffffff for (const byte of bytes) crc = crcTable[(crc ^ byte) & 0xff]! ^ (crc >>> 8) return (crc ^ 0xffffffff) >>> 0 } function localHeader(name: Buffer, bytes: Uint8Array): Buffer { const header = Buffer.alloc(30) header.writeUInt32LE(0x04034b50, 0) header.writeUInt16LE(20, 4) header.writeUInt16LE(0x0800, 6) header.writeUInt16LE(0, 8) header.writeUInt16LE(0, 10) header.writeUInt16LE(0x0021, 12) header.writeUInt32LE(crc32(bytes), 14) header.writeUInt32LE(bytes.byteLength, 18) header.writeUInt32LE(bytes.byteLength, 22) header.writeUInt16LE(name.byteLength, 26) header.writeUInt16LE(0, 28) return header } function centralHeader( name: Buffer, bytes: Uint8Array, offset: number, ): Buffer { const header = Buffer.alloc(46) header.writeUInt32LE(0x02014b50, 0) header.writeUInt16LE(0x0314, 4) header.writeUInt16LE(20, 6) header.writeUInt16LE(0x0800, 8) header.writeUInt16LE(0, 10) header.writeUInt16LE(0, 12) header.writeUInt16LE(0x0021, 14) header.writeUInt32LE(crc32(bytes), 16) header.writeUInt32LE(bytes.byteLength, 20) header.writeUInt32LE(bytes.byteLength, 24) header.writeUInt16LE(name.byteLength, 28) header.writeUInt16LE(0, 30) header.writeUInt16LE(0, 32) header.writeUInt16LE(0, 34) header.writeUInt16LE(0, 36) header.writeUInt32LE((0o100644 * 65_536) >>> 0, 38) header.writeUInt32LE(offset, 42) return header } export function encodeDeterministicZip( entries: readonly { readonly path: string; readonly bytes: Uint8Array }[], ): Uint8Array { const localParts: Buffer[] = [] const centralParts: Buffer[] = [] let offset = 0 for (const entry of entries) { const name = Buffer.from(entry.path, 'utf8') const local = localHeader(name, entry.bytes) localParts.push(local, name, Buffer.from(entry.bytes)) centralParts.push(centralHeader(name, entry.bytes, offset), name) offset += local.byteLength + name.byteLength + entry.bytes.byteLength } const centralSize = centralParts.reduce( (total, part) => total + part.byteLength, 0, ) const end = Buffer.alloc(22) end.writeUInt32LE(0x06054b50, 0) end.writeUInt16LE(0, 4) end.writeUInt16LE(0, 6) end.writeUInt16LE(entries.length, 8) end.writeUInt16LE(entries.length, 10) end.writeUInt32LE(centralSize, 12) end.writeUInt32LE(offset, 16) end.writeUInt16LE(0, 20) return new Uint8Array(Buffer.concat([...localParts, ...centralParts, end])) } export interface BoundedZipEntry { readonly path: string readonly bytes: Uint8Array } function archiveFailure(path: string, message: string): never { throw new RunPackError('run_pack_archive_invalid', path, message) } export function readBoundedZip( bytes: Uint8Array, limits: Pick< RunPackLimits, 'maxArchiveBytes' | 'maxExpandedBytes' | 'maxFiles' | 'maxFileBytes' >, ): BoundedZipEntry[] { if (bytes.byteLength > limits.maxArchiveBytes) { throw new RunPackError( 'run_pack_archive_limit', 'archive', 'compressed archive limit exceeded', ) } if (bytes.byteLength < 22) archiveFailure('archive', 'ZIP end record is missing') const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength) const endOffset = buffer.byteLength - 22 if ( buffer.readUInt32LE(endOffset) !== 0x06054b50 || buffer.readUInt16LE(endOffset + 20) !== 0 ) { archiveFailure( 'archive', 'ZIP comments, trailing data, and missing end records are rejected', ) } const disk = buffer.readUInt16LE(endOffset + 4) const centralDisk = buffer.readUInt16LE(endOffset + 6) const diskEntries = buffer.readUInt16LE(endOffset + 8) const entryCount = buffer.readUInt16LE(endOffset + 10) const centralSize = buffer.readUInt32LE(endOffset + 12) const centralOffset = buffer.readUInt32LE(endOffset + 16) if (disk !== 0 || centralDisk !== 0 || diskEntries !== entryCount) { archiveFailure('archive', 'multi-disk ZIP archives are rejected') } if (entryCount === 0 || entryCount > limits.maxFiles) { throw new RunPackError( 'run_pack_archive_limit', 'archive.files', 'file-count limit exceeded', ) } if (centralOffset + centralSize !== endOffset) archiveFailure('archive', 'central directory bounds are invalid') const entries: BoundedZipEntry[] = [] const seen = new Set() const offsets = new Set() const occupiedLocalRegions: { readonly start: number readonly end: number readonly path: string }[] = [] let expandedTotal = 0 let cursor = centralOffset for (let index = 0; index < entryCount; index += 1) { if (cursor + 46 > endOffset || buffer.readUInt32LE(cursor) !== 0x02014b50) { archiveFailure( `archive.entries[${index}]`, 'central-directory entry is invalid', ) } const madeBy = buffer.readUInt16LE(cursor + 4) const flags = buffer.readUInt16LE(cursor + 8) const method = buffer.readUInt16LE(cursor + 10) const declaredCrc = buffer.readUInt32LE(cursor + 16) const compressedSize = buffer.readUInt32LE(cursor + 20) const expandedSize = buffer.readUInt32LE(cursor + 24) const nameLength = buffer.readUInt16LE(cursor + 28) const extraLength = buffer.readUInt16LE(cursor + 30) const commentLength = buffer.readUInt16LE(cursor + 32) const diskStart = buffer.readUInt16LE(cursor + 34) const externalAttributes = buffer.readUInt32LE(cursor + 38) const localOffset = buffer.readUInt32LE(cursor + 42) const next = cursor + 46 + nameLength + extraLength + commentLength if (next > endOffset) archiveFailure( `archive.entries[${index}]`, 'entry metadata exceeds archive bounds', ) let entryPath: string try { entryPath = utf8.decode( buffer.subarray(cursor + 46, cursor + 46 + nameLength), ) } catch { archiveFailure( `archive.entries[${index}].path`, 'path is not valid UTF-8', ) } assertSafePath(entryPath!, `archive.entries[${index}].path`) const collisionKey = entryPath!.toLowerCase() if (seen.has(collisionKey)) archiveFailure(entryPath!, 'duplicate or case-colliding path') seen.add(collisionKey) if (diskStart !== 0 || (flags & 0x0001) !== 0 || (flags & 0x0008) !== 0) { archiveFailure( entryPath!, 'encrypted, split, or data-descriptor entries are rejected', ) } if (method !== 0 && method !== 8) archiveFailure(entryPath!, 'unsupported compression method') const allowedFlags = 0x0800 | (method === 8 ? 0x0006 : 0) if ((flags & 0x0800) === 0 || (flags & ~allowedFlags) !== 0) { archiveFailure( entryPath!, 'unsupported or ambiguous ZIP flags are rejected', ) } const origin = madeBy >>> 8 const unixMode = externalAttributes >>> 16 if ( (externalAttributes & 0x10) !== 0 || (origin === 3 && (unixMode & 0o170000) !== 0o100000) ) { archiveFailure( entryPath!, 'directory, symlink, device, and non-regular entries are rejected', ) } if (expandedSize > limits.maxFileBytes) { throw new RunPackError( 'run_pack_archive_limit', entryPath!, 'single-file expanded limit exceeded', ) } expandedTotal += expandedSize if (expandedTotal > limits.maxExpandedBytes) { throw new RunPackError( 'run_pack_archive_limit', 'archive', 'expanded archive limit exceeded', ) } if (offsets.has(localOffset)) archiveFailure(entryPath!, 'multiple entries reference one local header') offsets.add(localOffset) if ( localOffset + 30 > centralOffset || buffer.readUInt32LE(localOffset) !== 0x04034b50 ) { archiveFailure(entryPath!, 'local header is invalid') } const localFlags = buffer.readUInt16LE(localOffset + 6) const localMethod = buffer.readUInt16LE(localOffset + 8) const localCrc = buffer.readUInt32LE(localOffset + 14) const localCompressedSize = buffer.readUInt32LE(localOffset + 18) const localExpandedSize = buffer.readUInt32LE(localOffset + 22) const localNameLength = buffer.readUInt16LE(localOffset + 26) const localExtraLength = buffer.readUInt16LE(localOffset + 28) const dataOffset = localOffset + 30 + localNameLength + localExtraLength const dataEnd = dataOffset + compressedSize const overlap = occupiedLocalRegions.find( (region) => localOffset < region.end && dataEnd > region.start, ) if (overlap) { archiveFailure(entryPath!, `local data overlaps ${overlap.path}`) } occupiedLocalRegions.push({ start: localOffset, end: dataEnd, path: entryPath!, }) if ( dataEnd > centralOffset || localFlags !== flags || localMethod !== method || localCrc !== declaredCrc || localCompressedSize !== compressedSize || localExpandedSize !== expandedSize || !buffer .subarray(localOffset + 30, localOffset + 30 + localNameLength) .equals(buffer.subarray(cursor + 46, cursor + 46 + nameLength)) ) { archiveFailure(entryPath!, 'local and central metadata do not match') } const compressed = buffer.subarray(dataOffset, dataEnd) let content: Buffer try { content = method === 0 ? Buffer.from(compressed) : inflateRawSync(compressed, { maxOutputLength: limits.maxFileBytes }) } catch { archiveFailure( entryPath!, 'compressed payload is invalid or exceeds limits', ) } if (content.byteLength !== expandedSize || crc32(content) !== declaredCrc) { archiveFailure(entryPath!, 'expanded size or CRC does not match') } entries.push({ path: entryPath!, bytes: new Uint8Array(content) }) cursor = next } if (cursor !== endOffset) archiveFailure('archive', 'central directory contains undeclared bytes') return entries } function isRecord(value: unknown): value is Record { return value !== null && typeof value === 'object' && !Array.isArray(value) } function assertNoDuplicateJsonKeys(source: string): void { let cursor = 0 const fail = (): never => { throw new RunPackError( 'run_pack_manifest_invalid', 'manifest.json', 'contains duplicate keys or invalid JSON structure', ) } const whitespace = (): void => { while (/\s/u.test(source[cursor] ?? '')) cursor += 1 } const stringToken = (): string => { if (source[cursor] !== '"') fail() const start = cursor cursor += 1 while (cursor < source.length) { const character = source[cursor++]! if (character === '"') { try { return JSON.parse(source.slice(start, cursor)) as string } catch { fail() } } if (character === '\\') cursor += 1 else if (character < ' ') fail() } return fail() } const value = (): void => { whitespace() const character = source[cursor] if (character === '{') { cursor += 1 whitespace() const keys = new Set() if (source[cursor] === '}') { cursor += 1 return } while (true) { whitespace() const key = stringToken() if (keys.has(key)) fail() keys.add(key) whitespace() if (source[cursor++] !== ':') fail() value() whitespace() const separator = source[cursor++] if (separator === '}') return if (separator !== ',') fail() } } if (character === '[') { cursor += 1 whitespace() if (source[cursor] === ']') { cursor += 1 return } while (true) { value() whitespace() const separator = source[cursor++] if (separator === ']') return if (separator !== ',') fail() } } if (character === '"') { stringToken() return } const match = source .slice(cursor) .match( /^(?:true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)/u, ) if (match === null) return fail() cursor += match[0].length } value() whitespace() if (cursor !== source.length) fail() } function assertExactKeys( value: Record, keys: readonly string[], label: string, ): void { const actual = Object.keys(value).sort() const expected = [...keys].sort() if ( actual.length !== expected.length || actual.some((key, index) => key !== expected[index]) ) { throw new RunPackError( 'run_pack_manifest_invalid', label, 'contains missing or unknown properties', ) } } function parseManifest( bytes: Uint8Array, limits: RunPackLimits, ): RunPackManifest { let value: unknown try { const source = utf8.decode(bytes) assertNoDuplicateJsonKeys(source) value = JSON.parse(source) } catch { throw new RunPackError( 'run_pack_manifest_invalid', 'manifest.json', 'must be valid UTF-8 JSON', ) } if (!isRecord(value)) throw new RunPackError( 'run_pack_manifest_invalid', 'manifest.json', 'must be an object', ) assertExactKeys( value, ['apiVersion', 'kind', 'run', 'files', 'manifestDigest'], 'manifest.json', ) if ( value.apiVersion !== 'devrunbook.io/v1alpha1' || value.kind !== 'RunPackManifest' ) { throw new RunPackError( 'run_pack_manifest_invalid', 'manifest.json', 'has an unsupported identity', ) } if (!isRecord(value.run)) throw new RunPackError( 'run_pack_manifest_invalid', 'manifest.json.run', 'must be an object', ) const runKeys = [ 'id', 'playbookId', 'playbookVersion', 'playbookDigest', 'renderDigest', 'generatedAt', 'platformVersion', ] if ('repositoryProfileDigest' in value.run) runKeys.push('repositoryProfileDigest') assertExactKeys(value.run, runKeys, 'manifest.json.run') const run = value.run as unknown as RunPackRunMetadata try { assertRunMetadata(run) } catch (error) { if (error instanceof RunPackError) { throw new RunPackError( 'run_pack_manifest_invalid', `manifest.json.${error.path}`, error.message.slice(error.message.indexOf(':') + 2), ) } throw error } if ( !Array.isArray(value.files) || value.files.length < 1 || value.files.length > limits.maxManifestFiles ) { throw new RunPackError( 'run_pack_manifest_invalid', 'manifest.json.files', 'has an invalid file count', ) } const files: RunPackManifestFile[] = [] const seen = new Set() for (const [index, candidate] of value.files.entries()) { const label = `manifest.json.files[${index}]` if (!isRecord(candidate)) throw new RunPackError( 'run_pack_manifest_invalid', label, 'must be an object', ) assertExactKeys( candidate, ['path', 'mediaType', 'sizeBytes', 'sha256'], label, ) if (typeof candidate.path !== 'string') throw new RunPackError( 'run_pack_manifest_invalid', `${label}.path`, 'must be a string', ) assertSafePath(candidate.path, `${label}.path`) if (candidate.path.toLowerCase() === 'manifest.json') { throw new RunPackError( 'run_pack_manifest_invalid', `${label}.path`, 'must not declare manifest.json', ) } const collisionKey = candidate.path.toLowerCase() if (seen.has(collisionKey)) throw new RunPackError( 'run_pack_manifest_invalid', `${label}.path`, 'duplicates another path', ) seen.add(collisionKey) if ( typeof candidate.mediaType !== 'string' || candidate.mediaType.length < 3 || candidate.mediaType.length > 120 || /[\r\n]/u.test(candidate.mediaType) ) { throw new RunPackError( 'run_pack_manifest_invalid', `${label}.mediaType`, 'is invalid', ) } if ( !Number.isSafeInteger(candidate.sizeBytes) || (candidate.sizeBytes as number) < 0 || (candidate.sizeBytes as number) > limits.maxFileBytes ) { throw new RunPackError( 'run_pack_manifest_invalid', `${label}.sizeBytes`, 'is invalid', ) } assertDigest(candidate.sha256, `${label}.sha256`) files.push(candidate as unknown as RunPackManifestFile) } const sorted = [...files].sort((left, right) => compareUtf8(left.path, right.path), ) if (files.some((file, index) => file.path !== sorted[index]!.path)) { throw new RunPackError( 'run_pack_manifest_invalid', 'manifest.json.files', 'must be sorted by UTF-8 path bytes', ) } assertDigest(value.manifestDigest, 'manifest.json.manifestDigest') return { apiVersion: 'devrunbook.io/v1alpha1', kind: 'RunPackManifest', run, files, manifestDigest: value.manifestDigest, } } export function verifyRunPack( archive: Uint8Array, limitOverrides?: Partial, ): VerifiedRunPack { const limits = mergeLimits(limitOverrides) const entries = readBoundedZip(archive, limits) const firstSlash = entries[0]!.path.indexOf('/') if (firstSlash <= 0) archiveFailure( entries[0]!.path, 'all Run Pack files must be below one root directory', ) const rootDirectory = entries[0]!.path.slice(0, firstSlash) assertSafePath(`${rootDirectory}/placeholder`, 'archive.rootDirectory') for (const entry of entries) { if ( !entry.path.startsWith(`${rootDirectory}/`) || entry.path.slice(rootDirectory.length + 1).includes('../') ) { archiveFailure( entry.path, 'all Run Pack files must share one root directory', ) } } const relative = new Map( entries.map((entry) => [ entry.path.slice(rootDirectory.length + 1), entry.bytes, ]), ) const manifestBytes = relative.get('manifest.json') if (!manifestBytes) throw new RunPackError( 'run_pack_inventory_mismatch', 'manifest.json', 'is missing', ) const manifest = parseManifest(manifestBytes, limits) const actualPaths = [...relative.keys()] .filter((path) => path !== 'manifest.json') .sort(compareUtf8) const declaredPaths = manifest.files.map((file) => file.path) if (actualPaths.length !== declaredPaths.length) { throw new RunPackError( 'run_pack_inventory_mismatch', 'manifest.json.files', 'does not match the archive file set', ) } for (let index = 0; index < actualPaths.length; index += 1) { if (actualPaths[index] !== declaredPaths[index]) { throw new RunPackError( 'run_pack_inventory_mismatch', actualPaths[index] ?? declaredPaths[index]!, 'is missing or undeclared', ) } } for (const declared of manifest.files) { const content = relative.get(declared.path)! if (content.byteLength !== declared.sizeBytes) { throw new RunPackError( 'run_pack_file_integrity_failed', declared.path, 'size does not match manifest', ) } if (digest(content) !== declared.sha256) { throw new RunPackError( 'run_pack_file_integrity_failed', declared.path, 'SHA-256 does not match manifest', ) } } const { manifestDigest: ignored, ...unsigned } = manifest void ignored const calculatedManifestDigest = digest( canonicalJson(unsigned as unknown as JsonValue), ) if (calculatedManifestDigest !== manifest.manifestDigest) { throw new RunPackError( 'run_pack_manifest_digest_failed', 'manifest.json.manifestDigest', 'does not match canonical manifest bytes', ) } for (const requiredPath of [ 'RUNBOOK.md', 'TASK.md', 'VALIDATION.md', 'HANDOFF_TEMPLATE.md', ]) { if (!relative.has(requiredPath)) { throw new RunPackError( 'run_pack_inventory_mismatch', requiredPath, 'is required', ) } } const task = relative.get('TASK.md')! let taskText: string try { taskText = utf8.decode(task) } catch { throw new RunPackError( 'run_pack_file_integrity_failed', 'TASK.md', 'must be valid UTF-8', ) } const envelopePrefix = '\n\n## Run metadata\n\n```json\n' const delimiter = '\n```\n\n' if (!taskText.startsWith(envelopePrefix) || !taskText.endsWith('\n')) { throw new RunPackError( 'run_pack_file_integrity_failed', 'TASK.md', 'has an invalid deterministic metadata envelope', ) } const delimiterAt = taskText.indexOf(delimiter, envelopePrefix.length) if (delimiterAt < 0) { throw new RunPackError( 'run_pack_file_integrity_failed', 'TASK.md', 'has an incomplete deterministic metadata envelope', ) } let parsedTaskMetadata: unknown const metadataSource = taskText.slice(envelopePrefix.length, delimiterAt) try { assertNoDuplicateJsonKeys(metadataSource) parsedTaskMetadata = JSON.parse(metadataSource) } catch { throw new RunPackError( 'run_pack_file_integrity_failed', 'TASK.md', 'metadata is invalid', ) } if ( !isRecord(parsedTaskMetadata) || metadataSource !== JSON.stringify(taskMetadata(manifest.run), null, 2) ) { throw new RunPackError( 'run_pack_file_integrity_failed', 'TASK.md', 'metadata does not match the manifest run', ) } const embeddedPrompt = taskText.slice(delimiterAt + delimiter.length) if (digest(embeddedPrompt) !== manifest.run.renderDigest) { throw new RunPackError( 'run_pack_file_integrity_failed', 'TASK.md', 'embedded prompt does not match the historical render digest', ) } return Object.freeze({ rootDirectory, manifest: Object.freeze(manifest), files: relative, archiveSha256: digest(archive), }) }