From 9931603adb932eb643e9516ec83a7cf1ee1d9f94 Mon Sep 17 00:00:00 2001 From: "clawsweeper[bot]" <274271284+clawsweeper[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:46:52 -0700 Subject: [PATCH] fix(pairing): rethrow unreadable allowlist files Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com> --- src/pairing/allow-from-store-file.test.ts | 47 +++++++++++++++++++++++ src/pairing/allow-from-store-file.ts | 29 +++++++++----- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/pairing/allow-from-store-file.test.ts b/src/pairing/allow-from-store-file.test.ts index f2276ff99e2..b6ee147b880 100644 --- a/src/pairing/allow-from-store-file.test.ts +++ b/src/pairing/allow-from-store-file.test.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import { describe, expect, it, vi } from "vitest"; import { + readAllowFromFileWithExists, readAllowFromFileSyncWithExists, resolveAllowFromAccountId, resolveAllowFromFilePath, @@ -73,6 +74,52 @@ describe("allow-from store file keys", () => { }); describe("allow-from store file reads", () => { + it("rethrows unexpected async read errors after a successful stat", async () => { + const error = fsError("permission denied", "EACCES"); + const statSpy = vi.spyOn(fs.promises, "stat").mockResolvedValue({ + mtimeMs: 1, + size: 2, + } as fs.Stats); + const readSpy = vi.spyOn(fs.promises, "readFile").mockRejectedValue(error); + + try { + await expect( + readAllowFromFileWithExists({ + cacheNamespace: "test-async-read-error", + filePath: "/tmp/openclaw-allowFrom.json", + normalizeStore: () => [], + }), + ).rejects.toBe(error); + } finally { + readSpy.mockRestore(); + statSpy.mockRestore(); + } + }); + + it("rethrows unexpected sync read errors after a successful stat", () => { + const error = fsError("permission denied", "EACCES"); + const statSpy = vi.spyOn(fs, "statSync").mockReturnValue({ + mtimeMs: 1, + size: 2, + } as fs.Stats); + const readSpy = vi.spyOn(fs, "readFileSync").mockImplementation(() => { + throw error; + }); + + try { + expect(() => + readAllowFromFileSyncWithExists({ + cacheNamespace: "test-sync-read-error", + filePath: "/tmp/openclaw-allowFrom.json", + normalizeStore: () => [], + }), + ).toThrow(error); + } finally { + readSpy.mockRestore(); + statSpy.mockRestore(); + } + }); + it("rethrows unexpected sync stat errors", () => { const error = fsError("permission denied", "EACCES"); const statSpy = vi.spyOn(fs, "statSync").mockImplementation(() => { diff --git a/src/pairing/allow-from-store-file.ts b/src/pairing/allow-from-store-file.ts index a1a16e8eafa..9695a287a1b 100644 --- a/src/pairing/allow-from-store-file.ts +++ b/src/pairing/allow-from-store-file.ts @@ -3,7 +3,6 @@ import os from "node:os"; import path from "node:path"; import { resolveOAuthDir, resolveStateDir } from "../config/paths.js"; import { resolveRequiredHomeDir } from "../infra/home-dir.js"; -import { readJsonFileWithFallback } from "../plugin-sdk/json-store.js"; import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; import { normalizeLowercaseStringOrEmpty, @@ -243,22 +242,34 @@ export async function readAllowFromFileWithExists(params: { return { entries: [], exists: false }; } - const { value, exists } = await readJsonFileWithFallback(params.filePath, { - version: 1, - allowFrom: [], - }); - const entries = params.normalizeStore(value); + let raw = ""; + try { + raw = await fs.promises.readFile(params.filePath, "utf8"); + } catch (err) { + const code = (err as { code?: string }).code; + if (code === "ENOENT") { + return { entries: [], exists: false }; + } + throw err; + } + + let entries: string[] = []; + try { + entries = params.normalizeStore(JSON.parse(raw) as AllowFromStore); + } catch { + entries = []; + } setAllowFromFileReadCache({ cacheNamespace: params.cacheNamespace, filePath: params.filePath, entry: { - exists, + exists: true, mtimeMs: stat.mtimeMs, size: stat.size, entries, }, }); - return { entries, exists }; + return { entries, exists: true }; } export function readAllowFromFileSyncWithExists(params: { @@ -296,7 +307,7 @@ export function readAllowFromFileSyncWithExists(params: { if (code === "ENOENT") { return { entries: [], exists: false }; } - return { entries: [], exists: false }; + throw err; } try {