{"record":{"id":"8e3c157f8cf8e490","repo":"CherryHQ/cherry-studio","slug":"agent-memory-file-must-be-a-real-file-exact","errorCode":null,"errorMessage":"Agent memory file must be a real file: ${exact}","messagePattern":"Agent memory file must be a real file: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/mcp/servers/agentMemory.ts","lineNumber":29,"sourceCode":"import type { Tool } from '@modelcontextprotocol/sdk/types.js'\nimport { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js'\n\nconst logger = loggerService.withContext('McpServer:AgentMemory')\n\nfunction withNoFollow(flags: number): number {\n  return isWin ? flags : flags | constants.O_NOFOLLOW\n}\n\n/**\n * Resolve a filename within a directory using case-insensitive matching.\n * Returns the full path if found (preferring exact match), or the canonical path as fallback.\n */\nasync function resolveFileCI(dir: string, name: string): Promise<string> {\n  const exact = path.join(dir, name)\n  try {\n    const fileStat = await lstat(exact)\n    if (!fileStat.isFile() || fileStat.isSymbolicLink()) {\n      throw new Error(`Agent memory file must be a real file: ${exact}`)\n    }\n    return exact\n  } catch (err) {\n    if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err\n    // exact match not found, try case-insensitive\n  }\n\n  try {\n    const entries = await readdir(dir)\n    const target = name.toLowerCase()\n    const match = entries.find((e) => e.toLowerCase() === target)\n    if (!match) return exact\n    const matchedPath = path.join(dir, match)\n    const fileStat = await lstat(matchedPath)\n    if (!fileStat.isFile() || fileStat.isSymbolicLink()) {\n      throw new Error(`Agent memory file must be a real file: ${matchedPath}`)\n    }\n    return matchedPath","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/mcp/servers/agentMemory.ts#L11-L47","documentation":"resolveFileCI() resolves a filename inside the agent memory directory, preferring an exact case match. After lstat succeeds on the exact path, it verifies the entry is a regular file and NOT a symlink. This is a security invariant: agent memory files (FACT.md, JOURNAL.jsonl) must be real files to prevent symlink-based path traversal or replacement attacks.","triggerScenarios":"Calling any memory tool action when a non-regular file (directory, symlink, FIFO, device node) exists at <agentDataPath>/memory/FACT.md or memory/JOURNAL.jsonl with the exact casing. lstat succeeds but isFile() is false or isSymbolicLink() is true.","commonSituations":"A user or another process created a symlink at FACT.md pointing elsewhere; the memory directory was corrupted by a partial filesystem restore; a directory named FACT.md was accidentally created; an editor or sync tool (Dropbox, OneDrive) replaced the file with a symlink.","solutions":["Inspect the offending path with ls -la <agentDataPath>/memory/ and remove or replace the non-regular entry.","If a symlink is intentional, replace it with a real file copy — symlinks are deliberately rejected here.","Restore FACT.md / JOURNAL.jsonl from a backup as regular files."],"exampleFix":"# before: FACT.md is a symlink\nls -la memory/FACT.md  # lrwxrwxrwx ... FACT.md -> /tmp/evil\n\n# after: replace with a real file\nrm memory/FACT.md && touch memory/FACT.md","handlingStrategy":"validation","validationCode":"import { lstat } from 'node:fs/promises'\n\nasync function isSafeRegularFile(p: string): Promise<boolean> {\n  try {\n    const s = await lstat(p)\n    return s.isFile() && !s.isSymbolicLink()\n  } catch {\n    return false\n  }\n}\n// Before resolving a memory file path:\nif (await isSafeRegularFile(path.join(memoryDir, 'FACT.md'))) { /* safe to proceed */ }","typeGuard":"function isRegularFile(stat: import('node:fs').Stats): boolean {\n  return stat.isFile() && !stat.isSymbolicLink()\n}","tryCatchPattern":null,"preventionTips":["Never store agent memory files as symlinks; the guards deliberately reject them.","If you programmatically create FACT.md/JOURNAL.jsonl, use fs.writeFile (which creates regular files) rather than symlink-based indirection.","Educate users not to place the userData directory under symlink-heavy sync tools."],"tags":["security","filesystem","symlink-guard","agent-memory"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}