diff --git a/CHANGELOG.md b/CHANGELOG.md index 4836f7e2cc..7b879f2b9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai - Config: clamp `maxTokens` to `contextWindow` to prevent invalid model configs. (#5516) Thanks @lailoo. - Docs: fix language switcher ordering and Japanese locale flag in Mintlify nav. (#12023) Thanks @joshp123. - Paths: make internal path resolution respect `HOME`/`USERPROFILE` before `os.homedir()` across config, agents, sessions, pairing, cron, and CLI profiles. (#12091) Thanks @sebslight. +- Paths: structurally resolve `OPENCLAW_HOME`-derived home paths and fix Windows drive-letter handling in tool meta shortening. (#12125) Thanks @mcaxtr. - Thinking: allow xhigh for `github-copilot/gpt-5.2-codex` and `github-copilot/gpt-5.2`. (#11646) Thanks @seans-openclawbot. - Discord: support forum/media `thread create` starter messages, wire `message thread create --message`, and harden thread-create routing. (#10062) Thanks @jarvis89757. - Gateway: stabilize chat routing by canonicalizing node session keys for node-originated chat methods. (#11755) Thanks @mbelinky. diff --git a/src/agents/agent-scope.test.ts b/src/agents/agent-scope.test.ts index 167748aa7b..487c0a9492 100644 --- a/src/agents/agent-scope.test.ts +++ b/src/agents/agent-scope.test.ts @@ -1,3 +1,4 @@ +import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { @@ -212,14 +213,17 @@ describe("resolveAgentConfig", () => { vi.stubEnv("OPENCLAW_HOME", "/srv/openclaw-home"); const workspace = resolveAgentWorkspaceDir({} as OpenClawConfig, "main"); - expect(workspace).toBe("/srv/openclaw-home/.openclaw/workspace"); + expect(workspace).toBe(path.join(path.resolve("/srv/openclaw-home"), ".openclaw", "workspace")); }); // Unix-style paths behave differently on Windows; skip there it.skipIf(process.platform === "win32")("uses OPENCLAW_HOME for default agentDir", () => { vi.stubEnv("OPENCLAW_HOME", "/srv/openclaw-home"); + vi.stubEnv("OPENCLAW_STATE_DIR", ""); const agentDir = resolveAgentDir({} as OpenClawConfig, "main"); - expect(agentDir).toBe("/srv/openclaw-home/.openclaw/agents/main/agent"); + expect(agentDir).toBe( + path.join(path.resolve("/srv/openclaw-home"), ".openclaw", "agents", "main", "agent"), + ); }); }); diff --git a/src/agents/workspace.defaults.test.ts b/src/agents/workspace.defaults.test.ts index c6f698cd93..1c9ff7fff0 100644 --- a/src/agents/workspace.defaults.test.ts +++ b/src/agents/workspace.defaults.test.ts @@ -1,3 +1,4 @@ +import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; afterEach(() => { @@ -13,6 +14,8 @@ describe("DEFAULT_AGENT_WORKSPACE_DIR", () => { vi.resetModules(); const mod = await import("./workspace.js"); - expect(mod.DEFAULT_AGENT_WORKSPACE_DIR).toBe("/srv/openclaw-home/.openclaw/workspace"); + expect(mod.DEFAULT_AGENT_WORKSPACE_DIR).toBe( + path.join(path.resolve("/srv/openclaw-home"), ".openclaw", "workspace"), + ); }); }); diff --git a/src/agents/workspace.test.ts b/src/agents/workspace.test.ts index b09e62ed13..d4f842e6ea 100644 --- a/src/agents/workspace.test.ts +++ b/src/agents/workspace.test.ts @@ -15,7 +15,7 @@ describe("resolveDefaultAgentWorkspaceDir", () => { HOME: "/home/other", } as NodeJS.ProcessEnv); - expect(dir).toBe(path.join("/srv/openclaw-home", ".openclaw", "workspace")); + expect(dir).toBe(path.join(path.resolve("/srv/openclaw-home"), ".openclaw", "workspace")); }); }); diff --git a/src/auto-reply/tool-meta.test.ts b/src/auto-reply/tool-meta.test.ts index 293a340ea6..68994267b1 100644 --- a/src/auto-reply/tool-meta.test.ts +++ b/src/auto-reply/tool-meta.test.ts @@ -1,31 +1,35 @@ +import path from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { formatToolAggregate, formatToolPrefix, shortenMeta, shortenPath } from "./tool-meta.js"; +// Use path.resolve so inputs match the resolved HOME on every platform. +const home = path.resolve("/Users/test"); + describe("tool meta formatting", () => { beforeEach(() => { vi.unstubAllEnvs(); }); it("shortens paths under HOME", () => { - vi.stubEnv("HOME", "/Users/test"); - expect(shortenPath("/Users/test")).toBe("~"); - expect(shortenPath("/Users/test/a/b.txt")).toBe("~/a/b.txt"); + vi.stubEnv("HOME", home); + expect(shortenPath(home)).toBe("~"); + expect(shortenPath(`${home}/a/b.txt`)).toBe("~/a/b.txt"); expect(shortenPath("/opt/x")).toBe("/opt/x"); }); it("shortens meta strings with optional colon suffix", () => { - vi.stubEnv("HOME", "/Users/test"); - expect(shortenMeta("/Users/test/a.txt")).toBe("~/a.txt"); - expect(shortenMeta("/Users/test/a.txt:12")).toBe("~/a.txt:12"); - expect(shortenMeta("cd /Users/test/dir && ls")).toBe("cd ~/dir && ls"); + vi.stubEnv("HOME", home); + expect(shortenMeta(`${home}/a.txt`)).toBe("~/a.txt"); + expect(shortenMeta(`${home}/a.txt:12`)).toBe("~/a.txt:12"); + expect(shortenMeta(`cd ${home}/dir && ls`)).toBe("cd ~/dir && ls"); expect(shortenMeta("")).toBe(""); }); it("formats aggregates with grouping and brace-collapse", () => { - vi.stubEnv("HOME", "/Users/test"); + vi.stubEnv("HOME", home); const out = formatToolAggregate(" fs ", [ - "/Users/test/dir/a.txt", - "/Users/test/dir/b.txt", + `${home}/dir/a.txt`, + `${home}/dir/b.txt`, "note", "a→b", ]); @@ -36,22 +40,22 @@ describe("tool meta formatting", () => { }); it("wraps aggregate meta in backticks when markdown is enabled", () => { - vi.stubEnv("HOME", "/Users/test"); - const out = formatToolAggregate("fs", ["/Users/test/dir/a.txt"], { markdown: true }); + vi.stubEnv("HOME", home); + const out = formatToolAggregate("fs", [`${home}/dir/a.txt`], { markdown: true }); expect(out).toContain("`~/dir/a.txt`"); }); it("keeps exec flags outside markdown and moves them to the front", () => { - vi.stubEnv("HOME", "/Users/test"); - const out = formatToolAggregate("exec", ["cd /Users/test/dir && gemini 2>&1 · elevated"], { + vi.stubEnv("HOME", home); + const out = formatToolAggregate("exec", [`cd ${home}/dir && gemini 2>&1 · elevated`], { markdown: true, }); expect(out).toBe("🛠️ Exec: elevated · `cd ~/dir && gemini 2>&1`"); }); it("formats prefixes with default labels", () => { - vi.stubEnv("HOME", "/Users/test"); + vi.stubEnv("HOME", home); expect(formatToolPrefix(undefined, undefined)).toBe("🧩 Tool"); - expect(formatToolPrefix("x", "/Users/test/a.txt")).toBe("🧩 X: ~/a.txt"); + expect(formatToolPrefix("x", `${home}/a.txt`)).toBe("🧩 X: ~/a.txt"); }); }); diff --git a/src/auto-reply/tool-meta.ts b/src/auto-reply/tool-meta.ts index 4297741f81..ce929284ec 100644 --- a/src/auto-reply/tool-meta.ts +++ b/src/auto-reply/tool-meta.ts @@ -13,13 +13,7 @@ export function shortenMeta(meta: string): string { if (!meta) { return meta; } - const colonIdx = meta.indexOf(":"); - if (colonIdx === -1) { - return shortenHomeInString(meta); - } - const base = meta.slice(0, colonIdx); - const rest = meta.slice(colonIdx); - return `${shortenHomeInString(base)}${rest}`; + return shortenHomeInString(meta); } export function formatToolAggregate( diff --git a/src/cli/profile.test.ts b/src/cli/profile.test.ts index 812417b819..5c78eaa367 100644 --- a/src/cli/profile.test.ts +++ b/src/cli/profile.test.ts @@ -61,7 +61,7 @@ describe("applyCliProfileEnv", () => { env, homedir: () => "/home/peter", }); - const expectedStateDir = path.join("/home/peter", ".openclaw-dev"); + const expectedStateDir = path.join(path.resolve("/home/peter"), ".openclaw-dev"); expect(env.OPENCLAW_PROFILE).toBe("dev"); expect(env.OPENCLAW_STATE_DIR).toBe(expectedStateDir); expect(env.OPENCLAW_CONFIG_PATH).toBe(path.join(expectedStateDir, "openclaw.json")); @@ -94,9 +94,10 @@ describe("applyCliProfileEnv", () => { homedir: () => "/home/fallback", }); - expect(env.OPENCLAW_STATE_DIR).toBe(path.join("/srv/openclaw-home", ".openclaw-work")); + const resolvedHome = path.resolve("/srv/openclaw-home"); + expect(env.OPENCLAW_STATE_DIR).toBe(path.join(resolvedHome, ".openclaw-work")); expect(env.OPENCLAW_CONFIG_PATH).toBe( - path.join("/srv/openclaw-home", ".openclaw-work", "openclaw.json"), + path.join(resolvedHome, ".openclaw-work", "openclaw.json"), ); }); }); diff --git a/src/config/paths.test.ts b/src/config/paths.test.ts index 443bc2ab7e..fb956a10a7 100644 --- a/src/config/paths.test.ts +++ b/src/config/paths.test.ts @@ -49,10 +49,11 @@ describe("state + config path candidates", () => { OPENCLAW_HOME: "/srv/openclaw-home", } as NodeJS.ProcessEnv; - expect(resolveStateDir(env)).toBe(path.join("/srv/openclaw-home", ".openclaw")); + const resolvedHome = path.resolve("/srv/openclaw-home"); + expect(resolveStateDir(env)).toBe(path.join(resolvedHome, ".openclaw")); const candidates = resolveDefaultConfigCandidates(env); - expect(candidates[0]).toBe(path.join("/srv/openclaw-home", ".openclaw", "openclaw.json")); + expect(candidates[0]).toBe(path.join(resolvedHome, ".openclaw", "openclaw.json")); }); it("prefers OPENCLAW_HOME over HOME for default state/config locations", () => { @@ -61,32 +62,34 @@ describe("state + config path candidates", () => { HOME: "/home/other", } as NodeJS.ProcessEnv; - expect(resolveStateDir(env)).toBe(path.join("/srv/openclaw-home", ".openclaw")); + const resolvedHome = path.resolve("/srv/openclaw-home"); + expect(resolveStateDir(env)).toBe(path.join(resolvedHome, ".openclaw")); const candidates = resolveDefaultConfigCandidates(env); - expect(candidates[0]).toBe(path.join("/srv/openclaw-home", ".openclaw", "openclaw.json")); + expect(candidates[0]).toBe(path.join(resolvedHome, ".openclaw", "openclaw.json")); }); it("orders default config candidates in a stable order", () => { const home = "/home/test"; + const resolvedHome = path.resolve(home); const candidates = resolveDefaultConfigCandidates({} as NodeJS.ProcessEnv, () => home); const expected = [ - path.join(home, ".openclaw", "openclaw.json"), - path.join(home, ".openclaw", "clawdbot.json"), - path.join(home, ".openclaw", "moltbot.json"), - path.join(home, ".openclaw", "moldbot.json"), - path.join(home, ".clawdbot", "openclaw.json"), - path.join(home, ".clawdbot", "clawdbot.json"), - path.join(home, ".clawdbot", "moltbot.json"), - path.join(home, ".clawdbot", "moldbot.json"), - path.join(home, ".moltbot", "openclaw.json"), - path.join(home, ".moltbot", "clawdbot.json"), - path.join(home, ".moltbot", "moltbot.json"), - path.join(home, ".moltbot", "moldbot.json"), - path.join(home, ".moldbot", "openclaw.json"), - path.join(home, ".moldbot", "clawdbot.json"), - path.join(home, ".moldbot", "moltbot.json"), - path.join(home, ".moldbot", "moldbot.json"), + path.join(resolvedHome, ".openclaw", "openclaw.json"), + path.join(resolvedHome, ".openclaw", "clawdbot.json"), + path.join(resolvedHome, ".openclaw", "moltbot.json"), + path.join(resolvedHome, ".openclaw", "moldbot.json"), + path.join(resolvedHome, ".clawdbot", "openclaw.json"), + path.join(resolvedHome, ".clawdbot", "clawdbot.json"), + path.join(resolvedHome, ".clawdbot", "moltbot.json"), + path.join(resolvedHome, ".clawdbot", "moldbot.json"), + path.join(resolvedHome, ".moltbot", "openclaw.json"), + path.join(resolvedHome, ".moltbot", "clawdbot.json"), + path.join(resolvedHome, ".moltbot", "moltbot.json"), + path.join(resolvedHome, ".moltbot", "moldbot.json"), + path.join(resolvedHome, ".moldbot", "openclaw.json"), + path.join(resolvedHome, ".moldbot", "clawdbot.json"), + path.join(resolvedHome, ".moldbot", "moltbot.json"), + path.join(resolvedHome, ".moldbot", "moldbot.json"), ]; expect(candidates).toEqual(expected); }); diff --git a/src/gateway/session-utils.fs.test.ts b/src/gateway/session-utils.fs.test.ts index 83b49bb564..e465999b16 100644 --- a/src/gateway/session-utils.fs.test.ts +++ b/src/gateway/session-utils.fs.test.ts @@ -502,6 +502,8 @@ describe("resolveSessionTranscriptCandidates", () => { const candidates = resolveSessionTranscriptCandidates("sess-1", undefined); const fallback = candidates[candidates.length - 1]; - expect(fallback).toBe(path.join("/srv/openclaw-home", ".openclaw", "sessions", "sess-1.jsonl")); + expect(fallback).toBe( + path.join(path.resolve("/srv/openclaw-home"), ".openclaw", "sessions", "sess-1.jsonl"), + ); }); }); diff --git a/src/infra/home-dir.test.ts b/src/infra/home-dir.test.ts index b4cee701f6..f1f721cd7f 100644 --- a/src/infra/home-dir.test.ts +++ b/src/infra/home-dir.test.ts @@ -1,3 +1,4 @@ +import path from "node:path"; import { describe, expect, it } from "vitest"; import { expandHomePrefix, resolveEffectiveHomeDir, resolveRequiredHomeDir } from "./home-dir.js"; @@ -9,17 +10,21 @@ describe("resolveEffectiveHomeDir", () => { USERPROFILE: "C:/Users/other", } as NodeJS.ProcessEnv; - expect(resolveEffectiveHomeDir(env, () => "/fallback")).toBe("/srv/openclaw-home"); + expect(resolveEffectiveHomeDir(env, () => "/fallback")).toBe( + path.resolve("/srv/openclaw-home"), + ); }); it("falls back to HOME then USERPROFILE then homedir", () => { expect(resolveEffectiveHomeDir({ HOME: "/home/alice" } as NodeJS.ProcessEnv)).toBe( - "/home/alice", + path.resolve("/home/alice"), ); expect(resolveEffectiveHomeDir({ USERPROFILE: "C:/Users/alice" } as NodeJS.ProcessEnv)).toBe( - "C:/Users/alice", + path.resolve("C:/Users/alice"), + ); + expect(resolveEffectiveHomeDir({} as NodeJS.ProcessEnv, () => "/fallback")).toBe( + path.resolve("/fallback"), ); - expect(resolveEffectiveHomeDir({} as NodeJS.ProcessEnv, () => "/fallback")).toBe("/fallback"); }); it("expands OPENCLAW_HOME when set to ~", () => { @@ -28,7 +33,7 @@ describe("resolveEffectiveHomeDir", () => { HOME: "/home/alice", } as NodeJS.ProcessEnv; - expect(resolveEffectiveHomeDir(env)).toBe("/home/alice/svc"); + expect(resolveEffectiveHomeDir(env)).toBe(path.resolve("/home/alice/svc")); }); }); @@ -41,6 +46,14 @@ describe("resolveRequiredHomeDir", () => { ).toBe(process.cwd()); }); + it("returns a fully resolved path for OPENCLAW_HOME", () => { + const result = resolveRequiredHomeDir( + { OPENCLAW_HOME: "/custom/home" } as NodeJS.ProcessEnv, + () => "/fallback", + ); + expect(result).toBe(path.resolve("/custom/home")); + }); + it("returns cwd when OPENCLAW_HOME is tilde-only and no fallback home exists", () => { expect( resolveRequiredHomeDir({ OPENCLAW_HOME: "~" } as NodeJS.ProcessEnv, () => { @@ -55,7 +68,7 @@ describe("expandHomePrefix", () => { const value = expandHomePrefix("~/x", { env: { OPENCLAW_HOME: "/srv/openclaw-home" } as NodeJS.ProcessEnv, }); - expect(value).toBe("/srv/openclaw-home/x"); + expect(value).toBe(`${path.resolve("/srv/openclaw-home")}/x`); }); it("keeps non-tilde values unchanged", () => { diff --git a/src/infra/home-dir.ts b/src/infra/home-dir.ts index c698dd19b3..7dd2bbdd1e 100644 --- a/src/infra/home-dir.ts +++ b/src/infra/home-dir.ts @@ -1,4 +1,5 @@ import os from "node:os"; +import path from "node:path"; function normalize(value: string | undefined): string | undefined { const trimmed = value?.trim(); @@ -9,6 +10,11 @@ export function resolveEffectiveHomeDir( env: NodeJS.ProcessEnv = process.env, homedir: () => string = os.homedir, ): string | undefined { + const raw = resolveRawHomeDir(env, homedir); + return raw ? path.resolve(raw) : undefined; +} + +function resolveRawHomeDir(env: NodeJS.ProcessEnv, homedir: () => string): string | undefined { const explicitHome = normalize(env.OPENCLAW_HOME); if (explicitHome) { if (explicitHome === "~" || explicitHome.startsWith("~/") || explicitHome.startsWith("~\\")) { @@ -47,7 +53,7 @@ export function resolveRequiredHomeDir( env: NodeJS.ProcessEnv = process.env, homedir: () => string = os.homedir, ): string { - return resolveEffectiveHomeDir(env, homedir) ?? process.cwd(); + return resolveEffectiveHomeDir(env, homedir) ?? path.resolve(process.cwd()); } export function expandHomePrefix( diff --git a/src/utils.test.ts b/src/utils.test.ts index 9bf6e00cff..163470090f 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -142,7 +142,7 @@ describe("resolveHomeDir", () => { vi.stubEnv("OPENCLAW_HOME", "/srv/openclaw-home"); vi.stubEnv("HOME", "/home/other"); - expect(resolveHomeDir()).toBe("/srv/openclaw-home"); + expect(resolveHomeDir()).toBe(path.resolve("/srv/openclaw-home")); vi.unstubAllEnvs(); }); @@ -153,7 +153,7 @@ describe("shortenHomePath", () => { vi.stubEnv("OPENCLAW_HOME", "/srv/openclaw-home"); vi.stubEnv("HOME", "/home/other"); - expect(shortenHomePath("/srv/openclaw-home/.openclaw/openclaw.json")).toBe( + expect(shortenHomePath(`${path.resolve("/srv/openclaw-home")}/.openclaw/openclaw.json`)).toBe( "$OPENCLAW_HOME/.openclaw/openclaw.json", ); @@ -166,9 +166,9 @@ describe("shortenHomeInString", () => { vi.stubEnv("OPENCLAW_HOME", "/srv/openclaw-home"); vi.stubEnv("HOME", "/home/other"); - expect(shortenHomeInString("config: /srv/openclaw-home/.openclaw/openclaw.json")).toBe( - "config: $OPENCLAW_HOME/.openclaw/openclaw.json", - ); + expect( + shortenHomeInString(`config: ${path.resolve("/srv/openclaw-home")}/.openclaw/openclaw.json`), + ).toBe("config: $OPENCLAW_HOME/.openclaw/openclaw.json"); vi.unstubAllEnvs(); });