diff --git a/src/config/sessions/paths.test.ts b/src/config/sessions/paths.test.ts index 3ca4cdb9b2..baa45079bf 100644 --- a/src/config/sessions/paths.test.ts +++ b/src/config/sessions/paths.test.ts @@ -72,6 +72,42 @@ describe("session path safety", () => { expect(resolved).toBe(path.resolve(sessionsDir, "subdir/threaded-session.jsonl")); }); + it("accepts absolute sessionFile paths that resolve within the sessions dir", () => { + const sessionsDir = "/tmp/openclaw/agents/main/sessions"; + + const resolved = resolveSessionFilePath( + "sess-1", + { sessionFile: "/tmp/openclaw/agents/main/sessions/abc-123.jsonl" }, + { sessionsDir }, + ); + + expect(resolved).toBe(path.resolve(sessionsDir, "abc-123.jsonl")); + }); + + it("accepts absolute sessionFile with topic suffix within the sessions dir", () => { + const sessionsDir = "/tmp/openclaw/agents/main/sessions"; + + const resolved = resolveSessionFilePath( + "sess-1", + { sessionFile: "/tmp/openclaw/agents/main/sessions/abc-123-topic-42.jsonl" }, + { sessionsDir }, + ); + + expect(resolved).toBe(path.resolve(sessionsDir, "abc-123-topic-42.jsonl")); + }); + + it("rejects absolute sessionFile paths outside the sessions dir", () => { + const sessionsDir = "/tmp/openclaw/agents/main/sessions"; + + expect(() => + resolveSessionFilePath( + "sess-1", + { sessionFile: "/tmp/openclaw/agents/work/sessions/abc-123.jsonl" }, + { sessionsDir }, + ), + ).toThrow(/within sessions directory/); + }); + it("uses agent sessions dir fallback for transcript path", () => { const resolved = resolveSessionTranscriptPath("sess-1", "main"); expect(resolved.endsWith(path.join("agents", "main", "sessions", "sess-1.jsonl"))).toBe(true); diff --git a/src/config/sessions/paths.ts b/src/config/sessions/paths.ts index f123390a55..a630f68c2f 100644 --- a/src/config/sessions/paths.ts +++ b/src/config/sessions/paths.ts @@ -77,12 +77,14 @@ function resolvePathWithinSessionsDir(sessionsDir: string, candidate: string): s throw new Error("Session file path must not be empty"); } const resolvedBase = path.resolve(sessionsDir); - const resolvedCandidate = path.resolve(resolvedBase, trimmed); - const relative = path.relative(resolvedBase, resolvedCandidate); - if (relative.startsWith("..") || path.isAbsolute(relative)) { + // Normalize absolute paths that are within the sessions directory. + // Older versions stored absolute sessionFile paths in sessions.json; + // convert them to relative so the containment check passes. + const normalized = path.isAbsolute(trimmed) ? path.relative(resolvedBase, trimmed) : trimmed; + if (!normalized || normalized.startsWith("..") || path.isAbsolute(normalized)) { throw new Error("Session file path must be within sessions directory"); } - return resolvedCandidate; + return path.resolve(resolvedBase, normalized); } export function resolveSessionTranscriptPathInDir(