diff --git a/src/filesystem/path-validation.ts b/src/filesystem/path-validation.ts index 972e9c49d0..0c9cbd84d3 100644 --- a/src/filesystem/path-validation.ts +++ b/src/filesystem/path-validation.ts @@ -1,4 +1,4 @@ -import path from 'path'; +import path from 'path'; /** * Checks if an absolute path is within any of the allowed directories. @@ -61,26 +61,23 @@ export function isPathWithinAllowedDirectories(absolutePath: string, allowedDire throw new Error('Allowed directories must be absolute paths after normalization'); } - // Check if normalizedPath is within normalizedDir - // Path is inside if it's the same or a subdirectory - if (normalizedPath === normalizedDir) { + // Windows file systems are case-insensitive: compare drive letters and + // UNC shares case-insensitively before the prefix containment check. + const ci = path.sep === '\\' ? (s: string) => s.toLowerCase() : (s: string) => s; + + if (ci(normalizedPath) === ci(normalizedDir)) { return true; } - - // Special case for root directory to avoid double slash - // On Windows, we need to check if both paths are on the same drive + if (normalizedDir === path.sep) { return normalizedPath.startsWith(path.sep); } - - // On Windows, also check for drive root (e.g., "C:\") + if (path.sep === '\\' && normalizedDir.match(/^[A-Za-z]:\\?$/)) { - // Ensure both paths are on the same drive - const dirDrive = normalizedDir.charAt(0).toLowerCase(); - const pathDrive = normalizedPath.charAt(0).toLowerCase(); - return pathDrive === dirDrive && normalizedPath.startsWith(normalizedDir.replace(/\\?$/, '\\')); + return ci(normalizedDir.charAt(0)) === ci(normalizedPath.charAt(0)) + && ci(normalizedPath).startsWith(ci(normalizedDir.replace(/\\?$/, '\\'))); } - - return normalizedPath.startsWith(normalizedDir + path.sep); + + return ci(normalizedPath).startsWith(ci(normalizedDir) + path.sep); }); -} +} \ No newline at end of file diff --git a/src/memory/index.ts b/src/memory/index.ts index 9865c5318e..930c6cddc6 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -70,10 +70,20 @@ export class KnowledgeGraphManager { constructor(private memoryFilePath: string) {} private async loadGraph(): Promise { + let data: string; try { - const data = await fs.readFile(this.memoryFilePath, "utf-8"); - const lines = data.split("\n").filter(line => line.trim() !== ""); - return lines.reduce((graph: KnowledgeGraph, line) => { + data = await fs.readFile(this.memoryFilePath, "utf-8"); + } catch (error) { + if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") { + return { entities: [], relations: [] }; + } + throw error; + } + + const lines = data.split("\n").filter(line => line.trim() !== ""); + const graph: KnowledgeGraph = { entities: [], relations: [] }; + for (const line of lines) { + try { const item = JSON.parse(line); if (item.type === "entity") { graph.entities.push({ @@ -81,22 +91,20 @@ export class KnowledgeGraphManager { entityType: item.entityType, observations: item.observations }); - } - if (item.type === "relation") { + } else if (item.type === "relation") { graph.relations.push({ from: item.from, to: item.to, relationType: item.relationType }); } - return graph; - }, { entities: [], relations: [] }); - } catch (error) { - if (error instanceof Error && 'code' in error && (error as any).code === "ENOENT") { - return { entities: [], relations: [] }; + } catch { + // Skip malformed lines (e.g. truncated by a crash mid-write) instead + // of crashing the whole server. Surfaces a warning on stderr. + console.error(`Skipping malformed memory line: ${line.slice(0, 120)}`); } - throw error; } + return graph; } private async saveGraph(graph: KnowledgeGraph): Promise { @@ -114,7 +122,11 @@ export class KnowledgeGraphManager { relationType: r.relationType })), ]; - await fs.writeFile(this.memoryFilePath, lines.join("\n")); + // Atomic replace: write to a pid-suffixed temp file then rename so a + // crash mid-write can never leave a truncated memory file behind. + const tmpPath = `${this.memoryFilePath}.${process.pid}.tmp`; + await fs.writeFile(tmpPath, lines.join("\n")); + await fs.rename(tmpPath, this.memoryFilePath); } async createEntities(entities: Entity[]): Promise {