Publish DevRunbook source
Managed validation / full (push) Successful in 3m18s

This commit is contained in:
DevRunbook release export
2026-09-03 04:09:17 +02:00
commit cfd2804e27
928 changed files with 161642 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
{
"name": "@devrunbook/artifacts",
"version": "0.1.0",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"lint": "eslint src --max-warnings=0",
"test": "vitest run --passWithNoTests",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"devDependencies": {
"@types/node": "24.13.3",
"typescript": "5.9.3",
"vitest": "4.1.10"
}
}
@@ -0,0 +1,85 @@
import { describe, expect, it } from 'vitest'
import {
AgentsSuggestionError,
createAgentsSuggestion,
} from './agents-suggestion'
const digest = 'a'.repeat(64)
function snapshot() {
return {
contentDigest: digest,
profile: {
metadata: {
name: 'DevRunbook',
revision: 4,
contentDigest: digest,
},
spec: {
commands: [
{
role: 'unit-test',
command: 'pnpm test',
workingDirectory: '.',
platform: 'any',
shell: 'auto',
confirmed: true,
safeForAgentSuggestion: true,
},
{
role: 'build',
command: 'unconfirmed --must-not-leak',
workingDirectory: '.',
confirmed: false,
safeForAgentSuggestion: true,
},
],
paths: {
protected: ['runtime/secrets'],
excluded: ['node_modules'],
},
policies: {
preserveBackwardCompatibility: true,
newDependencies: 'justify',
gitWrite: 'none',
migrations: 'reversible-only',
documentationRequired: true,
networkAccess: 'forbidden',
productionDataAccess: 'forbidden',
environmentConstraints: ['Use Node.js 24'],
},
},
},
}
}
describe('createAgentsSuggestion', () => {
it('renders only durable confirmed repository guidance as review-only text', () => {
const result = createAgentsSuggestion({
id: '4a36b398-5692-4da0-b915-78395579ca68',
renderDigest: digest,
repositoryProfileSnapshot: snapshot(),
})
const text = new TextDecoder().decode(result.content)
expect(result.filename).toBe('AGENTS.md.suggested')
expect(text).toContain('Review-only export')
expect(text).toContain('pnpm test')
expect(text).toContain('Protected: `runtime/secrets`')
expect(text).toContain('Git writes: none')
expect(text).toContain('Use Node.js 24')
expect(text).not.toContain('unconfirmed --must-not-leak')
expect(text).not.toContain('normalizedInput')
})
it('fails explicitly without an integrity-bound frozen profile', () => {
expect(() =>
createAgentsSuggestion({
id: '4a36b398-5692-4da0-b915-78395579ca68',
renderDigest: digest,
repositoryProfileSnapshot: null,
}),
).toThrowError(AgentsSuggestionError)
})
})
+203
View File
@@ -0,0 +1,203 @@
export interface AgentsSuggestionRun {
readonly id: string
readonly renderDigest: string
readonly repositoryProfileSnapshot: unknown
}
export interface AgentsSuggestion {
readonly filename: 'AGENTS.md.suggested'
readonly content: Uint8Array
}
export class AgentsSuggestionError extends Error {
readonly code = 'agents_suggestion_profile_unavailable'
constructor() {
super(
'A frozen repository profile is required for an AGENTS.md recommendation',
)
this.name = 'AgentsSuggestionError'
}
}
type JsonRecord = Readonly<Record<string, unknown>>
function record(value: unknown): JsonRecord | null {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? (value as JsonRecord)
: null
}
function safeText(value: unknown): string | null {
if (typeof value !== 'string') return null
const normalized = value
.normalize('NFC')
.replace(/\r\n?/gu, '\n')
.split('')
.filter((character) => {
const point = character.codePointAt(0)!
return !(
point <= 8 ||
point === 11 ||
point === 12 ||
(point >= 14 && point <= 31) ||
point === 127 ||
(point >= 0x202a && point <= 0x202e) ||
(point >= 0x2066 && point <= 0x2069)
)
})
.join('')
.trim()
return normalized.length > 0 ? normalized : null
}
function strings(value: unknown): readonly string[] {
if (!Array.isArray(value)) return []
return value.flatMap((item) => {
const text = safeText(item)
return text === null ? [] : [text]
})
}
function indented(value: string): string {
return value
.split('\n')
.map((line) => ` ${line}`)
.join('\n')
}
function profileFromSnapshot(value: unknown): JsonRecord | null {
const snapshot = record(value)
const profile = record(snapshot?.profile)
const metadata = record(profile?.metadata)
const spec = record(profile?.spec)
if (
!snapshot ||
!profile ||
!metadata ||
!spec ||
safeText(snapshot.contentDigest) === null ||
safeText(metadata.contentDigest) === null
) {
return null
}
return profile
}
function commandSection(spec: JsonRecord): string {
if (!Array.isArray(spec.commands))
return 'No confirmed agent-safe commands were captured.'
const commands = spec.commands.flatMap((candidate) => {
const command = record(candidate)
const value = safeText(command?.command)
if (
!command ||
value === null ||
command.confirmed !== true ||
command.safeForAgentSuggestion !== true
) {
return []
}
const role = safeText(command.role) ?? 'validation'
const directory = safeText(command.workingDirectory) ?? '.'
const platform = safeText(command.platform) ?? 'any'
const shell = safeText(command.shell) ?? 'auto'
return [
`- ${role} — working directory: \`${directory.replace(/`/gu, '')}\`; platform: ${platform}; shell: ${shell}\n\n${indented(value)}`,
]
})
return commands.length > 0
? commands.join('\n\n')
: 'No confirmed agent-safe commands were captured.'
}
function pathSection(spec: JsonRecord): string {
const paths = record(spec.paths)
const protectedPaths = strings(paths?.protected)
const excludedPaths = strings(paths?.excluded)
const lines = [
...protectedPaths.map(
(path) => `- Protected: \`${path.replace(/`/gu, '')}\``,
),
...excludedPaths.map(
(path) => `- Excluded: \`${path.replace(/`/gu, '')}\``,
),
]
return lines.length > 0
? lines.join('\n')
: 'No protected or excluded paths were captured.'
}
function policySection(spec: JsonRecord): string {
const policies = record(spec.policies)
if (!policies) return 'No durable repository policies were captured.'
const labels: Readonly<Record<string, string>> = {
preserveBackwardCompatibility: 'Preserve backward compatibility',
newDependencies: 'New dependencies',
gitWrite: 'Git writes',
migrations: 'Migrations',
documentationRequired: 'Documentation required',
networkAccess: 'Network access',
productionDataAccess: 'Production data access',
}
const lines = Object.entries(labels).flatMap(([key, label]) => {
const value = policies[key]
if (typeof value !== 'string' && typeof value !== 'boolean') return []
return [`- ${label}: ${String(value)}`]
})
const constraints = strings(policies.environmentConstraints)
lines.push(
...constraints.map((value) => `- Environment constraint: ${value}`),
)
return lines.length > 0
? lines.join('\n')
: 'No durable repository policies were captured.'
}
export function createAgentsSuggestion(
run: AgentsSuggestionRun,
): AgentsSuggestion {
const profile = profileFromSnapshot(run.repositoryProfileSnapshot)
if (!profile) throw new AgentsSuggestionError()
const metadata = record(profile.metadata)!
const spec = record(profile.spec)!
const name = safeText(metadata.name) ?? 'repository'
const revision = Number.isSafeInteger(metadata.revision)
? String(metadata.revision)
: 'unknown'
const content = [
'# Suggested repository instructions',
'',
'> Review-only export. Inspect and merge these durable rules manually. DevRunbook does not write or overwrite `AGENTS.md`.',
'',
'## Scope',
'',
`These rules apply at the repository root for ${name}. Profile revision: ${revision}. No global or directory-specific instructions are proposed by this export.`,
'',
'## Confirmed commands',
'',
commandSection(spec),
'',
'## Protected and excluded paths',
'',
pathSection(spec),
'',
'## Repository policies',
'',
policySection(spec),
'',
'## Review checklist',
'',
'- Confirm each command and working directory still matches the repository.',
'- Keep secrets, credentials and one-time task requirements out of persistent instructions.',
'- Place narrower rules in a nested `AGENTS.md` only after reviewing their directory scope.',
'- Resolve conflicts with existing instruction files before adopting this suggestion.',
'',
`Evidence: immutable DevRunbook run \`${run.id.replace(/`/gu, '')}\`, prompt digest \`${run.renderDigest.replace(/`/gu, '')}\`.`,
'',
].join('\n')
return Object.freeze({
filename: 'AGENTS.md.suggested',
content: new TextEncoder().encode(content),
})
}
+12
View File
@@ -0,0 +1,12 @@
export interface ArtifactDescriptor {
storageKey: string
filename: string
mediaType: string
sizeBytes: number
sha256: string
}
export * from './local-artifact-storage'
export * from './agents-suggestion'
export * from './run-pack'
export * from './playbook-package-archive'
@@ -0,0 +1,86 @@
import { createHash } from 'node:crypto'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { LocalArtifactStorage } from './local-artifact-storage'
const roots: string[] = []
afterEach(async () => {
await Promise.all(
roots.splice(0).map((root) => rm(root, { recursive: true })),
)
})
async function root(): Promise<string> {
const value = await mkdtemp(path.join(tmpdir(), 'devrunbook-artifacts-'))
roots.push(value)
return value
}
function digest(content: Uint8Array): string {
return createHash('sha256').update(content).digest('hex')
}
describe('LocalArtifactStorage', () => {
it('persists immutable bytes that a restarted adapter can read', async () => {
const artifactRoot = await root()
const content = new TextEncoder().encode('# deterministic\n')
const key = 'a'.repeat(64)
const first = new LocalArtifactStorage(artifactRoot)
await expect(
first.putImmutable(key, content, digest(content)),
).resolves.toEqual({ created: true })
await expect(
first.putImmutable(key, content, digest(content)),
).resolves.toEqual({ created: false })
const restarted = new LocalArtifactStorage(artifactRoot)
await expect(restarted.read(key)).resolves.toEqual(content)
})
it('rejects traversal-shaped keys and relative roots', async () => {
expect(() => new LocalArtifactStorage('relative/artifacts')).toThrowError(
expect.objectContaining({ code: 'artifact_storage_root_invalid' }),
)
const storage = new LocalArtifactStorage(await root())
await expect(storage.read('../outside')).rejects.toMatchObject({
code: 'artifact_storage_key_invalid',
})
await expect(storage.read('A'.repeat(64))).rejects.toMatchObject({
code: 'artifact_storage_key_invalid',
})
})
it('rejects a digest mismatch and conflicting on-disk bytes', async () => {
const artifactRoot = await root()
const storage = new LocalArtifactStorage(artifactRoot)
const key = 'b'.repeat(64)
const content = new TextEncoder().encode('expected')
await expect(
storage.putImmutable(key, content, 'c'.repeat(64)),
).rejects.toMatchObject({ code: 'artifact_storage_integrity_failed' })
const different = new TextEncoder().encode('different')
await storage.putImmutable(key, different, digest(different))
await expect(
storage.putImmutable(key, content, digest(content)),
).rejects.toMatchObject({ code: 'artifact_storage_conflict' })
})
it('deletes only validated storage keys and treats missing bytes idempotently', async () => {
const storage = new LocalArtifactStorage(await root())
const content = new TextEncoder().encode('expired')
const key = digest(content)
await storage.putImmutable(key, content, key)
await expect(storage.deleteIfPresent(key)).resolves.toBe(true)
await expect(storage.deleteIfPresent(key)).resolves.toBe(false)
await expect(storage.deleteIfPresent('../outside')).rejects.toMatchObject({
code: 'artifact_storage_key_invalid',
})
})
})
@@ -0,0 +1,155 @@
import { createHash } from 'node:crypto'
import { mkdir, open, readFile, unlink } from 'node:fs/promises'
import path from 'node:path'
const storageKeyPattern = /^[0-9a-f]{64}$/
export class ArtifactStorageError extends Error {
constructor(
readonly code:
| 'artifact_storage_root_invalid'
| 'artifact_storage_key_invalid'
| 'artifact_storage_conflict'
| 'artifact_storage_not_found'
| 'artifact_storage_integrity_failed',
message: string,
) {
super(message)
this.name = 'ArtifactStorageError'
}
}
function sha256(content: Uint8Array): string {
return createHash('sha256').update(content).digest('hex')
}
function assertDigest(digest: string): void {
if (!storageKeyPattern.test(digest)) {
throw new ArtifactStorageError(
'artifact_storage_integrity_failed',
'Artifact digest must be a lowercase SHA-256 value',
)
}
}
/**
* Local immutable byte storage. Storage keys are opaque SHA-256-shaped values;
* no caller-controlled filename or path segment reaches the filesystem.
*/
export class LocalArtifactStorage {
readonly root: string
constructor(artifactRoot: string) {
if (!path.isAbsolute(artifactRoot)) {
throw new ArtifactStorageError(
'artifact_storage_root_invalid',
'ARTIFACT_ROOT must be an absolute path',
)
}
this.root = path.resolve(artifactRoot)
}
private resolveKey(storageKey: string): string {
if (!storageKeyPattern.test(storageKey)) {
throw new ArtifactStorageError(
'artifact_storage_key_invalid',
'Artifact storage key is invalid',
)
}
const target = path.resolve(
this.root,
storageKey.slice(0, 2),
storageKey.slice(2),
)
const relative = path.relative(this.root, target)
if (
relative.length === 0 ||
relative.startsWith(`..${path.sep}`) ||
path.isAbsolute(relative)
) {
throw new ArtifactStorageError(
'artifact_storage_key_invalid',
'Artifact storage key escapes ARTIFACT_ROOT',
)
}
return target
}
async putImmutable(
storageKey: string,
content: Uint8Array,
expectedSha256: string,
): Promise<{ readonly created: boolean }> {
assertDigest(expectedSha256)
if (sha256(content) !== expectedSha256) {
throw new ArtifactStorageError(
'artifact_storage_integrity_failed',
'Artifact bytes do not match their declared digest',
)
}
const target = this.resolveKey(storageKey)
await mkdir(path.dirname(target), { recursive: true, mode: 0o700 })
let file
try {
file = await open(target, 'wx', 0o600)
await file.writeFile(content)
await file.sync()
return Object.freeze({ created: true })
} catch (error) {
if (!isAlreadyExists(error)) throw error
const existing = await readFile(target)
if (
existing.byteLength !== content.byteLength ||
sha256(existing) !== expectedSha256
) {
throw new ArtifactStorageError(
'artifact_storage_conflict',
'Artifact storage key already contains different immutable bytes',
)
}
return Object.freeze({ created: false })
} finally {
await file?.close()
}
}
async read(storageKey: string): Promise<Uint8Array> {
const target = this.resolveKey(storageKey)
try {
return new Uint8Array(await readFile(target))
} catch (error) {
if (isNotFound(error)) {
throw new ArtifactStorageError(
'artifact_storage_not_found',
'Artifact bytes were not found',
)
}
throw error
}
}
async deleteIfPresent(storageKey: string): Promise<boolean> {
const target = this.resolveKey(storageKey)
try {
await unlink(target)
return true
} catch (error) {
if (isNotFound(error)) return false
throw error
}
}
}
function errorCode(error: unknown): string | undefined {
return error && typeof error === 'object' && 'code' in error
? String(error.code)
: undefined
}
function isAlreadyExists(error: unknown): boolean {
return errorCode(error) === 'EEXIST'
}
function isNotFound(error: unknown): boolean {
return errorCode(error) === 'ENOENT'
}
@@ -0,0 +1,236 @@
import { createHash } from 'node:crypto'
import { describe, expect, it } from 'vitest'
import {
encodeDeterministicZip,
exportPlaybookPackageArchive,
importPlaybookPackageArchive,
type PlaybookPackageArchiveFile,
} from './index'
const manifest = Buffer.from(
'apiVersion: devrunbook.io/v1alpha1\nkind: Playbook\n',
)
const prompt = Uint8Array.from([
0x23, 0x20, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x0a,
])
function packageFiles(): PlaybookPackageArchiveFile[] {
return [
{ path: 'prompt.md', role: 'template', content: prompt },
{ path: 'playbook.yaml', role: 'manifest', content: manifest },
]
}
interface LocatedEntry {
readonly centralOffset: number
readonly localOffset: number
readonly dataOffset: number
readonly size: number
readonly path: string
}
function locateEntries(bytes: Uint8Array): LocatedEntry[] {
const buffer = Buffer.from(bytes)
const endOffset = buffer.byteLength - 22
const count = buffer.readUInt16LE(endOffset + 10)
let cursor = buffer.readUInt32LE(endOffset + 16)
const result: LocatedEntry[] = []
for (let index = 0; index < count; index += 1) {
const nameLength = buffer.readUInt16LE(cursor + 28)
const extraLength = buffer.readUInt16LE(cursor + 30)
const commentLength = buffer.readUInt16LE(cursor + 32)
const localOffset = buffer.readUInt32LE(cursor + 42)
const localNameLength = buffer.readUInt16LE(localOffset + 26)
const localExtraLength = buffer.readUInt16LE(localOffset + 28)
result.push({
centralOffset: cursor,
localOffset,
dataOffset: localOffset + 30 + localNameLength + localExtraLength,
size: buffer.readUInt32LE(cursor + 24),
path: buffer
.subarray(cursor + 46, cursor + 46 + nameLength)
.toString('utf8'),
})
cursor += 46 + nameLength + extraLength + commentLength
}
return result
}
function errorCode(action: () => unknown): string | undefined {
try {
action()
} catch (error) {
return (error as { code?: string }).code
}
return undefined
}
describe('playbook package archive codec', () => {
it('exports deterministic bytes and imports exact in-memory contents', () => {
const first = exportPlaybookPackageArchive(packageFiles())
const second = exportPlaybookPackageArchive([...packageFiles()].reverse())
expect(first.bytes).toEqual(second.bytes)
expect(first.sha256).toBe(
createHash('sha256').update(first.bytes).digest('hex'),
)
const imported = importPlaybookPackageArchive(first.bytes)
expect(imported.files.map((file) => file.path)).toEqual([
'playbook.yaml',
'prompt.md',
])
expect(Buffer.from(imported.files[0]!.content)).toEqual(manifest)
expect(imported.files[1]!.content).toEqual(prompt)
first.bytes.fill(0)
expect(Buffer.from(first.files[0]!.content)).toEqual(manifest)
expect(first.files[1]!.content).toEqual(prompt)
})
it('requires exactly one canonical file with the reserved manifest role', () => {
expect(() =>
exportPlaybookPackageArchive([
{ path: 'playbook.yaml', role: 'template', content: manifest },
]),
).toThrowError(
expect.objectContaining({ code: 'playbook_archive_input_invalid' }),
)
expect(() =>
exportPlaybookPackageArchive([
{ path: 'other.yaml', role: 'manifest', content: manifest },
]),
).toThrowError(
expect.objectContaining({ code: 'playbook_archive_input_invalid' }),
)
})
it.each([
['traversal', '../evil.yaml'],
['absolute', '/evil.yaml'],
['backslash', 'bad\\name.yaml'],
])('rejects %s paths on import', (_label, path) => {
const hostile = encodeDeterministicZip([
{ path: 'playbook.yaml', bytes: manifest },
{ path, bytes: prompt },
])
expect(errorCode(() => importPlaybookPackageArchive(hostile))).toBe(
'playbook_archive_path_unsafe',
)
})
it('rejects duplicate and portable case-colliding paths', () => {
const duplicate = encodeDeterministicZip([
{ path: 'playbook.yaml', bytes: manifest },
{ path: 'playbook.yaml', bytes: manifest },
])
const collision = encodeDeterministicZip([
{ path: 'playbook.yaml', bytes: manifest },
{ path: 'PLAYBOOK.YAML', bytes: manifest },
])
for (const archive of [duplicate, collision]) {
expect(errorCode(() => importPlaybookPackageArchive(archive))).toBe(
'playbook_archive_invalid',
)
}
})
it('rejects symlinks, encryption, data descriptors, and compression methods outside store/deflate', () => {
const created = exportPlaybookPackageArchive(packageFiles()).bytes
const mutations: Buffer[] = []
const symlink = Buffer.from(created)
const symlinkEntry = locateEntries(symlink)[0]!
symlink.writeUInt32LE(
(0o120777 * 65_536) >>> 0,
symlinkEntry.centralOffset + 38,
)
mutations.push(symlink)
for (const flag of [0x0001, 0x0008]) {
const hostile = Buffer.from(created)
const entry = locateEntries(hostile)[0]!
hostile.writeUInt16LE(0x0800 | flag, entry.localOffset + 6)
hostile.writeUInt16LE(0x0800 | flag, entry.centralOffset + 8)
mutations.push(hostile)
}
const unsupported = Buffer.from(created)
const unsupportedEntry = locateEntries(unsupported)[0]!
unsupported.writeUInt16LE(99, unsupportedEntry.localOffset + 8)
unsupported.writeUInt16LE(99, unsupportedEntry.centralOffset + 10)
mutations.push(unsupported)
for (const archive of mutations) {
expect(errorCode(() => importPlaybookPackageArchive(archive))).toBe(
'playbook_archive_invalid',
)
}
})
it('rejects aliased or overlapping local data regions', () => {
const aliased = Buffer.from(
exportPlaybookPackageArchive(packageFiles()).bytes,
)
const aliasedEntries = locateEntries(aliased)
aliased.writeUInt32LE(
aliasedEntries[0]!.localOffset,
aliasedEntries[1]!.centralOffset + 42,
)
expect(errorCode(() => importPlaybookPackageArchive(aliased))).toBe(
'playbook_archive_invalid',
)
const embeddedHeader = Buffer.alloc(80)
embeddedHeader.writeUInt32LE(0x04034b50, 0)
const overlapping = Buffer.from(
encodeDeterministicZip([
{ path: 'playbook.yaml', bytes: embeddedHeader },
{ path: 'prompt.md', bytes: prompt },
]),
)
const overlappingEntries = locateEntries(overlapping)
overlapping.writeUInt32LE(
overlappingEntries[0]!.dataOffset,
overlappingEntries[1]!.centralOffset + 42,
)
expect(errorCode(() => importPlaybookPackageArchive(overlapping))).toBe(
'playbook_archive_invalid',
)
})
it('rejects CRC corruption and compressed, expanded, file, and count limit violations', () => {
const created = exportPlaybookPackageArchive(packageFiles()).bytes
const corrupted = Buffer.from(created)
const corruptAt = locateEntries(corrupted)[0]!.dataOffset
corrupted[corruptAt] = corrupted[corruptAt]! ^ 0xff
expect(errorCode(() => importPlaybookPackageArchive(corrupted))).toBe(
'playbook_archive_invalid',
)
const cases = [
{ maxArchiveBytes: created.byteLength - 1 },
{ maxExpandedBytes: 4 },
{ maxFileBytes: 4 },
{ maxFiles: 1 },
]
for (const limits of cases) {
expect(
errorCode(() => importPlaybookPackageArchive(created, limits)),
).toBe('playbook_archive_limit')
}
})
it('rejects declared expansion bombs before allocating their output', () => {
const hostile = Buffer.from(
exportPlaybookPackageArchive(packageFiles()).bytes,
)
const entry = locateEntries(hostile)[0]!
hostile.writeUInt32LE(2 * 1024 * 1024, entry.localOffset + 22)
hostile.writeUInt32LE(2 * 1024 * 1024, entry.centralOffset + 24)
expect(errorCode(() => importPlaybookPackageArchive(hostile))).toBe(
'playbook_archive_limit',
)
})
})
@@ -0,0 +1,296 @@
import { createHash } from 'node:crypto'
import {
encodeDeterministicZip,
readBoundedZip,
RunPackError,
} from './run-pack'
const encoder = new TextEncoder()
const safePathPattern = /^[A-Za-z0-9._/-]+$/u
const windowsReservedName = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/iu
/**
* These values intentionally mirror `@devrunbook/content` package validation
* limits without introducing an artifacts -> content domain dependency.
*/
export const defaultPlaybookPackageArchiveLimits = Object.freeze({
maxArchiveBytes: 11 * 1024 * 1024,
maxExpandedBytes: 10 * 1024 * 1024,
maxFiles: 201,
maxFileBytes: 1024 * 1024,
})
export interface PlaybookPackageArchiveLimits {
readonly maxArchiveBytes: number
readonly maxExpandedBytes: number
readonly maxFiles: number
readonly maxFileBytes: number
}
export interface PlaybookPackageArchiveFile {
readonly path: string
readonly role: string
readonly content: string | Uint8Array
}
export interface ImportedPlaybookPackageFile {
readonly path: string
readonly content: Uint8Array
}
export interface ExportedPlaybookPackageArchive {
readonly bytes: Uint8Array
readonly sha256: string
readonly files: readonly ImportedPlaybookPackageFile[]
}
export interface ImportedPlaybookPackageArchive {
readonly files: readonly ImportedPlaybookPackageFile[]
readonly sha256: string
}
export type PlaybookPackageArchiveErrorCode =
| 'playbook_archive_input_invalid'
| 'playbook_archive_limit'
| 'playbook_archive_path_unsafe'
| 'playbook_archive_invalid'
export class PlaybookPackageArchiveError extends Error {
constructor(
readonly code: PlaybookPackageArchiveErrorCode,
readonly path: string,
message: string,
) {
super(`${path}: ${message}`)
this.name = 'PlaybookPackageArchiveError'
}
}
function mergeLimits(
overrides?: Partial<PlaybookPackageArchiveLimits>,
): PlaybookPackageArchiveLimits {
const limits = { ...defaultPlaybookPackageArchiveLimits, ...overrides }
for (const [name, value] of Object.entries(limits)) {
if (!Number.isSafeInteger(value) || value <= 0) {
throw new PlaybookPackageArchiveError(
'playbook_archive_input_invalid',
`limits.${name}`,
'must be a positive safe integer',
)
}
}
return limits
}
function assertSafePath(candidate: string, label: string): void {
if (
candidate.length === 0 ||
candidate.length > 500 ||
!safePathPattern.test(candidate) ||
candidate.startsWith('/') ||
candidate.endsWith('/') ||
candidate.includes('//') ||
candidate.includes('\\') ||
candidate.includes('\0')
) {
throw new PlaybookPackageArchiveError(
'playbook_archive_path_unsafe',
label,
'must be a normalized relative ASCII file path',
)
}
for (const segment of candidate.split('/')) {
if (
segment === '.' ||
segment === '..' ||
segment.endsWith('.') ||
segment.endsWith(' ') ||
windowsReservedName.test(segment)
) {
throw new PlaybookPackageArchiveError(
'playbook_archive_path_unsafe',
label,
`contains unsafe path segment ${JSON.stringify(segment)}`,
)
}
}
}
function bytesFor(content: string | Uint8Array, path: string): Uint8Array {
if (typeof content === 'string') return encoder.encode(content)
if (!(content instanceof Uint8Array)) {
throw new PlaybookPackageArchiveError(
'playbook_archive_input_invalid',
path,
'content must be a string or Uint8Array',
)
}
return content.slice()
}
function compareUtf8(left: string, right: string): number {
return Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8'))
}
function archiveDigest(bytes: Uint8Array): string {
return createHash('sha256').update(bytes).digest('hex')
}
function mapZipError(error: unknown): never {
if (!(error instanceof RunPackError)) throw error
const code: PlaybookPackageArchiveErrorCode =
error.code === 'run_pack_archive_limit'
? 'playbook_archive_limit'
: error.code === 'run_pack_path_unsafe'
? 'playbook_archive_path_unsafe'
: 'playbook_archive_invalid'
const prefix = `${error.path}: `
const message = error.message.startsWith(prefix)
? error.message.slice(prefix.length)
: error.message
throw new PlaybookPackageArchiveError(code, error.path, message)
}
function validateInventory(
files: readonly PlaybookPackageArchiveFile[],
limits: PlaybookPackageArchiveLimits,
): ImportedPlaybookPackageFile[] {
if (files.length === 0 || files.length > limits.maxFiles) {
throw new PlaybookPackageArchiveError(
'playbook_archive_limit',
'files',
`must contain between 1 and ${limits.maxFiles} files`,
)
}
const seen = new Set<string>()
const result: ImportedPlaybookPackageFile[] = []
let totalBytes = 0
let manifestCount = 0
for (const [index, file] of files.entries()) {
const label = `files[${index}]`
assertSafePath(file.path, `${label}.path`)
const collisionKey = file.path.normalize('NFC').toLowerCase()
if (seen.has(collisionKey)) {
throw new PlaybookPackageArchiveError(
'playbook_archive_input_invalid',
`${label}.path`,
'duplicates or case-collides with another package path',
)
}
seen.add(collisionKey)
if (
typeof file.role !== 'string' ||
!/^[a-z][a-z0-9-]{0,63}$/u.test(file.role)
) {
throw new PlaybookPackageArchiveError(
'playbook_archive_input_invalid',
`${label}.role`,
'must be a normalized package role',
)
}
if (file.path === 'playbook.yaml') {
manifestCount += 1
if (file.role !== 'manifest') {
throw new PlaybookPackageArchiveError(
'playbook_archive_input_invalid',
`${label}.role`,
'playbook.yaml must use the reserved manifest role',
)
}
} else if (file.role === 'manifest') {
throw new PlaybookPackageArchiveError(
'playbook_archive_input_invalid',
`${label}.role`,
'the manifest role is reserved for playbook.yaml',
)
}
const content = bytesFor(file.content, `${label}.content`)
if (content.byteLength > limits.maxFileBytes) {
throw new PlaybookPackageArchiveError(
'playbook_archive_limit',
file.path,
'single-file expanded limit exceeded',
)
}
totalBytes += content.byteLength
if (totalBytes > limits.maxExpandedBytes) {
throw new PlaybookPackageArchiveError(
'playbook_archive_limit',
'files',
'expanded package limit exceeded',
)
}
result.push({ path: file.path, content })
}
if (manifestCount !== 1) {
throw new PlaybookPackageArchiveError(
'playbook_archive_input_invalid',
'playbook.yaml',
'exactly one manifest file is required',
)
}
return result.sort((left, right) => compareUtf8(left.path, right.path))
}
export function exportPlaybookPackageArchive(
files: readonly PlaybookPackageArchiveFile[],
limitOverrides?: Partial<PlaybookPackageArchiveLimits>,
): ExportedPlaybookPackageArchive {
const limits = mergeLimits(limitOverrides)
const inventory = validateInventory(files, limits)
const bytes = encodeDeterministicZip(
inventory.map((file) => ({ path: file.path, bytes: file.content })),
)
if (bytes.byteLength > limits.maxArchiveBytes) {
throw new PlaybookPackageArchiveError(
'playbook_archive_limit',
'archive',
'compressed archive limit exceeded',
)
}
return {
bytes,
sha256: archiveDigest(bytes),
files: inventory.map((file) => ({
path: file.path,
content: file.content.slice(),
})),
}
}
export function importPlaybookPackageArchive(
archive: Uint8Array,
limitOverrides?: Partial<PlaybookPackageArchiveLimits>,
): ImportedPlaybookPackageArchive {
if (!(archive instanceof Uint8Array)) {
throw new PlaybookPackageArchiveError(
'playbook_archive_input_invalid',
'archive',
'must be a Uint8Array',
)
}
const limits = mergeLimits(limitOverrides)
try {
const entries = readBoundedZip(archive, limits)
if (!entries.some((entry) => entry.path === 'playbook.yaml')) {
throw new PlaybookPackageArchiveError(
'playbook_archive_invalid',
'playbook.yaml',
'required manifest file is missing',
)
}
return {
files: entries
.sort((left, right) => compareUtf8(left.path, right.path))
.map((entry) => ({
path: entry.path,
content: entry.bytes.slice(),
})),
sha256: archiveDigest(archive),
}
} catch (error) {
if (error instanceof PlaybookPackageArchiveError) throw error
mapZipError(error)
}
}
+511
View File
@@ -0,0 +1,511 @@
import { createHash } from 'node:crypto'
import { describe, expect, it } from 'vitest'
import {
RunPackError,
createRunPack,
createTaskMarkdown,
verifyRunPack,
type CreateRunPackInput,
} from './run-pack'
const prompt = '# Mission\n\nImplement the bounded change.\n'
function hash(value: string | Uint8Array): string {
return createHash('sha256').update(value).digest('hex')
}
function input(
overrides: Partial<CreateRunPackInput> = {},
): CreateRunPackInput {
return {
slug: 'safe-refactor',
run: {
id: '019fa0c7-e928-7720-b3f5-3c703b8a7ab6',
playbookId: 'engineering.safe-refactor',
playbookVersion: '1.2.0',
playbookDigest: 'a'.repeat(64),
repositoryProfileDigest: 'b'.repeat(64),
renderDigest: hash(prompt),
generatedAt: '2026-07-27T09:30:00.000Z',
platformVersion: '1.0.0',
},
renderedPrompt: prompt,
repositoryContext: '# Repository context\n\nProtected: `infra/`.\n',
...overrides,
}
}
interface LocatedEntry {
readonly centralOffset: number
readonly localOffset: number
readonly dataOffset: number
readonly size: number
readonly path: string
}
function entries(bytes: Uint8Array): LocatedEntry[] {
const buffer = Buffer.from(bytes)
const end = buffer.byteLength - 22
const count = buffer.readUInt16LE(end + 10)
let cursor = buffer.readUInt32LE(end + 16)
const result: LocatedEntry[] = []
for (let index = 0; index < count; index += 1) {
const nameLength = buffer.readUInt16LE(cursor + 28)
const extraLength = buffer.readUInt16LE(cursor + 30)
const commentLength = buffer.readUInt16LE(cursor + 32)
const localOffset = buffer.readUInt32LE(cursor + 42)
const localNameLength = buffer.readUInt16LE(localOffset + 26)
const localExtraLength = buffer.readUInt16LE(localOffset + 28)
result.push({
centralOffset: cursor,
localOffset,
dataOffset: localOffset + 30 + localNameLength + localExtraLength,
size: buffer.readUInt32LE(cursor + 24),
path: buffer
.subarray(cursor + 46, cursor + 46 + nameLength)
.toString('utf8'),
})
cursor += 46 + nameLength + extraLength + commentLength
}
return result
}
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 canonicalJson(value: unknown): string {
if (value === null || typeof value !== 'object') return JSON.stringify(value)
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`
const record = value as Record<string, unknown>
return `{${Object.keys(record)
.sort()
.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
.join(',')}}`
}
function replaceEntryPath(
archive: Uint8Array,
currentRelativePath: string,
replacementRelativePath: string,
): Uint8Array {
expect(Buffer.byteLength(replacementRelativePath)).toBe(
Buffer.byteLength(currentRelativePath),
)
const result = Buffer.from(archive)
const located = entries(result).find((entry) =>
entry.path.endsWith(`/${currentRelativePath}`),
)!
const original = Buffer.from(located.path, 'utf8')
const replacement = Buffer.from(
`${located.path.slice(0, -currentRelativePath.length)}${replacementRelativePath}`,
'utf8',
)
expect(replacement.byteLength).toBe(original.byteLength)
replacement.copy(result, located.centralOffset + 46)
replacement.copy(result, located.localOffset + 30)
return result
}
function replaceStoredEntryContent(
archive: Uint8Array,
relativePath: string,
transform: (content: string) => string,
): Uint8Array {
const result = Buffer.from(archive)
const located = entries(result).find((entry) =>
entry.path.endsWith(`/${relativePath}`),
)!
const current = result
.subarray(located.dataOffset, located.dataOffset + located.size)
.toString('utf8')
const replacement = Buffer.from(transform(current), 'utf8')
expect(replacement.byteLength).toBe(located.size)
replacement.copy(result, located.dataOffset)
const crc = crc32(replacement)
result.writeUInt32LE(crc, located.localOffset + 14)
result.writeUInt32LE(crc, located.centralOffset + 16)
return result
}
describe('deterministic Run Pack generation', () => {
it('creates byte-identical archives, sorted inventories, and a canonical manifest digest', () => {
const first = createRunPack(input())
const second = createRunPack(input())
expect(first.bytes).toEqual(second.bytes)
expect(first.sha256).toBe(second.sha256)
expect(first.filename).toBe('DevRunbook-safe-refactor-019fa0c7-e92.zip')
expect(first.manifest.files.map((file) => file.path)).toEqual([
'HANDOFF_TEMPLATE.md',
'REPOSITORY_CONTEXT.md',
'RUNBOOK.md',
'TASK.md',
'VALIDATION.md',
])
expect(entries(first.bytes).map((entry) => entry.path)).toEqual(
[...entries(first.bytes).map((entry) => entry.path)].sort(),
)
expect(first.taskMarkdown).toContain(first.manifest.run.renderDigest)
expect(first.taskMarkdown.endsWith('\n')).toBe(true)
expect(first.taskMarkdown).not.toContain('\r')
const verified = verifyRunPack(first.bytes)
expect(verified.rootDirectory).toBe(first.rootDirectory)
expect(verified.archiveSha256).toBe(first.sha256)
expect(verified.manifest).toEqual(first.manifest)
expect(verified.files.get('TASK.md')).toEqual(
new TextEncoder().encode(first.taskMarkdown),
)
})
it('normalizes Markdown deterministically and requires the historical prompt digest', () => {
const crlfPrompt = '# Mission\r\n\r\nImplement. \r\n'
const normalized = '# Mission\n\nImplement.\n'
const configured = input({
renderedPrompt: crlfPrompt,
run: { ...input().run, renderDigest: hash(normalized) },
})
const task = createTaskMarkdown(configured.run, configured.renderedPrompt)
expect(task).toContain(normalized)
expect(task).not.toContain('\r')
expect(() =>
createTaskMarkdown(
{ ...configured.run, renderDigest: 'c'.repeat(64) },
crlfPrompt,
),
).toThrowError(
expect.objectContaining({
code: 'run_pack_input_invalid',
path: 'renderedPrompt',
}),
)
})
it('sanitizes archive metadata names without allowing Windows device names', () => {
const created = createRunPack(input({ slug: 'CON' }))
expect(created.filename).toMatch(/^DevRunbook-run-CON-/)
expect(created.filename).not.toContain('..')
})
it('rejects unsafe, duplicate, reserved, manifest, and oversized additional files', () => {
for (const unsafePath of [
'../escape.md',
'/absolute.md',
'nested\\file.md',
'CON.txt',
'folder/NUL',
'trailing.',
'manifest.json',
'double//slash.md',
'unicode-\u202e.md',
]) {
expect(() =>
createRunPack(
input({
additionalFiles: [
{ path: unsafePath, mediaType: 'text/markdown', content: 'safe' },
],
}),
),
).toThrowError(RunPackError)
}
expect(() =>
createRunPack(
input({
additionalFiles: [
{ path: 'EXTRA.md', mediaType: 'text/markdown', content: 'one' },
{ path: 'extra.md', mediaType: 'text/markdown', content: 'two' },
],
}),
),
).toThrowError(expect.objectContaining({ code: 'run_pack_input_invalid' }))
expect(() =>
createRunPack(
input({
additionalFiles: [
{
path: 'BIG.bin',
mediaType: 'application/octet-stream',
content: new Uint8Array(17),
},
],
limits: { maxFileBytes: 16 },
}),
),
).toThrowError(expect.objectContaining({ code: 'run_pack_archive_limit' }))
})
})
describe('hostile Run Pack verification', () => {
it.each([
['traversal', '../x.md'],
['Windows reserved name', 'CON.txt'],
['backslash', 'bad\\.md'],
])(
'rejects a %s archive path before reading the manifest',
(_label, replacement) => {
const created = createRunPack(input())
const hostile = replaceEntryPath(created.bytes, 'TASK.md', replacement)
expect(() => verifyRunPack(hostile)).toThrowError(
expect.objectContaining({ code: 'run_pack_path_unsafe' }),
)
},
)
it('rejects duplicate and case-colliding archive entries', () => {
const created = createRunPack(
input({
additionalFiles: [
{ path: 'ALPHA.md', mediaType: 'text/markdown', content: 'alpha' },
{ path: 'BRAVO.md', mediaType: 'text/markdown', content: 'bravo' },
],
}),
)
const duplicate = replaceEntryPath(created.bytes, 'BRAVO.md', 'ALPHA.md')
expect(() => verifyRunPack(duplicate)).toThrowError(
expect.objectContaining({ code: 'run_pack_archive_invalid' }),
)
})
it('rejects symlinks and other non-regular Unix entries', () => {
const created = createRunPack(input())
const hostile = Buffer.from(created.bytes)
const task = entries(hostile).find((entry) =>
entry.path.endsWith('/TASK.md'),
)!
hostile.writeUInt32LE((0o120777 * 65_536) >>> 0, task.centralOffset + 38)
expect(() => verifyRunPack(hostile)).toThrowError(
expect.objectContaining({
code: 'run_pack_archive_invalid',
path: task.path,
}),
)
})
it('rejects local-entry aliases before trusting either payload', () => {
const created = createRunPack(input())
const hostile = Buffer.from(created.bytes)
const located = entries(hostile)
const task = located.find((entry) => entry.path.endsWith('/TASK.md'))!
const runbook = located.find((entry) => entry.path.endsWith('/RUNBOOK.md'))!
hostile.writeUInt32LE(runbook.localOffset, task.centralOffset + 42)
expect(() => verifyRunPack(hostile)).toThrowError(
expect.objectContaining({
code: 'run_pack_archive_invalid',
path: task.path,
}),
)
})
it('enforces compressed, expanded, single-file, and file-count limits before manifest trust', () => {
const created = createRunPack(input())
expect(() =>
verifyRunPack(created.bytes, {
maxArchiveBytes: created.bytes.byteLength - 1,
}),
).toThrowError(
expect.objectContaining({
code: 'run_pack_archive_limit',
path: 'archive',
}),
)
expect(() =>
verifyRunPack(created.bytes, { maxExpandedBytes: 64 }),
).toThrowError(expect.objectContaining({ code: 'run_pack_archive_limit' }))
expect(() =>
verifyRunPack(created.bytes, { maxFileBytes: 64 }),
).toThrowError(expect.objectContaining({ code: 'run_pack_archive_limit' }))
expect(() =>
verifyRunPack(created.bytes, { maxFiles: 5, maxManifestFiles: 4 }),
).toThrowError(
expect.objectContaining({
code: 'run_pack_archive_limit',
path: 'archive.files',
}),
)
})
it('rejects undeclared and missing files before checking the manifest digest', () => {
const created = createRunPack(
input({
additionalFiles: [
{ path: 'EXTRA.md', mediaType: 'text/markdown', content: 'extra' },
],
}),
)
const changed = replaceStoredEntryContent(
created.bytes,
'manifest.json',
(manifest) => manifest.replace('EXTRA.md', 'EXTRB.md'),
)
expect(() => verifyRunPack(changed)).toThrowError(
expect.objectContaining({ code: 'run_pack_inventory_mismatch' }),
)
})
it('rejects file size and hash mismatches before the manifest digest', () => {
const created = createRunPack(input())
const taskChanged = replaceStoredEntryContent(
created.bytes,
'TASK.md',
(task) =>
task.replace(
'Implement the bounded change.',
'Implement the bounded changf.',
),
)
expect(() => verifyRunPack(taskChanged)).toThrowError(
expect.objectContaining({
code: 'run_pack_file_integrity_failed',
path: 'TASK.md',
}),
)
const taskSize = created.manifest.files.find(
(file) => file.path === 'TASK.md',
)!.sizeBytes
const replacementSize = taskSize + (taskSize % 10 === 9 ? -1 : 1)
const sizeChanged = replaceStoredEntryContent(
created.bytes,
'manifest.json',
(manifest) =>
manifest.replace(
`"sizeBytes": ${taskSize}`,
`"sizeBytes": ${replacementSize}`,
),
)
expect(() => verifyRunPack(sizeChanged)).toThrowError(
expect.objectContaining({
code: 'run_pack_file_integrity_failed',
path: 'TASK.md',
}),
)
})
it('rejects canonical manifest digest mismatches after file integrity passes', () => {
const created = createRunPack(input())
const changed = replaceStoredEntryContent(
created.bytes,
'manifest.json',
(manifest) =>
manifest.replace(
created.manifest.manifestDigest,
`${created.manifest.manifestDigest[0] === '0' ? '1' : '0'}${created.manifest.manifestDigest.slice(1)}`,
),
)
expect(() => verifyRunPack(changed)).toThrowError(
expect.objectContaining({ code: 'run_pack_manifest_digest_failed' }),
)
})
it('hashes the exact embedded TASK.md prompt instead of trusting its metadata digest', () => {
const created = createRunPack(input())
const taskChanged = replaceStoredEntryContent(
created.bytes,
'TASK.md',
(task) =>
task.replace(
'Implement the bounded change.',
'Implement the bounded changf.',
),
)
const changedTaskBytes = entries(taskChanged).find((entry) =>
entry.path.endsWith('/TASK.md'),
)!
const taskBuffer = Buffer.from(taskChanged).subarray(
changedTaskBytes.dataOffset,
changedTaskBytes.dataOffset + changedTaskBytes.size,
)
const changedTaskHash = hash(taskBuffer)
const withFileHash = replaceStoredEntryContent(
taskChanged,
'manifest.json',
(source) =>
source.replace(
created.manifest.files.find((file) => file.path === 'TASK.md')!
.sha256,
changedTaskHash,
),
)
const withCanonicalManifest = replaceStoredEntryContent(
withFileHash,
'manifest.json',
(source) => {
const manifest = JSON.parse(source) as Record<string, unknown>
const previous = manifest.manifestDigest as string
delete manifest.manifestDigest
const next = hash(canonicalJson(manifest))
return source.replace(previous, next)
},
)
expect(() => verifyRunPack(withCanonicalManifest)).toThrowError(
expect.objectContaining({
code: 'run_pack_file_integrity_failed',
path: 'TASK.md',
}),
)
})
it('rejects duplicate JSON mapping keys in manifest objects', () => {
const created = createRunPack(input())
const changed = replaceStoredEntryContent(
created.bytes,
'manifest.json',
(manifest) => manifest.replace('"mediaType"', '"sizeBytes"'),
)
expect(() => verifyRunPack(changed)).toThrowError(
expect.objectContaining({
code: 'run_pack_manifest_invalid',
path: 'manifest.json',
}),
)
})
it('rejects invalid manifest schema before inventory and digest verification', () => {
const created = createRunPack(input())
const changed = replaceStoredEntryContent(
created.bytes,
'manifest.json',
(manifest) =>
manifest.replace('devrunbook.io/v1alpha1', 'devrunbook.io/v9alpha9'),
)
expect(() => verifyRunPack(changed)).toThrowError(
expect.objectContaining({
code: 'run_pack_manifest_invalid',
path: 'manifest.json',
}),
)
})
it('rejects trailing bytes, ZIP comments, truncation, and unsupported signatures', () => {
const created = createRunPack(input())
const trailing = Buffer.concat([created.bytes, Buffer.from([0])])
const commented = Buffer.from(created.bytes)
commented.writeUInt16LE(1, commented.byteLength - 2)
const badSignature = Buffer.from(created.bytes)
badSignature.writeUInt32LE(0, badSignature.byteLength - 22)
for (const archive of [
trailing,
commented,
badSignature,
created.bytes.slice(0, 10),
]) {
expect(() => verifyRunPack(archive)).toThrowError(
expect.objectContaining({ code: 'run_pack_archive_invalid' }),
)
}
})
})
File diff suppressed because it is too large Load Diff
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"declaration": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*.ts"]
}