Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 24 additions & 2 deletions nemoclaw/src/onboard/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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", () => {
Expand Down
11 changes: 8 additions & 3 deletions nemoclaw/src/onboard/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading