From 73d30f46491ac1e387fdc1c2cda414e8d8b87356 Mon Sep 17 00:00:00 2001 From: Rui Luo Date: Wed, 20 May 2026 20:59:28 +0800 Subject: [PATCH] fix(plugin): tolerate empty/malformed onboard config.json --- Dockerfile | 2 +- nemoclaw/src/onboard/config.test.ts | 26 ++++++++++++++++++++++++-- nemoclaw/src/onboard/config.ts | 11 ++++++++--- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index cba29fb48d0..adcc66abafc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -630,7 +630,7 @@ RUN chown root:root /sandbox/.nemoclaw \ && chmod -R 755 /sandbox/.nemoclaw/blueprints \ && mkdir -p /sandbox/.nemoclaw/state /sandbox/.nemoclaw/migration /sandbox/.nemoclaw/snapshots /sandbox/.nemoclaw/staging \ && chown sandbox:sandbox /sandbox/.nemoclaw/state /sandbox/.nemoclaw/migration /sandbox/.nemoclaw/snapshots /sandbox/.nemoclaw/staging \ - && touch /sandbox/.nemoclaw/config.json \ + && printf '%s' '{}' > /sandbox/.nemoclaw/config.json \ && chown sandbox:sandbox /sandbox/.nemoclaw/config.json # OpenShell 0.0.37's macOS VM backend currently remaps rootfs ownership to the diff --git a/nemoclaw/src/onboard/config.test.ts b/nemoclaw/src/onboard/config.test.ts index bb0b8627209..9eed8ca7f2b 100644 --- a/nemoclaw/src/onboard/config.test.ts +++ b/nemoclaw/src/onboard/config.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; import { homedir } from "node:os"; +import { join } from "node:path"; import { describeOnboardEndpoint, describeOnboardProvider, @@ -145,16 +146,37 @@ describe("onboard/config", () => { it("returns parsed config when file exists", () => { const config = makeConfig(); - const configPath = `${homedir()}/.nemoclaw/config.json`; + const configPath = join(homedir(), ".nemoclaw", "config.json"); store.set(configPath, JSON.stringify(config)); expect(loadOnboardConfig()).toEqual(config); }); it("returns null when the parsed JSON root is not a valid onboard config", () => { - const configPath = `${homedir()}/.nemoclaw/config.json`; + const configPath = join(homedir(), ".nemoclaw", "config.json"); store.set(configPath, JSON.stringify({ endpointType: "bogus" })); expect(loadOnboardConfig()).toBeNull(); }); + + it("returns null without throwing for an empty (0-byte) config file", () => { + const configPath = join(homedir(), ".nemoclaw", "config.json"); + store.set(configPath, ""); + expect(() => loadOnboardConfig()).not.toThrow(); + expect(loadOnboardConfig()).toBeNull(); + }); + + it("returns null without throwing for a whitespace-only config file", () => { + const configPath = join(homedir(), ".nemoclaw", "config.json"); + store.set(configPath, " \n\t "); + expect(() => loadOnboardConfig()).not.toThrow(); + expect(loadOnboardConfig()).toBeNull(); + }); + + it("returns null without throwing for malformed JSON", () => { + const configPath = join(homedir(), ".nemoclaw", "config.json"); + store.set(configPath, "{ not json"); + expect(() => loadOnboardConfig()).not.toThrow(); + expect(loadOnboardConfig()).toBeNull(); + }); }); describe("saveOnboardConfig", () => { diff --git a/nemoclaw/src/onboard/config.ts b/nemoclaw/src/onboard/config.ts index ac59a05eebe..ffccc423424 100644 --- a/nemoclaw/src/onboard/config.ts +++ b/nemoclaw/src/onboard/config.ts @@ -161,9 +161,14 @@ export function loadOnboardConfig(): NemoClawOnboardConfig | null { if (!existsSync(path)) { return null; } - const parsed: unknown = JSON.parse(readFileSync(path, "utf-8")); - const parsedObject = typeof parsed === "object" && parsed !== null ? parsed : null; - return isOnboardConfig(parsedObject) ? parsedObject : null; + // Treat unreadable config as "no config" so plugin register doesn't abort. + try { + const parsed: unknown = JSON.parse(readFileSync(path, "utf-8")); + const parsedObject = typeof parsed === "object" && parsed !== null ? parsed : null; + return isOnboardConfig(parsedObject) ? parsedObject : null; + } catch { + return null; + } } export function saveOnboardConfig(config: NemoClawOnboardConfig): void {