import { constants } from "node:fs"; import { open, realpath } from "node:fs/promises"; import { createHash, randomUUID } from "node:crypto"; import { homedir } from "node:os"; import { basename, dirname, join, resolve } from "node:path"; import type { OpencodeRuntimeAdapter } from "../runtime/opencode.js"; import { SANDBOX_GID, SANDBOX_UID } from "../runtime/conversationWorkspace.js"; import { openArtifactDirectory } from "../mcp/artifact-files.js"; export const nativeToolOutputDirectory = () => resolve( process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), "opencode", "tool-output", ); // Only native truncation metadata from this session authorizes a source file. // Text supplied by the model or a tool response is never an ownership proof. export async function stageSessionToolOutputs( command: string, workspace: string, sessionId: string, messages: Awaited>, ) { const sources = new Set(); const root = nativeToolOutputDirectory(); for (const message of messages) for (const part of message.parts) { if (part.type !== "tool" || part.state.status !== "completed") continue; const metadata = part.state.metadata; const path = metadata?.outputPath; if (metadata?.truncated === true && typeof path === "string" && command.includes(path) && dirname(path) === root && /^tool_[a-zA-Z0-9]+$/.test(basename(path))) sources.add(path); } if (!sources.size) return []; const canonicalRoot = await realpath(root); const directory = await openArtifactDirectory(workspace, sessionId); const files: Array<{ source_path: string; relative_path: string; shell_path: string; size: number; sha256: string }> = []; let remaining = 128 * 1024 * 1024; try { for (const path of sources) { if (await realpath(path) !== join(canonicalRoot, basename(path))) throw new Error("Unsafe tool output path"); const source = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW); let bytes: Buffer; try { const stat = await source.stat(); if (!stat.isFile() || stat.size > remaining) throw new Error("Tool output staging budget exceeded"); bytes = await source.readFile(); if (bytes.length > remaining) throw new Error("Tool output staging budget exceeded"); remaining -= bytes.length; } finally { await source.close(); } const filename = `tool-output-${randomUUID()}.txt`; const destination = await open(`/proc/self/fd/${directory.fd}/${filename}`, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600); try { await destination.writeFile(bytes); if (typeof process.getuid === "function" && process.getuid() === 0) await destination.chown(SANDBOX_UID, SANDBOX_GID); } finally { await destination.close(); } files.push({ source_path: path, relative_path: `files/${filename}`, shell_path: filename, size: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") }); } } finally { await directory.close(); } return files; }