diff --git a/scripts/nemoclaw-start.sh b/scripts/nemoclaw-start.sh index 296948d13e8..f619a30996e 100755 --- a/scripts/nemoclaw-start.sh +++ b/scripts/nemoclaw-start.sh @@ -485,6 +485,20 @@ openclaw() { echo "This rebuilds the sandbox with your updated settings." >&2 return 1 ;; + config) + case "$2" in + set | unset) + echo "Error: 'openclaw config $2' cannot modify config inside the sandbox." >&2 + echo "The sandbox config is read-only (Landlock enforced) for security." >&2 + echo "" >&2 + echo "To change your configuration, exit the sandbox and run:" >&2 + echo " nemoclaw onboard --resume" >&2 + echo "" >&2 + echo "This rebuilds the sandbox with your updated settings." >&2 + return 1 + ;; + esac + ;; agent) # Block --local inside sandbox — it bypasses gateway protections and can # crash the container's main process, bricking the sandbox. Ref: #1632, #2016 diff --git a/test/nemoclaw-start.test.ts b/test/nemoclaw-start.test.ts index a7fa3443bcd..a03de02c549 100644 --- a/test/nemoclaw-start.test.ts +++ b/test/nemoclaw-start.test.ts @@ -243,6 +243,30 @@ describe("nemoclaw-start configure guard blocks --local (#2016)", () => { }); }); +describe("nemoclaw-start configure guard blocks config set/unset (#1973)", () => { + const src = fs.readFileSync(START_SCRIPT, "utf-8"); + + it("adds a config) case that matches only set and unset subcommands", () => { + expect(src).toMatch(/config\)\s+case "\$2" in\s+set \| unset\)/); + }); + + it("prints an actionable error quoting the invoked subcommand and returns 1", () => { + expect(src).toContain("'openclaw config $2' cannot modify config inside the sandbox"); + expect(src).toMatch(/set \| unset\)[\s\S]*?return 1/); + }); + + it("redirects users to nemoclaw onboard --resume", () => { + expect(src).toMatch(/set \| unset\)[\s\S]*?nemoclaw onboard --resume/); + }); + + it("does not block immutable subcommands (get, list) — they fall through to the real binary", () => { + // The config) arm only enumerates mutating subcommands. Read-only ones are + // not matched, so execution falls through to `command openclaw "$@"` below. + expect(src).not.toMatch(/config\)\s+case "\$2" in[\s\S]*?\b(get|list|show|view)\)/); + expect(src).toContain('command openclaw "$@"'); + }); +}); + describe("runtime model override (#759)", () => { const src = fs.readFileSync(START_SCRIPT, "utf-8");