import { randomUUID } from "node:crypto"; import { lstat, open, realpath, rename, rm } from "node:fs/promises"; import { isAbsolute, join, parse, relative, resolve, sep } from "node:path"; import { HarnessError } from "../errors/harness-error.js"; const protectedSegments = new Set([ ".env", ".git", ".ssh", "id_rsa", "id_ed25519", ]); export type ReportDraftInput = { title?: string; body: string; }; export class SafeWorkspace { private constructor(public readonly root: string) {} public static async create(root: string): Promise { const canonicalRoot = await realpath(resolve(root)); if (canonicalRoot === parse(canonicalRoot).root) { throw new HarnessError("UNSAFE_WORKSPACE_ROOT", "Filesystem root cannot be used as a workspace"); } const metadata = await lstat(canonicalRoot); if (!metadata.isDirectory() || metadata.isSymbolicLink()) { throw new HarnessError("UNSAFE_WORKSPACE_ROOT", "Workspace root must be a regular directory"); } return new SafeWorkspace(canonicalRoot); } public async resolveReadable(inputPath: string): Promise { const canonical = await this.resolveExisting(inputPath); const metadata = await lstat(canonical); if (!metadata.isFile()) { throw new HarnessError("NOT_A_REGULAR_FILE", "Only regular files may be read"); } return canonical; } public async resolveExisting(inputPath: string): Promise { const candidate = isAbsolute(inputPath) ? resolve(inputPath) : resolve(this.root, inputPath); this.#assertContained(candidate); this.#assertUnprotected(candidate); await this.#assertNoSymlinkSegments(candidate); const canonical = await realpath(candidate); this.#assertContained(canonical); return canonical; } public async writeReportDraft(input: ReportDraftInput): Promise<{ relativePath: "report.md"; byteSize: number }> { if (input.body.trim().length === 0) { throw new HarnessError("REPORT_BODY_REQUIRED", "Report body cannot be empty"); } const target = join(this.root, "report.md"); const temporary = join(this.root, `.report-${randomUUID()}.tmp`); this.#assertContained(target); await this.#assertNoSymlinkSegments(this.root); const title = input.title?.trim(); const content = title === undefined || title.length === 0 ? `${input.body.trim()}\n` : `# ${title}\n\n${input.body.trim()}\n`; const bytes = Buffer.from(content, "utf8"); let handle: Awaited> | undefined; try { handle = await open(temporary, "wx", 0o600); await handle.writeFile(bytes); await handle.sync(); await handle.close(); handle = undefined; try { const existing = await lstat(target); if (existing.isSymbolicLink()) { throw new HarnessError("SYMLINK_NOT_ALLOWED", "Report target cannot be a symbolic link"); } } catch (error) { if (error instanceof HarnessError) throw error; if (!isMissingFileError(error)) throw error; } await rename(temporary, target); return { relativePath: "report.md", byteSize: bytes.byteLength }; } finally { await handle?.close().catch(() => undefined); await rm(temporary, { force: true }).catch(() => undefined); } } public async remove(): Promise { const metadata = await lstat(this.root).catch((error: unknown) => { if (isMissingFileError(error)) return undefined; throw error; }); if (metadata === undefined) return; if (!metadata.isDirectory() || metadata.isSymbolicLink()) { throw new HarnessError("UNSAFE_WORKSPACE_ROOT", "Refusing to remove a non-directory workspace root"); } await rm(this.root, { recursive: true, force: false }); } #assertContained(candidate: string): void { const pathFromRoot = relative(this.root, candidate); if (pathFromRoot === "") return; if (pathFromRoot === ".." || pathFromRoot.startsWith(`..${sep}`) || isAbsolute(pathFromRoot)) { throw new HarnessError("PATH_OUTSIDE_WORKSPACE", "Path is outside the Session workspace"); } } #assertUnprotected(candidate: string): void { const pathFromRoot = relative(this.root, candidate); const segments = pathFromRoot.split(sep); if (segments.some((segment) => protectedSegments.has(segment.toLowerCase()))) { throw new HarnessError("PROTECTED_PATH", "Protected paths cannot be accessed"); } } async #assertNoSymlinkSegments(candidate: string): Promise { this.#assertContained(candidate); const pathFromRoot = relative(this.root, candidate); if (pathFromRoot === "") { const rootMetadata = await lstat(this.root); if (rootMetadata.isSymbolicLink()) { throw new HarnessError("SYMLINK_NOT_ALLOWED", "Symbolic links are not allowed"); } return; } let current = this.root; for (const segment of pathFromRoot.split(sep)) { current = join(current, segment); const metadata = await lstat(current); if (metadata.isSymbolicLink()) { throw new HarnessError("SYMLINK_NOT_ALLOWED", "Symbolic links are not allowed"); } } } } function isMissingFileError(error: unknown): boolean { return error instanceof Error && "code" in error && error.code === "ENOENT"; }