mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 13:11:40 +00:00
fix: use JSON5 parser for plugin manifest loading (#57734) [AI-assisted]
Plugin install fails when a third-party plugin manifest (openclaw.plugin.json) contains JSON5 features such as trailing commas, comments, or unquoted keys. The strict JSON.parse used in loadPluginManifest and loadBundleManifestRaw rejects these files during the config validation step of writeConfigFile, preventing the install from completing. - Replace JSON.parse with JSON5.parse in manifest.ts and bundle-manifest.ts - Add JSON5 tolerance tests covering trailing commas, comments, unquoted keys - Verified zero regression across manifest-registry, bundle-manifest, install, installs, and io.write-config test suites (101 tests pass) AI-assisted: code fix and test generation verified by automated test runs
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import JSON5 from "json5";
|
||||
import { matchBoundaryFileOpenFailure, openBoundaryFileSync } from "../infra/boundary-file-read.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import { DEFAULT_PLUGIN_ENTRY_CANDIDATES, PLUGIN_MANIFEST_FILENAME } from "./manifest.js";
|
||||
@@ -117,7 +118,7 @@ function loadBundleManifestFile(params: {
|
||||
});
|
||||
}
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(opened.fd, "utf-8")) as unknown;
|
||||
const raw = JSON5.parse(fs.readFileSync(opened.fd, "utf-8")) as unknown;
|
||||
if (!isRecord(raw)) {
|
||||
return { ok: false, error: "plugin manifest must be an object", manifestPath };
|
||||
}
|
||||
|
||||
93
src/plugins/manifest.json5-tolerance.test.ts
Normal file
93
src/plugins/manifest.json5-tolerance.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { loadPluginManifest } from "./manifest.js";
|
||||
import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
function makeTempDir() {
|
||||
return makeTrackedTempDir("openclaw-manifest-json5", tempDirs);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanupTrackedTempDirs(tempDirs);
|
||||
});
|
||||
|
||||
describe("loadPluginManifest JSON5 tolerance", () => {
|
||||
it("parses a standard JSON manifest without issues", () => {
|
||||
const dir = makeTempDir();
|
||||
const manifest = {
|
||||
id: "demo",
|
||||
configSchema: { type: "object" },
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "openclaw.plugin.json"),
|
||||
JSON.stringify(manifest, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
const result = loadPluginManifest(dir, false);
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.manifest.id).toBe("demo");
|
||||
}
|
||||
});
|
||||
|
||||
it("parses a manifest with trailing commas", () => {
|
||||
const dir = makeTempDir();
|
||||
const json5Content = `{
|
||||
"id": "hindsight",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apiKey": { "type": "string" },
|
||||
},
|
||||
},
|
||||
}`;
|
||||
fs.writeFileSync(path.join(dir, "openclaw.plugin.json"), json5Content, "utf-8");
|
||||
const result = loadPluginManifest(dir, false);
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.manifest.id).toBe("hindsight");
|
||||
}
|
||||
});
|
||||
|
||||
it("parses a manifest with single-line comments", () => {
|
||||
const dir = makeTempDir();
|
||||
const json5Content = `{
|
||||
// Plugin identifier
|
||||
"id": "commented-plugin",
|
||||
"configSchema": { "type": "object" }
|
||||
}`;
|
||||
fs.writeFileSync(path.join(dir, "openclaw.plugin.json"), json5Content, "utf-8");
|
||||
const result = loadPluginManifest(dir, false);
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.manifest.id).toBe("commented-plugin");
|
||||
}
|
||||
});
|
||||
|
||||
it("parses a manifest with unquoted property names", () => {
|
||||
const dir = makeTempDir();
|
||||
const json5Content = `{
|
||||
id: "unquoted-keys",
|
||||
configSchema: { type: "object" }
|
||||
}`;
|
||||
fs.writeFileSync(path.join(dir, "openclaw.plugin.json"), json5Content, "utf-8");
|
||||
const result = loadPluginManifest(dir, false);
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.manifest.id).toBe("unquoted-keys");
|
||||
}
|
||||
});
|
||||
|
||||
it("still rejects completely invalid syntax", () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(path.join(dir, "openclaw.plugin.json"), "not json at all {{{}}", "utf-8");
|
||||
const result = loadPluginManifest(dir, false);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error).toContain("failed to parse plugin manifest");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import JSON5 from "json5";
|
||||
import { MANIFEST_KEY } from "../compat/legacy-names.js";
|
||||
import { matchBoundaryFileOpenFailure, openBoundaryFileSync } from "../infra/boundary-file-read.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
@@ -273,7 +274,7 @@ export function loadPluginManifest(
|
||||
}
|
||||
let raw: unknown;
|
||||
try {
|
||||
raw = JSON.parse(fs.readFileSync(opened.fd, "utf-8")) as unknown;
|
||||
raw = JSON5.parse(fs.readFileSync(opened.fd, "utf-8")) as unknown;
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
|
||||
Reference in New Issue
Block a user