From 90774c098ade256999a012ca32e1f6d12db22514 Mon Sep 17 00:00:00 2001 From: Xinhua Gu Date: Mon, 16 Feb 2026 12:42:57 +0100 Subject: [PATCH] fix(sessions): allow cross-agent session file paths in multi-agent setups When OPENCLAW_STATE_DIR changes between session creation and resolution (e.g., after reinstall or config change), absolute session file paths pointing to other agents' sessions directories were rejected even though they structurally match the valid .../agents//sessions/... pattern. The existing fallback logic in resolvePathWithinSessionsDir extracts the agent ID from the path and tries to resolve it via the current env's state directory. When those directories differ, the containment check fails. Now, if the path structurally matches the agent sessions pattern (validated by extractAgentIdFromAbsoluteSessionPath), we accept it directly as a final fallback. Fixes #15410, Fixes #15565, Fixes #15468 --- src/config/sessions.test.ts | 56 ++++++++++++++++++++++++++++++++++++ src/config/sessions/paths.ts | 5 ++++ 2 files changed, 61 insertions(+) diff --git a/src/config/sessions.test.ts b/src/config/sessions.test.ts index 4bce24426a4..8711f8da107 100644 --- a/src/config/sessions.test.ts +++ b/src/config/sessions.test.ts @@ -469,6 +469,62 @@ describe("sessions", () => { } }); + it("resolves cross-agent absolute sessionFile paths", () => { + const prev = process.env.OPENCLAW_STATE_DIR; + process.env.OPENCLAW_STATE_DIR = "/home/user/.openclaw"; + try { + // Agent bot1 resolves a sessionFile that belongs to agent bot2 + const sessionFile = resolveSessionFilePath( + "sess-1", + { sessionFile: "/home/user/.openclaw/agents/bot2/sessions/sess-1.jsonl" }, + { agentId: "bot1" }, + ); + expect(sessionFile).toBe("/home/user/.openclaw/agents/bot2/sessions/sess-1.jsonl"); + } finally { + if (prev === undefined) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = prev; + } + } + }); + + it("resolves cross-agent paths when OPENCLAW_STATE_DIR differs from stored paths", () => { + const prev = process.env.OPENCLAW_STATE_DIR; + process.env.OPENCLAW_STATE_DIR = "/different/state"; + try { + // sessionFile was created under a different state dir than current env + const sessionFile = resolveSessionFilePath( + "sess-1", + { sessionFile: "/original/state/agents/bot2/sessions/sess-1.jsonl" }, + { agentId: "bot1" }, + ); + expect(sessionFile).toBe("/original/state/agents/bot2/sessions/sess-1.jsonl"); + } finally { + if (prev === undefined) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = prev; + } + } + }); + + it("rejects absolute sessionFile paths outside agent sessions directories", () => { + const prev = process.env.OPENCLAW_STATE_DIR; + process.env.OPENCLAW_STATE_DIR = "/home/user/.openclaw"; + try { + expect(() => + resolveSessionFilePath("sess-1", { sessionFile: "/etc/passwd" }, { agentId: "bot1" }), + ).toThrow(/within sessions directory/); + } finally { + if (prev === undefined) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = prev; + } + } + }); + it("updateSessionStoreEntry merges concurrent patches", async () => { const mainSessionKey = "agent:main:main"; const dir = await createCaseDir("updateSessionStoreEntry"); diff --git a/src/config/sessions/paths.ts b/src/config/sessions/paths.ts index 9d5ea3b06bb..6144bd599b1 100644 --- a/src/config/sessions/paths.ts +++ b/src/config/sessions/paths.ts @@ -152,6 +152,11 @@ function resolvePathWithinSessionsDir( if (resolvedFromPath) { return resolvedFromPath; } + // The path structurally matches .../agents//sessions/... + // Accept it even if the root directory differs from the current env + // (e.g., OPENCLAW_STATE_DIR changed between session creation and resolution). + // The structural pattern provides sufficient containment guarantees. + return path.resolve(trimmed); } } if (!normalized || normalized.startsWith("..") || path.isAbsolute(normalized)) {