From 2b11203eb3ce7fc0cf5b582aedcf6323978e514c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Jul 2026 13:56:00 -0400 Subject: [PATCH] fix(cli): keep config file queries fast and read-only (#114660) --- src/cli/command-catalog.ts | 11 +++++++++++ src/cli/command-path-policy.test.ts | 6 ++++++ src/cli/command-startup-policy.test.ts | 1 + src/cli/program/preaction.test.ts | 14 ++++++++++++++ test/cli-json-stdout.e2e.test.ts | 7 +++++++ 5 files changed, 39 insertions(+) diff --git a/src/cli/command-catalog.ts b/src/cli/command-catalog.ts index 2bf7a3ccd88b..9726485d2306 100644 --- a/src/cli/command-catalog.ts +++ b/src/cli/command-catalog.ts @@ -226,6 +226,17 @@ export const cliCommandCatalog: readonly CliCommandCatalogEntry[] = [ policy: { loadPlugins: "never", networkProxy: "bypass" }, route: { id: "agents-list" }, }, + { + commandPath: ["config", "file"], + exact: true, + // A path query must work before config validation and must not initialize state. + policy: { + bypassConfigGuard: true, + ensureCliPath: false, + loadPlugins: "never", + networkProxy: "bypass", + }, + }, { commandPath: ["config", "get"], exact: true, diff --git a/src/cli/command-path-policy.test.ts b/src/cli/command-path-policy.test.ts index e76546078b4f..79e3760d57b6 100644 --- a/src/cli/command-path-policy.test.ts +++ b/src/cli/command-path-policy.test.ts @@ -216,6 +216,12 @@ describe("command-path-policy", () => { loadPlugins: "never", networkProxy: "bypass", }); + expectResolvedPolicy(["config", "file"], { + bypassConfigGuard: true, + ensureCliPath: false, + loadPlugins: "never", + networkProxy: "bypass", + }); expectResolvedPolicy(["config", "set"], { loadPlugins: "never", networkProxy: "bypass", diff --git a/src/cli/command-startup-policy.test.ts b/src/cli/command-startup-policy.test.ts index dd6dc4713222..40260e6b18c0 100644 --- a/src/cli/command-startup-policy.test.ts +++ b/src/cli/command-startup-policy.test.ts @@ -22,6 +22,7 @@ describe("command-startup-policy", () => { it("matches config guard bypass commands", () => { expect(shouldBypassConfigGuardForCommandPath(["backup", "create"])).toBe(true); expect(shouldBypassConfigGuardForCommandPath(["config"])).toBe(true); + expect(shouldBypassConfigGuardForCommandPath(["config", "file"])).toBe(true); expect(shouldBypassConfigGuardForCommandPath(["config", "validate"])).toBe(true); expect(shouldBypassConfigGuardForCommandPath(["config", "schema"])).toBe(true); expect(shouldBypassConfigGuardForCommandPath(["docs"])).toBe(true); diff --git a/src/cli/program/preaction.test.ts b/src/cli/program/preaction.test.ts index d6121b907a53..525fb0beccc8 100644 --- a/src/cli/program/preaction.test.ts +++ b/src/cli/program/preaction.test.ts @@ -248,6 +248,7 @@ describe("registerPreActionHooks", () => { .argument("") .option("--json") .action(() => {}); + config.command("file").action(() => {}); config .command("validate") .option("--json") @@ -830,6 +831,19 @@ describe("registerPreActionHooks", () => { expect(ensurePluginRegistryLoadedMock).not.toHaveBeenCalled(); }); + it.each([ + ["default profile", ["node", "openclaw", "config", "file"]], + ["named profile", ["node", "openclaw", "--profile", "work", "config", "file"]], + ])("bypasses config guard for a %s config path query", async (_name, processArgv) => { + await runPreAction({ + parseArgv: ["config", "file"], + processArgv, + }); + + expect(ensureConfigReadyMock).not.toHaveBeenCalled(); + expect(ensurePluginRegistryLoadedMock).not.toHaveBeenCalled(); + }); + it("bypasses config guard for config validate", async () => { await runPreAction({ parseArgv: ["config", "validate"], diff --git a/test/cli-json-stdout.e2e.test.ts b/test/cli-json-stdout.e2e.test.ts index ccda1cde616e..6b9899bd9f44 100644 --- a/test/cli-json-stdout.e2e.test.ts +++ b/test/cli-json-stdout.e2e.test.ts @@ -24,6 +24,7 @@ function runSourceCli(tempHome: string, args: string[], envOverrides: NodeJS.Pro env, encoding: "utf8", maxBuffer: 10 * 1024 * 1024, + timeout: 60_000, }); } @@ -43,6 +44,9 @@ describe("cli json stdout contract", () => { expect(result.status, result.stderr).toBe(0); expect(result.stdout.trim()).toBe(path.join(tempHome, ".openclaw-work", "openclaw.json")); + await expect(fs.access(path.join(tempHome, ".openclaw-work"))).rejects.toMatchObject({ + code: "ENOENT", + }); }, { prefix: "openclaw-profile-isolation-e2e-" }, ); @@ -72,6 +76,9 @@ describe("cli json stdout contract", () => { await expect( fs.access(path.join(scratchStateDir, "exec-approvals.json")), ).rejects.toMatchObject({ code: "ENOENT" }); + await expect( + fs.access(path.join(scratchStateDir, "state", "openclaw.sqlite")), + ).rejects.toMatchObject({ code: "ENOENT" }); }, { prefix: "openclaw-read-only-state-e2e-" }, );