Files
DevRunbook-Public/packages/artifacts/src/run-pack.ts
T
DevRunbook release export cfd2804e27
Managed validation / full (push) Successful in 3m18s
Publish DevRunbook source
2026-09-03 04:09:17 +02:00

1272 lines
37 KiB
TypeScript

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<RunPackLimits>
}
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<string, Uint8Array>
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>): 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(
`<!-- DevRunbook immutable generated task -->\n\n## Run metadata\n\n\`\`\`json\n${metadata}\n\`\`\`\n\n${prompt}`,
)
}
function taskMetadata(
run: RunPackRunMetadata,
): Readonly<Record<string, string | null>> {
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<string>,
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<RunPackManifest, 'manifestDigest'> {
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<string>()
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<string>()
const offsets = new Set<number>()
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<string, unknown> {
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<string>()
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<string, unknown>,
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<string>()
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<RunPackLimits>,
): 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 =
'<!-- DevRunbook immutable generated task -->\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),
})
}