From af469c85cf6c5ce6ba83319b3d7366fb39023dfa Mon Sep 17 00:00:00 2001 From: krissding Date: Fri, 17 Jul 2026 17:00:35 +0800 Subject: [PATCH] fix(cli): guard secrets plan JSON.parse against malformed input (#109721) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(cli): guard secrets plan JSON.parse against malformed input readPlanFile reads a user-specified file and parses it with JSON.parse without a try-catch. A malformed or corrupted plan file produces a raw SyntaxError that is not user-actionable. Co-Authored-By: Claude Sonnet 4.6 * test: tighten malformed secrets plan proof Co-authored-by: 丁宇婷0668001435 --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Peter Steinberger --- src/cli/secrets-cli.test.ts | 18 ++++++++++++++++-- src/cli/secrets-cli.ts | 7 ++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/cli/secrets-cli.test.ts b/src/cli/secrets-cli.test.ts index d10849de0445..0ca43924d7d5 100644 --- a/src/cli/secrets-cli.test.ts +++ b/src/cli/secrets-cli.test.ts @@ -125,12 +125,15 @@ function createSecretsApplyResult(options?: { }; } -async function withPlanFile(run: (planPath: string) => Promise) { +async function withPlanFile( + run: (planPath: string) => Promise, + contents = `${JSON.stringify(createManualSecretsPlan())}\n`, +) { const planPath = path.join( os.tmpdir(), `openclaw-secrets-cli-test-${Date.now()}-${Math.random().toString(16).slice(2)}.json`, ); - await fs.writeFile(planPath, `${JSON.stringify(createManualSecretsPlan())}\n`, "utf8"); + await fs.writeFile(planPath, contents, "utf8"); try { await run(planPath); } finally { @@ -385,6 +388,17 @@ describe("secrets CLI", () => { }); }); + it("shows a user-friendly error when the secrets plan file is malformed JSON", async () => { + await withPlanFile(async (planPath) => { + await expect( + createProgram().parseAsync(["secrets", "apply", "--from", planPath], { from: "user" }), + ).rejects.toThrow("__exit__:1"); + + expect(runtimeErrors.at(-1)).toContain(`Malformed JSON in secrets plan file: ${planPath}`); + expect(runSecretsApply).not.toHaveBeenCalled(); + }, "{invalid json"); + }); + it("does not print skipped-exec note when apply dry-run skippedExecRefs is zero", async () => { await withPlanFile(async (planPath) => { runSecretsApply.mockResolvedValue(createSecretsApplyResult({ resolvabilityComplete: false })); diff --git a/src/cli/secrets-cli.ts b/src/cli/secrets-cli.ts index 65083a11335d..23a4db508af5 100644 --- a/src/cli/secrets-cli.ts +++ b/src/cli/secrets-cli.ts @@ -53,7 +53,12 @@ async function readPlanFile(pathname: string): Promise { import("../secrets/plan.js"), ]); const raw = readFileSync(pathname, "utf8"); - const parsed = JSON.parse(raw) as unknown; + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch (err) { + throw new Error(`Malformed JSON in secrets plan file: ${pathname}`, { cause: err }); + } if (!isSecretsApplyPlan(parsed)) { throw new Error( `Invalid secrets plan file: ${pathname}. Generate a fresh plan with ${formatCliCommand("openclaw secrets configure --plan-out ")}.`,