/** * Workspace path safety. * * The pi harness runs untrusted model output. Every file-touching tool must * resolve its path through `safeResolve()` so the agent can't read or write * outside the workspace via traversal (`../../etc/passwd`) or absolute paths. */ import path from 'path'; import fs from 'fs'; export function safeResolve(cwd: string, requested: string): string { if (!requested || typeof requested !== 'string') { throw new Error('Missing file path'); } // Canonicalize cwd (resolves symlinks) so the traversal check below compares real // paths. Falls back to a plain resolve when cwd doesn't exist yet (realpath throws). let root: string; try { root = fs.realpathSync.native(cwd); } catch { root = path.resolve(cwd); } const abs = path.isAbsolute(requested) ? path.normalize(requested) : path.normalize(path.join(root, requested)); const rel = path.relative(root, abs); if (rel.startsWith('..') || path.isAbsolute(rel)) { throw new Error(`Path escapes workspace: ${requested}`); } return abs; } export function displayPath(cwd: string, abs: string): string { const rel = path.relative(cwd, abs); return rel || path.basename(abs); }