mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(clawhub): read Windows auth config from AppData (#124658)
* fix(clawhub): read Windows auth config from AppData * fix(infra): block workspace APPDATA overrides * fix(clawhub): preserve canonical config precedence --------- Co-authored-by: Josh Lehman <550978+jalehman@users.noreply.github.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { withTestDir } from "../test-helpers/temp-dir.js";
|
||||
import { captureEnv, setTestEnvValue } from "../test-utils/env.js";
|
||||
import { captureEnv, deleteTestEnvValue, setTestEnvValue } from "../test-utils/env.js";
|
||||
import {
|
||||
fetchClawHubSkillInstallResolution,
|
||||
fetchClawHubSkillSecurityVerdicts,
|
||||
@@ -51,14 +51,15 @@ function malformedUtf8(prefix: string, suffix: string): ArrayBuffer {
|
||||
}
|
||||
|
||||
describe("clawhub client", () => {
|
||||
const originalEnv = captureEnv(["HOME", "XDG_CONFIG_HOME"]);
|
||||
const originalEnv = captureEnv(["APPDATA", "HOME", "XDG_CONFIG_HOME"]);
|
||||
|
||||
async function expectSearchUsesAuthToken(expectedToken: string): Promise<void> {
|
||||
async function searchAuthorizationHeader(): Promise<string | null> {
|
||||
let authorization: string | null = null;
|
||||
await expect(
|
||||
searchClawHubSkills({
|
||||
query: "calendar",
|
||||
fetchImpl: async (_input, init) => {
|
||||
expect(new Headers(init?.headers).get("Authorization")).toBe(`Bearer ${expectedToken}`);
|
||||
authorization = new Headers(init?.headers).get("Authorization");
|
||||
return new Response(JSON.stringify({ results: [] }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
@@ -66,6 +67,11 @@ describe("clawhub client", () => {
|
||||
},
|
||||
}),
|
||||
).resolves.toStrictEqual([]);
|
||||
return authorization;
|
||||
}
|
||||
|
||||
async function expectSearchUsesAuthToken(expectedToken: string): Promise<void> {
|
||||
await expect(searchAuthorizationHeader()).resolves.toBe(`Bearer ${expectedToken}`);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
@@ -104,6 +110,90 @@ describe("clawhub client", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["clawhub", "clawdhub"])(
|
||||
"loads ClawHub request auth from the Windows AppData %s config path",
|
||||
async (configDirectory) => {
|
||||
await withTestDir({ prefix: "openclaw-clawhub-appdata-" }, async (appDataRoot) => {
|
||||
const configPath = path.join(appDataRoot, configDirectory, "config.json");
|
||||
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
setTestEnvValue("APPDATA", appDataRoot);
|
||||
deleteTestEnvValue("XDG_CONFIG_HOME");
|
||||
try {
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
JSON.stringify({ token: "fixture-appdata-token" }),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
await expectSearchUsesAuthToken("fixture-appdata-token");
|
||||
} finally {
|
||||
platformSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps XDG_CONFIG_HOME ahead of AppData on Windows", async () => {
|
||||
await withTestDir({ prefix: "openclaw-clawhub-appdata-" }, async (appDataRoot) => {
|
||||
await withTestDir({ prefix: "openclaw-clawhub-xdg-" }, async (xdgRoot) => {
|
||||
const appDataConfigPath = path.join(appDataRoot, "clawhub", "config.json");
|
||||
const xdgConfigPath = path.join(xdgRoot, "clawhub", "config.json");
|
||||
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
setTestEnvValue("APPDATA", appDataRoot);
|
||||
setTestEnvValue("XDG_CONFIG_HOME", xdgRoot);
|
||||
try {
|
||||
await Promise.all([
|
||||
fs.mkdir(path.dirname(appDataConfigPath), { recursive: true }),
|
||||
fs.mkdir(path.dirname(xdgConfigPath), { recursive: true }),
|
||||
]);
|
||||
await Promise.all([
|
||||
fs.writeFile(
|
||||
appDataConfigPath,
|
||||
JSON.stringify({ token: "stale-appdata-token" }),
|
||||
"utf8",
|
||||
),
|
||||
fs.writeFile(xdgConfigPath, JSON.stringify({ token: "fixture-xdg-token" }), "utf8"),
|
||||
]);
|
||||
|
||||
await expectSearchUsesAuthToken("fixture-xdg-token");
|
||||
} finally {
|
||||
platformSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["without a token", JSON.stringify({})],
|
||||
["with malformed JSON", "{"],
|
||||
])(
|
||||
"does not fall back to a legacy token when the canonical config exists %s",
|
||||
async (_, contents) => {
|
||||
await withTestDir({ prefix: "openclaw-clawhub-appdata-" }, async (appDataRoot) => {
|
||||
const canonicalConfigPath = path.join(appDataRoot, "clawhub", "config.json");
|
||||
const legacyConfigPath = path.join(appDataRoot, "clawdhub", "config.json");
|
||||
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
setTestEnvValue("APPDATA", appDataRoot);
|
||||
deleteTestEnvValue("XDG_CONFIG_HOME");
|
||||
try {
|
||||
await Promise.all([
|
||||
fs.mkdir(path.dirname(canonicalConfigPath), { recursive: true }),
|
||||
fs.mkdir(path.dirname(legacyConfigPath), { recursive: true }),
|
||||
]);
|
||||
await Promise.all([
|
||||
fs.writeFile(canonicalConfigPath, contents, "utf8"),
|
||||
fs.writeFile(legacyConfigPath, JSON.stringify({ token: "stale-legacy-token" }), "utf8"),
|
||||
]);
|
||||
|
||||
await expect(searchAuthorizationHeader()).resolves.toBeNull();
|
||||
} finally {
|
||||
platformSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform === "darwin")(
|
||||
"loads ClawHub request auth from the macOS Application Support path",
|
||||
async () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { retryClawHubRead } from "./clawhub-retry.js";
|
||||
import { isTruthyEnvValue } from "./env.js";
|
||||
import { isErrno } from "./errno.js";
|
||||
import { readResponseTextSnippet, readResponseWithLimit } from "./http-body.js";
|
||||
|
||||
const DEFAULT_CLAWHUB_URL = "https://clawhub.ai";
|
||||
@@ -114,6 +115,12 @@ function extractTokenFromClawHubConfig(value: unknown): string | undefined {
|
||||
);
|
||||
}
|
||||
|
||||
function resolveClawHubConfigPathsIn(configHome: string): string[] {
|
||||
return ["clawhub", "clawdhub"].map((directory) =>
|
||||
path.join(configHome, directory, "config.json"),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveClawHubConfigPaths(): string[] {
|
||||
const explicit =
|
||||
normalizeOptionalString(process.env.CLAWHUB_CONFIG_PATH) ||
|
||||
@@ -125,16 +132,21 @@ function resolveClawHubConfigPaths(): string[] {
|
||||
const xdgConfigHome = normalizeOptionalString(process.env.XDG_CONFIG_HOME);
|
||||
const configHome =
|
||||
xdgConfigHome && xdgConfigHome.length > 0 ? xdgConfigHome : path.join(os.homedir(), ".config");
|
||||
const xdgPath = path.join(configHome, "clawhub", "config.json");
|
||||
const configPaths = resolveClawHubConfigPathsIn(configHome);
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
return [
|
||||
path.join(os.homedir(), "Library", "Application Support", "clawhub", "config.json"),
|
||||
xdgPath,
|
||||
...resolveClawHubConfigPathsIn(path.join(os.homedir(), "Library", "Application Support")),
|
||||
...configPaths,
|
||||
];
|
||||
}
|
||||
|
||||
return [xdgPath];
|
||||
const appData = normalizeOptionalString(process.env.APPDATA);
|
||||
if (process.platform === "win32" && !xdgConfigHome && appData) {
|
||||
return [...resolveClawHubConfigPathsIn(appData), ...configPaths];
|
||||
}
|
||||
|
||||
return configPaths;
|
||||
}
|
||||
|
||||
export async function resolveClawHubAuthToken(): Promise<string | undefined> {
|
||||
@@ -148,12 +160,11 @@ export async function resolveClawHubAuthToken(): Promise<string | undefined> {
|
||||
for (const configPath of resolveClawHubConfigPaths()) {
|
||||
try {
|
||||
const raw = await fs.readFile(configPath, "utf8");
|
||||
const token = extractTokenFromClawHubConfig(JSON.parse(raw));
|
||||
if (token) {
|
||||
return token;
|
||||
return extractTokenFromClawHubConfig(JSON.parse(raw));
|
||||
} catch (error) {
|
||||
if (!isErrno(error) || error.code !== "ENOENT") {
|
||||
return undefined;
|
||||
}
|
||||
} catch {
|
||||
// Try the next candidate path.
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@@ -194,6 +194,7 @@ describe("workspace .env blocklist completeness", () => {
|
||||
"HOMEBREW_BREW_FILE",
|
||||
"HOMEBREW_PREFIX",
|
||||
"IRC_HOST",
|
||||
"APPDATA",
|
||||
"LOCALAPPDATA",
|
||||
"MATTERMOST_URL",
|
||||
"MATRIX_HOMESERVER",
|
||||
|
||||
@@ -53,6 +53,8 @@ const BUNDLED_TRUST_ROOT_ENV_KEYS = BUNDLED_TRUST_ROOT_ENV_LINES.map(
|
||||
);
|
||||
|
||||
const WINDOWS_SHELL_TRUST_ROOT_ENV_KEYS = [
|
||||
"AppData",
|
||||
"APPDATA",
|
||||
"ComSpec",
|
||||
"COMSPEC",
|
||||
"LocalAppData",
|
||||
@@ -515,6 +517,8 @@ describe("loadDotEnv", () => {
|
||||
await writeEnvFile(
|
||||
path.join(cwdDir, ".env"),
|
||||
[
|
||||
"AppData=.\\evil-app-data",
|
||||
"APPDATA=.\\evil-app-data-upper",
|
||||
"ComSpec=.\\evil-comspec",
|
||||
"COMSPEC=.\\evil-comspec-upper",
|
||||
"LocalAppData=.\\evil-local-app-data",
|
||||
|
||||
@@ -112,6 +112,7 @@ const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([
|
||||
"HOMEBREW_BREW_FILE",
|
||||
"HOMEBREW_PREFIX",
|
||||
"IRC_HOST",
|
||||
"APPDATA",
|
||||
"LOCALAPPDATA",
|
||||
"MATTERMOST_URL",
|
||||
"MATRIX_HOMESERVER",
|
||||
|
||||
Reference in New Issue
Block a user