import { promises as fs } from 'node:fs'; import { dirname, isAbsolute, resolve, sep } from 'node:path'; /** * Resolve an ACP-supplied path against the agent worktree and reject anything * that escapes it. Mirrors `write_guard.ts`'s check: `resolve()` to normalize * `../` segments, then a **separator-bounded** prefix test — a bare * `startsWith(root)` wrongly admits a sibling dir like `-evil/...`. * * No realpath (consistent with `write_guard.ts`: the target may not exist yet on * write). This is a containment guard for the ACP fs bridge, not a hard trust * boundary — the agent process already runs with host FS access; symlink-swap * hardening (`O_NOFOLLOW`/realpath) is out of scope here. */ function resolveInWorktree(worktreePath: string, filePath: string): string { const root = resolve(worktreePath); const absolute = isAbsolute(filePath) ? resolve(filePath) : resolve(root, filePath); if (absolute !== root && !absolute.startsWith(root + sep)) { throw new Error(`path escapes worktree: ${filePath}`); } return absolute; } /** Resolve an ACP path against the agent worktree and read a slice of lines. */ export async function readWorktreeTextFile( worktreePath: string, filePath: string, line?: number | null, limit?: number | null, ): Promise { const absolute = resolveInWorktree(worktreePath, filePath); const raw = await fs.readFile(absolute, 'utf8'); if (!line && !limit) return raw; const lines = raw.split(/\r?\n/); const start = Math.max((line ?? 1) - 1, 0); const end = limit ? start + limit : undefined; return lines.slice(start, end).join('\n'); } /** Write a file inside the worktree (creates parent dirs). */ export async function writeWorktreeTextFile( worktreePath: string, filePath: string, content: string, ): Promise { const absolute = resolveInWorktree(worktreePath, filePath); await fs.mkdir(dirname(absolute), { recursive: true }); await fs.writeFile(absolute, content, 'utf8'); }