mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:50:44 +00:00
fix: reject manifestless plugin archives
This commit is contained in:
@@ -120,6 +120,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/subagents: keep queued subagent announces session-only when the requester has no external channel target, avoiding ambiguous multi-channel delivery failures. Fixes #59201. Thanks @larrylhollan.
|
||||
- Image understanding: preserve configured provider-prefixed vision model metadata when callers request the model without the provider prefix, so custom image models keep their `input: ["text", "image"]` capability. Fixes #33185. Thanks @Kobe9312 and @vincentkoc.
|
||||
- Plugins/install: restore the previous plugin index records if a concurrent config write conflict interrupts install, update, or uninstall metadata commits. Thanks @shakkernerd.
|
||||
- Plugins/install: reject native plugin archives that do not include a valid `openclaw.plugin.json`, preventing manifestless archives from writing install records that later show missing-manifest diagnostics. Thanks @shakkernerd.
|
||||
- Plugins/update: restore previous plugin index records if core update or channel setup hits a concurrent config write conflict after plugin metadata changes. Thanks @shakkernerd.
|
||||
- Plugins/onboarding: defer channel/provider plugin install records until the owning config write commits, keeping setup failures from advancing the plugin index ahead of `openclaw.json`. Thanks @shakkernerd.
|
||||
- Plugins/config: route configure and agent setup writes with pending plugin install records through the plugin index commit helper so provider onboarding metadata is not stripped by plain config writes. Thanks @shakkernerd.
|
||||
|
||||
@@ -122,6 +122,9 @@ installs the bundled plugin directly. To install an npm package with the same
|
||||
name, use an explicit scoped spec (for example `@scope/diffs`).
|
||||
|
||||
Supported archives: `.zip`, `.tgz`, `.tar.gz`, `.tar`.
|
||||
Native OpenClaw plugin archives must contain a valid `openclaw.plugin.json` at
|
||||
the extracted plugin root; archives that only contain `package.json` are
|
||||
rejected before OpenClaw writes install records.
|
||||
|
||||
Claude marketplace installs are also supported.
|
||||
|
||||
|
||||
@@ -117,10 +117,6 @@ async function packToArchive({
|
||||
return dest;
|
||||
}
|
||||
|
||||
function readVoiceCallArchiveBuffer(version: string): Buffer {
|
||||
return fs.readFileSync(path.join(pluginFixturesDir, `voice-call-${version}.tgz`));
|
||||
}
|
||||
|
||||
function getArchiveFixturePath(params: {
|
||||
cacheKey: string;
|
||||
outName: string;
|
||||
@@ -140,8 +136,6 @@ function readZipperArchiveBuffer(): Buffer {
|
||||
return fs.readFileSync(path.join(pluginFixturesDir, "zipper-0.0.1.zip"));
|
||||
}
|
||||
|
||||
const VOICE_CALL_ARCHIVE_V1_BUFFER = readVoiceCallArchiveBuffer("0.0.1");
|
||||
const VOICE_CALL_ARCHIVE_V2_BUFFER = readVoiceCallArchiveBuffer("0.0.2");
|
||||
const ZIPPER_ARCHIVE_BUFFER = readZipperArchiveBuffer();
|
||||
|
||||
function expectPluginFiles(result: { targetDir: string }, stateDir: string, pluginId: string) {
|
||||
@@ -430,6 +424,8 @@ async function installArchivePackageAndReturnResult(params: {
|
||||
outName: string;
|
||||
withDistIndex?: boolean;
|
||||
flatRoot?: boolean;
|
||||
writePluginManifest?: boolean;
|
||||
manifestId?: string;
|
||||
}) {
|
||||
const stateDir = suiteTempRootTracker.makeTempDir();
|
||||
const archivePath = await ensureDynamicArchiveTemplate({
|
||||
@@ -437,6 +433,8 @@ async function installArchivePackageAndReturnResult(params: {
|
||||
packageJson: params.packageJson,
|
||||
withDistIndex: params.withDistIndex === true,
|
||||
flatRoot: params.flatRoot === true,
|
||||
writePluginManifest: params.writePluginManifest,
|
||||
manifestId: params.manifestId,
|
||||
});
|
||||
|
||||
const extensionsDir = path.join(stateDir, "extensions");
|
||||
@@ -452,12 +450,16 @@ function buildDynamicArchiveTemplateKey(params: {
|
||||
withDistIndex: boolean;
|
||||
distIndexJsContent?: string;
|
||||
flatRoot: boolean;
|
||||
writePluginManifest?: boolean;
|
||||
manifestId?: string;
|
||||
}): string {
|
||||
return JSON.stringify({
|
||||
packageJson: params.packageJson,
|
||||
withDistIndex: params.withDistIndex,
|
||||
distIndexJsContent: params.distIndexJsContent ?? null,
|
||||
flatRoot: params.flatRoot,
|
||||
writePluginManifest: params.writePluginManifest ?? true,
|
||||
manifestId: params.manifestId ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -467,12 +469,16 @@ async function ensureDynamicArchiveTemplate(params: {
|
||||
withDistIndex: boolean;
|
||||
distIndexJsContent?: string;
|
||||
flatRoot?: boolean;
|
||||
writePluginManifest?: boolean;
|
||||
manifestId?: string;
|
||||
}): Promise<string> {
|
||||
const templateKey = buildDynamicArchiveTemplateKey({
|
||||
packageJson: params.packageJson,
|
||||
withDistIndex: params.withDistIndex,
|
||||
distIndexJsContent: params.distIndexJsContent,
|
||||
flatRoot: params.flatRoot === true,
|
||||
writePluginManifest: params.writePluginManifest,
|
||||
manifestId: params.manifestId,
|
||||
});
|
||||
const cachedPath = dynamicArchiveTemplatePathCache.get(templateKey);
|
||||
if (cachedPath) {
|
||||
@@ -490,6 +496,18 @@ async function ensureDynamicArchiveTemplate(params: {
|
||||
);
|
||||
}
|
||||
fs.writeFileSync(path.join(pkgDir, "package.json"), JSON.stringify(params.packageJson), "utf-8");
|
||||
if (params.writePluginManifest !== false) {
|
||||
const packageName =
|
||||
typeof params.packageJson.name === "string" ? params.packageJson.name : "fixture-plugin";
|
||||
fs.writeFileSync(
|
||||
path.join(pkgDir, "openclaw.plugin.json"),
|
||||
JSON.stringify({
|
||||
id: params.manifestId ?? packageName,
|
||||
configSchema: { type: "object", properties: {} },
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
const archivePath = await packToArchive({
|
||||
pkgDir,
|
||||
outDir: ensureSuiteFixtureRoot(),
|
||||
@@ -578,15 +596,23 @@ beforeEach(() => {
|
||||
describe("installPluginFromArchive", () => {
|
||||
it("installs scoped archives, rejects duplicate installs, and allows updates", async () => {
|
||||
const stateDir = suiteTempRootTracker.makeTempDir();
|
||||
const archiveV1 = getArchiveFixturePath({
|
||||
cacheKey: "voice-call:0.0.1",
|
||||
const archiveV1 = await ensureDynamicArchiveTemplate({
|
||||
outName: "voice-call-0.0.1.tgz",
|
||||
buffer: VOICE_CALL_ARCHIVE_V1_BUFFER,
|
||||
packageJson: {
|
||||
name: "@openclaw/voice-call",
|
||||
version: "0.0.1",
|
||||
openclaw: { extensions: ["./dist/index.js"] },
|
||||
},
|
||||
withDistIndex: true,
|
||||
});
|
||||
const archiveV2 = getArchiveFixturePath({
|
||||
cacheKey: "voice-call:0.0.2",
|
||||
const archiveV2 = await ensureDynamicArchiveTemplate({
|
||||
outName: "voice-call-0.0.2.tgz",
|
||||
buffer: VOICE_CALL_ARCHIVE_V2_BUFFER,
|
||||
packageJson: {
|
||||
name: "@openclaw/voice-call",
|
||||
version: "0.0.2",
|
||||
openclaw: { extensions: ["./dist/index.js"] },
|
||||
},
|
||||
withDistIndex: true,
|
||||
});
|
||||
|
||||
const extensionsDir = path.join(stateDir, "extensions");
|
||||
@@ -620,7 +646,7 @@ describe("installPluginFromArchive", () => {
|
||||
expect(manifest.version).toBe("0.0.2");
|
||||
});
|
||||
|
||||
it("installs from a zip archive", async () => {
|
||||
it("rejects native plugin zip archives without openclaw.plugin.json", async () => {
|
||||
const stateDir = suiteTempRootTracker.makeTempDir();
|
||||
const archivePath = getArchiveFixturePath({
|
||||
cacheKey: "zipper:0.0.1",
|
||||
@@ -633,7 +659,12 @@ describe("installPluginFromArchive", () => {
|
||||
archivePath,
|
||||
extensionsDir,
|
||||
});
|
||||
expectSuccessfulArchiveInstall({ result, stateDir, pluginId: "@openclaw/zipper" });
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error).toContain("package missing valid openclaw.plugin.json");
|
||||
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.MISSING_PLUGIN_MANIFEST);
|
||||
}
|
||||
expect(fs.existsSync(resolvePluginInstallDir("@openclaw/zipper", extensionsDir))).toBe(false);
|
||||
});
|
||||
|
||||
it("allows archive installs with dangerous code patterns when forced unsafe install is set", async () => {
|
||||
|
||||
@@ -51,6 +51,7 @@ export const PLUGIN_INSTALL_ERROR_CODE = {
|
||||
UNKNOWN_HOST_VERSION: "unknown_host_version",
|
||||
INCOMPATIBLE_HOST_VERSION: "incompatible_host_version",
|
||||
MISSING_OPENCLAW_EXTENSIONS: "missing_openclaw_extensions",
|
||||
MISSING_PLUGIN_MANIFEST: "missing_plugin_manifest",
|
||||
EMPTY_OPENCLAW_EXTENSIONS: "empty_openclaw_extensions",
|
||||
NPM_PACKAGE_NOT_FOUND: "npm_package_not_found",
|
||||
PLUGIN_ID_MISMATCH: "plugin_id_mismatch",
|
||||
@@ -241,6 +242,7 @@ type PackageInstallCommonParams = InstallSafetyOverrides & {
|
||||
mode?: "install" | "update";
|
||||
dryRun?: boolean;
|
||||
expectedPluginId?: string;
|
||||
requirePluginManifest?: boolean;
|
||||
installPolicyRequest?: PluginInstallPolicyRequest;
|
||||
};
|
||||
|
||||
@@ -265,6 +267,7 @@ function pickPackageInstallCommonParams(
|
||||
mode: params.mode,
|
||||
dryRun: params.dryRun,
|
||||
expectedPluginId: params.expectedPluginId,
|
||||
requirePluginManifest: params.requirePluginManifest,
|
||||
installPolicyRequest: params.installPolicyRequest,
|
||||
};
|
||||
}
|
||||
@@ -697,6 +700,13 @@ async function installPluginFromPackageDir(
|
||||
// differs from the npm package name (e.g. "cognee-openclaw"), the plugin registry
|
||||
// uses the manifest id as the authoritative key, so the config entry must match it.
|
||||
const ocManifestResult = runtime.loadPluginManifest(params.packageDir);
|
||||
if (!ocManifestResult.ok && params.requirePluginManifest) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `package missing valid openclaw.plugin.json: ${ocManifestResult.error}`,
|
||||
code: PLUGIN_INSTALL_ERROR_CODE.MISSING_PLUGIN_MANIFEST,
|
||||
};
|
||||
}
|
||||
const manifestPluginId =
|
||||
ocManifestResult.ok && ocManifestResult.manifest.id
|
||||
? ocManifestResult.manifest.id.trim()
|
||||
@@ -882,6 +892,7 @@ export async function installPluginFromArchive(
|
||||
mode,
|
||||
dryRun: params.dryRun,
|
||||
expectedPluginId: params.expectedPluginId,
|
||||
requirePluginManifest: true,
|
||||
installPolicyRequest,
|
||||
}),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user