fix(backup): hide manifest parser internals (#82539)

This commit is contained in:
Vincent Koc
2026-05-16 18:56:45 +08:00
committed by GitHub
parent 225e48f632
commit 01eb56e45a
3 changed files with 61 additions and 1 deletions

View File

@@ -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.

View File

@@ -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<void>,
) {
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 [
{

View File

@@ -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)) {