diff --git a/CHANGELOG.md b/CHANGELOG.md index 2337b4434d9..8e7cbaee962 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai - Docker: pin base images to SHA256 digests in Docker builds to prevent mutable tag drift. (#7734) Thanks @coygeek. - Provider/HTTP: treat HTTP 503 as failover-eligible for LLM provider errors. (#21086) Thanks @Protocol-zero-0. - Slack: pass `recipient_team_id` / `recipient_user_id` through Slack native streaming calls so `chat.startStream`/`appendStream`/`stopStream` work reliably across DMs and Slack Connect setups, and disable block streaming when native streaming is active. (#20988) Thanks @Dithilli. Earlier recipient-ID groundwork was contributed in #20377 by @AsserAl1012. +- CLI/Config: add canonical `--strict-json` parsing for `config set` and keep `--json` as a legacy alias to reduce help/behavior drift. (#21332) thanks @adhitShet. - Discord/Gateway: handle close code 4014 (missing privileged gateway intents) without crashing the gateway. Thanks @thewilloftheshadow. - Security/Net: strip sensitive headers (`Authorization`, `Proxy-Authorization`, `Cookie`, `Cookie2`) on cross-origin redirects in `fetchWithSsrFGuard` to prevent credential forwarding across origin boundaries. (#20313) Thanks @afurm. diff --git a/docs/cli/config.md b/docs/cli/config.md index a94b4614d09..18a3a0f197d 100644 --- a/docs/cli/config.md +++ b/docs/cli/config.md @@ -39,12 +39,12 @@ openclaw config set agents.list[1].tools.exec.node "node-id-or-name" ## Values Values are parsed as JSON5 when possible; otherwise they are treated as strings. -Use `--json` to require JSON5 parsing. +Use `--strict-json` to require JSON5 parsing. `--json` remains supported as a legacy alias. ```bash openclaw config set agents.defaults.heartbeat.every "0m" -openclaw config set gateway.port 19001 --json -openclaw config set channels.whatsapp.groups '["*"]' --json +openclaw config set gateway.port 19001 --strict-json +openclaw config set channels.whatsapp.groups '["*"]' --strict-json ``` Restart the gateway after edits. diff --git a/src/cli/config-cli.test.ts b/src/cli/config-cli.test.ts index 82494353ddd..ec1b6523ba0 100644 --- a/src/cli/config-cli.test.ts +++ b/src/cli/config-cli.test.ts @@ -135,6 +135,51 @@ describe("config cli", () => { }); }); + describe("config set parsing flags", () => { + it("falls back to raw string when parsing fails and strict mode is off", async () => { + const resolved: OpenClawConfig = { gateway: { port: 18789 } }; + setSnapshot(resolved, resolved); + + await runConfigCommand(["config", "set", "gateway.auth.mode", "{bad"]); + + expect(mockWriteConfigFile).toHaveBeenCalledTimes(1); + const written = mockWriteConfigFile.mock.calls[0]?.[0]; + expect(written.gateway?.auth).toEqual({ mode: "{bad" }); + }); + + it("throws when strict parsing is enabled via --strict-json", async () => { + await expect( + runConfigCommand(["config", "set", "gateway.auth.mode", "{bad", "--strict-json"]), + ).rejects.toThrow("__exit__:1"); + + expect(mockWriteConfigFile).not.toHaveBeenCalled(); + expect(mockReadConfigFileSnapshot).not.toHaveBeenCalled(); + }); + + it("keeps --json as a strict parsing alias", async () => { + await expect( + runConfigCommand(["config", "set", "gateway.auth.mode", "{bad", "--json"]), + ).rejects.toThrow("__exit__:1"); + + expect(mockWriteConfigFile).not.toHaveBeenCalled(); + expect(mockReadConfigFileSnapshot).not.toHaveBeenCalled(); + }); + + it("shows --strict-json and keeps --json as a legacy alias in help", async () => { + const { registerConfigCli } = await import("./config-cli.js"); + const program = new Command(); + registerConfigCli(program); + + const configCommand = program.commands.find((command) => command.name() === "config"); + const setCommand = configCommand?.commands.find((command) => command.name() === "set"); + const helpText = setCommand?.helpInformation() ?? ""; + + expect(helpText).toContain("--strict-json"); + expect(helpText).toContain("--json"); + expect(helpText).toContain("Legacy alias for --strict-json"); + }); + }); + describe("config unset - issue #6070", () => { it("preserves existing config keys when unsetting a value", async () => { const resolved: OpenClawConfig = { diff --git a/src/cli/config-cli.ts b/src/cli/config-cli.ts index c35caf5f007..8ba693329b4 100644 --- a/src/cli/config-cli.ts +++ b/src/cli/config-cli.ts @@ -10,6 +10,9 @@ import { shortenHomePath } from "../utils.js"; import { formatCliCommand } from "./command-format.js"; type PathSegment = string; +type ConfigSetParseOpts = { + strictJson?: boolean; +}; function isIndexSegment(raw: string): boolean { return /^[0-9]+$/.test(raw); @@ -67,9 +70,9 @@ function parsePath(raw: string): PathSegment[] { return parts.map((part) => part.trim()).filter(Boolean); } -function parseValue(raw: string, opts: { json?: boolean }): unknown { +function parseValue(raw: string, opts: ConfigSetParseOpts): unknown { const trimmed = raw.trim(); - if (opts.json) { + if (opts.strictJson) { try { return JSON5.parse(trimmed); } catch (err) { @@ -313,14 +316,17 @@ export function registerConfigCli(program: Command) { .description("Set a config value by dot path") .argument("", "Config path (dot or bracket notation)") .argument("", "Value (JSON5 or raw string)") - .option("--json", "Parse value as JSON5 (required)", false) + .option("--strict-json", "Strict JSON5 parsing (error instead of raw string fallback)", false) + .option("--json", "Legacy alias for --strict-json", false) .action(async (path: string, value: string, opts) => { try { const parsedPath = parsePath(path); if (parsedPath.length === 0) { throw new Error("Path is empty."); } - const parsedValue = parseValue(value, opts); + const parsedValue = parseValue(value, { + strictJson: Boolean(opts.strictJson || opts.json), + }); const snapshot = await loadValidConfig(); // Use snapshot.resolved (config after $include and ${ENV} resolution, but BEFORE runtime defaults) // instead of snapshot.config (runtime-merged with defaults).