import { constants } from "node:fs"; import { open } from "node:fs/promises"; import { createHash, randomUUID } from "node:crypto"; import type { CallToolResult } from "@modelcontextprotocol/client"; import { config } from "../config.js"; import { isManagedConversationWorkspace, resolveConversationWorkspace, SANDBOX_UID, SANDBOX_GID } from "../runtime/conversationWorkspace.js"; import type { RuntimeSessionContext } from "../runtime/sessionContext.js"; const formats = new Map([ ["application/zip", ".zip"], ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xlsx"], ]); export const artifactFilename = /^mcp-[a-f0-9-]{36}\.(zip|xlsx)$/; export async function openArtifactDirectory(workspace: string, sessionId: string) { const canonical = await resolveConversationWorkspace(workspace, config.CONVERSATION_WORKSPACE_ROOT); if (!isManagedConversationWorkspace(canonical, config.CONVERSATION_WORKSPACE_ROOT, sessionId)) throw new Error("Export workspace owner mismatch"); const root = await open(canonical, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW); try { return await open(`/proc/self/fd/${root.fd}/files`, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW); } finally { await root.close(); } } export async function saveMcpArtifact(result: CallToolResult, context: RuntimeSessionContext, workspace: string): Promise { if (result.isError) return result; const output = result.structuredContent as Record | undefined; const data = output?.data as Record | undefined; const file = data?.file as Record | undefined; if (!file) throw new Error("Missing export file"); const extension = formats.get(String(file.mime_type)); if (!extension || file.encoding !== "base64" || typeof file.data !== "string" || file.data.length > 7 * 1024 * 1024) throw new Error("Invalid export format"); const bytes = Buffer.from(file.data, "base64"); if (!bytes.length || bytes.length > 5 * 1024 * 1024 || bytes.length !== file.size || bytes.toString("base64") !== file.data || createHash("sha256").update(bytes).digest("hex") !== file.sha256) throw new Error("Export integrity check failed"); const filename = `mcp-${randomUUID()}${extension}`; const directory = await openArtifactDirectory(workspace, context.sessionId); try { const path = `/proc/self/fd/${directory.fd}/${filename}`; const destination = await open(path, 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(); } } finally { await directory.close(); } const url = `/api/v1/session/${encodeURIComponent(context.clientSessionId)}/files/${filename}`; const next = { ...output, data: { file: { name: file.name, mime_type: file.mime_type, size: bytes.length, sha256: file.sha256, relative_path: `files/${filename}`, download_url: url, markdown: `[下载 ${extension === ".zip" ? "模型文件" : "监测点清单"}](${url})`, } } }; return { ...result, structuredContent: next, content: [{ type: "text", text: JSON.stringify(next) }] }; }