diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f310dbce9c..417aa5f75c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ Docs: https://docs.openclaw.ai - Gateway/sessions: keep reachable transcript history when imported tree transcripts reference missing or legacy parent rows, preventing session history reads from going empty after a partial import. - Trajectory export: report incomplete transcript parent chains and stop cyclic branch walks so malformed imports cannot hang `/export-trajectory`. - Session replay: skip malformed user/assistant-shaped transcript rows during silent session resets instead of copying invalid entries into the fresh transcript. +- Backup verify: report malformed archive manifests with a stable error instead of leaking raw JSON parser details. - Providers: reject malformed successful Runway, BytePlus, and Ollama embedding responses with provider-owned errors instead of raw parser/type failures, silent bad vectors, or long bogus polling. - Providers/images: reject malformed successful OpenAI-compatible, OpenAI, Google, fal, and OpenRouter image responses with provider-owned errors instead of raw shape failures, silent invalid base64 skips, or empty image results. - Providers/videos: reject malformed successful xAI, OpenRouter, and fal video create, poll, and result responses with provider-owned errors instead of raw parser failures or long bogus polling. diff --git a/src/commands/backup-verify.test.ts b/src/commands/backup-verify.test.ts index e24b3c65002..61c6451ae12 100644 --- a/src/commands/backup-verify.test.ts +++ b/src/commands/backup-verify.test.ts @@ -32,6 +32,47 @@ function createBackupManifest(assetArchivePath: string, archiveRoot = TEST_ARCHI }; } +async function createArchiveWithManifestContent( + options: { + tempPrefix: string; + manifestContent: string; + payloadArchivePath?: string; + }, + run: (archivePath: string) => Promise, +) { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), options.tempPrefix)); + const archivePath = path.join(tempDir, "broken.tar.gz"); + const manifestPath = path.join(tempDir, "manifest.json"); + const payloadPath = path.join(tempDir, "payload.txt"); + const payloadArchivePath = + options.payloadArchivePath ?? `${TEST_ARCHIVE_ROOT}/payload/posix/tmp/.openclaw/payload.txt`; + try { + await fs.writeFile(manifestPath, options.manifestContent, "utf8"); + await fs.writeFile(payloadPath, "payload\n", "utf8"); + await tar.c( + { + file: archivePath, + gzip: true, + portable: true, + preservePaths: true, + onWriteEntry: (entry) => { + if (entry.path === manifestPath) { + entry.path = `${TEST_ARCHIVE_ROOT}/manifest.json`; + return; + } + if (entry.path === payloadPath) { + entry.path = payloadArchivePath; + } + }, + }, + [manifestPath, payloadPath], + ); + await run(archivePath); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } +} + async function withBrokenArchiveFixture( options: { tempPrefix: string; @@ -196,6 +237,24 @@ describe("backupVerifyCommand", () => { } }); + it("reports malformed manifest JSON without leaking parser internals", async () => { + await createArchiveWithManifestContent( + { + tempPrefix: "openclaw-backup-bad-manifest-json-", + manifestContent: '{"schemaVersion":1,', + }, + async (archivePath) => { + const runtime = createBackupVerifyRuntime(); + await expect(backupVerifyCommand(runtime, { archive: archivePath })).rejects.toThrow( + /^Backup manifest is not valid JSON\.$/u, + ); + await expect(backupVerifyCommand(runtime, { archive: archivePath })).rejects.not.toThrow( + /position|Unexpected|Expected|SyntaxError/u, + ); + }, + ); + }); + it("rejects unsafe archive paths", async () => { for (const { tempPrefix, archivePath, error } of [ { diff --git a/src/commands/backup-verify.ts b/src/commands/backup-verify.ts index ab9880e9dfb..f9e78804af4 100644 --- a/src/commands/backup-verify.ts +++ b/src/commands/backup-verify.ts @@ -96,7 +96,7 @@ function parseManifest(raw: string): BackupManifest { try { parsed = JSON.parse(raw); } catch (err) { - throw new Error(`Backup manifest is not valid JSON: ${String(err)}`, { cause: err }); + throw new Error("Backup manifest is not valid JSON.", { cause: err }); } if (!isRecord(parsed)) {