import { resolve, sep } from 'node:path'; export class WriteGuardError extends Error { constructor(message: string) { super(message); this.name = 'WriteGuardError'; } } // Deny list: files that should never be written regardless of path-guard. // Subset of BooChat's secret_guard.ts — covers the most dangerous patterns. // Full parity with BooChat's deny list is not needed for write-guard because // the write tools are intentional (model chose to create/edit); we block only // files that are unambiguously secrets. const SECRET_PATTERNS: readonly string[] = [ '.env', '.env.local', '.env.production', '.env.development', '.env.staging', 'id_rsa', 'id_dsa', 'id_ecdsa', 'id_ed25519', '*.pem', '*.key', '*.p12', '*.pfx', '*.crt', 'credentials.json', '*.kdbx', '.netrc', ]; export function isSecretPath(filePath: string): boolean { const normalized = filePath.replace(/\\/g, '/'); const segments = normalized.split('/').filter((s) => s.length > 0); if (segments.length === 0) return false; const basename = segments[segments.length - 1]!; return SECRET_PATTERNS.some((pattern) => { if (pattern.startsWith('*')) { return basename.endsWith(pattern.slice(1)); } return basename === pattern; }); } /** * Resolve and validate a write target path. * * Key difference from BooChat's pathGuard: no realpath() — the file may not * exist yet (creates). Uses resolve() to normalize ../ segments and then * checks the result stays within projectRoot. */ export function resolveWritePath(projectRoot: string, filePath: string): string { if (!filePath || filePath.trim().length === 0) { throw new WriteGuardError('file path is required'); } if (filePath.includes('\x00')) { throw new WriteGuardError('file path contains null byte'); } const candidate = filePath.startsWith('/') ? filePath : resolve(projectRoot, filePath); const normalized = resolve(candidate); // normalizes ../ segments if (!normalized.startsWith(projectRoot + sep) && normalized !== projectRoot) { throw new WriteGuardError(`path escapes project root: ${filePath}`); } if (isSecretPath(normalized)) { throw new WriteGuardError(`cannot write to secret file: ${filePath}`); } return normalized; }