import { constants } from "node:fs"; import { lstat, mkdir, open, readdir, readFile, realpath, writeFile, } from "node:fs/promises"; import { dirname, join, resolve } from "node:path"; import { ENVIRONMENT_MANIFEST_FILE, type EnvironmentManifest, type EnvironmentSourceBundle, isForbiddenEnvironmentSourcePath, MAX_ENVIRONMENT_FILES, MAX_ENVIRONMENT_SOURCE_BYTES, parseEnvironmentManifest, validateEnvironmentPath, } from "./environment-contract"; function fail(message: string): never { throw new Error(`validation_failed: ${message}`); } async function checkedPath(root: string, relative: string) { validateEnvironmentPath(relative); let at = root; for (const part of relative.split("/")) { at = join(at, part); const stat = await lstat(at); if (stat.isSymbolicLink()) fail(`Source symlinks are not supported: ${relative}`); } return at; } export function validateSourceBundle(value: unknown): EnvironmentSourceBundle { if ( !value || typeof value !== "object" || (value as { version?: unknown }).version !== 1 || !Array.isArray((value as { files?: unknown }).files) ) fail("Invalid environment source bundle."); const input = value as EnvironmentSourceBundle; if (input.files.length > MAX_ENVIRONMENT_FILES) fail("Too many environment source files."); const seen = new Set(); for (const file of input.files) { if ( !file || typeof file.path !== "string" || typeof file.contentBase64 !== "string" || (file.mode !== 420 && file.mode !== 493) ) fail("Invalid source file."); validateEnvironmentPath(file.path); if (seen.has(file.path) || isForbiddenEnvironmentSourcePath(file.path)) fail("Duplicate or excluded source path."); if ( !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test( file.contentBase64, ) ) fail("Invalid source encoding."); seen.add(file.path); } if ( Buffer.byteLength(JSON.stringify(input), "utf8") > MAX_ENVIRONMENT_SOURCE_BYTES ) fail("Environment source exceeds 16 MiB."); return input; } export async function collectEnvironmentSource( directory: string, name: string, ): Promise<{ manifest: EnvironmentManifest; bundle: EnvironmentSourceBundle; bytes: Buffer; }> { const root = await realpath(resolve(directory)); const manifestPath = await checkedPath(root, ENVIRONMENT_MANIFEST_FILE); const manifestStat = await lstat(manifestPath); if (!manifestStat.isFile() || manifestStat.size > 64 * 1024) fail("dench-env.json must be a file of at most 64 KiB."); const manifest = parseEnvironmentManifest( JSON.parse(await readFile(manifestPath, "utf8")), ); if (manifest.name !== name) fail("The saved environment name must match dench-env.json."); const files: EnvironmentSourceBundle["files"] = []; const seen = new Set(); let sourceBytes = 0; async function visit(relative: string): Promise { if (seen.has(relative)) return; validateEnvironmentPath(relative); if (isForbiddenEnvironmentSourcePath(relative)) return; const absolute = await checkedPath(root, relative); const stat = await lstat(absolute); if (stat.isDirectory()) { for (const child of (await readdir(absolute)).sort()) await visit(`${relative}/${child}`); return; } if (!stat.isFile()) fail(`Only regular source files can be saved: ${relative}`); if (stat.size > MAX_ENVIRONMENT_SOURCE_BYTES) fail("A source file exceeds 16 MiB."); // NOFOLLOW also closes the final-component symlink race during collection. const handle = await open( absolute, constants.O_RDONLY | constants.O_NOFOLLOW, ); let data: Buffer; try { data = await handle.readFile(); } finally { await handle.close(); } if (/-----BEGIN [A-Z ]*PRIVATE KEY-----/.test(data.toString("utf8"))) fail(`Private keys belong in encrypted secrets, not source: ${relative}`); sourceBytes += data.length; if ( sourceBytes > MAX_ENVIRONMENT_SOURCE_BYTES || files.length >= MAX_ENVIRONMENT_FILES ) fail("Environment source exceeds the file or byte limit."); seen.add(relative); files.push({ path: relative, contentBase64: data.toString("base64"), mode: stat.mode & 0o111 ? 493 : 420, }); } for (const path of [ENVIRONMENT_MANIFEST_FILE, "SKILL.md", ...manifest.files]) await visit(path); files.sort((a, b) => a.path.localeCompare(b.path)); const bundle = validateSourceBundle({ version: 1, files }); return { manifest, bundle, bytes: Buffer.from(JSON.stringify(bundle), "utf8"), }; } export async function materializeSource( bundle: EnvironmentSourceBundle, directory: string, ): Promise { validateSourceBundle(bundle); await mkdir(directory, { recursive: true, mode: 0o700 }); for (const file of bundle.files) { const target = join(directory, file.path); await mkdir(dirname(target), { recursive: true, mode: 0o700 }); await writeFile(target, Buffer.from(file.contentBase64, "base64"), { mode: file.mode, flag: "wx", }); } }