





















@@ -1,5 +1,8 @@
1+import fsSync from "node:fs";
12import fs from "node:fs/promises";
23import path from "node:path";
4+import { matchBoundaryFileOpenFailure, openBoundaryFile } from "../infra/boundary-file-read.js";
5+import { resolveBoundaryPath } from "../infra/boundary-path.js";
36import {
47packageNameMatchesId,
58resolveSafeInstallDir,
@@ -14,9 +17,11 @@ import { CONFIG_DIR, resolveUserPath } from "../utils.js";
1417import type { InstallSecurityScanResult } from "./install-security-scan.js";
1518import type { InstallSafetyOverrides } from "./install-security-scan.js";
1619import {
20+getPackageManifestMetadata,
1721resolvePackageExtensionEntries,
1822type PackageManifest as PluginPackageManifest,
1923} from "./manifest.js";
24+import { listBuiltRuntimeEntryCandidates } from "./package-entrypoints.js";
20252126let pluginInstallRuntimePromise: Promise<typeof import("./install.runtime.js")> | undefined;
2227@@ -54,6 +59,7 @@ export const PLUGIN_INSTALL_ERROR_CODE = {
5459MISSING_OPENCLAW_EXTENSIONS: "missing_openclaw_extensions",
5560MISSING_PLUGIN_MANIFEST: "missing_plugin_manifest",
5661EMPTY_OPENCLAW_EXTENSIONS: "empty_openclaw_extensions",
62+INVALID_OPENCLAW_EXTENSIONS: "invalid_openclaw_extensions",
5763NPM_PACKAGE_NOT_FOUND: "npm_package_not_found",
5864PLUGIN_ID_MISMATCH: "plugin_id_mismatch",
5965SECURITY_SCAN_BLOCKED: "security_scan_blocked",
@@ -186,6 +192,139 @@ function ensureOpenClawExtensions(params: { manifest: PackageManifest }):
186192};
187193}
188194195+type ExtensionEntryValidation = { ok: true; exists: boolean } | { ok: false; error: string };
196+197+async function validatePackageExtensionEntry(params: {
198+packageDir: string;
199+entry: string;
200+label: string;
201+requireExisting: boolean;
202+}): Promise<ExtensionEntryValidation> {
203+const absolutePath = path.resolve(params.packageDir, params.entry);
204+try {
205+const resolved = await resolveBoundaryPath({
206+ absolutePath,
207+rootPath: params.packageDir,
208+boundaryLabel: "plugin package directory",
209+});
210+if (!resolved.exists) {
211+return params.requireExisting
212+ ? { ok: false, error: `${params.label} not found: ${params.entry}` }
213+ : { ok: true, exists: false };
214+}
215+} catch {
216+return {
217+ok: false,
218+error: `${params.label} escapes plugin directory: ${params.entry}`,
219+};
220+}
221+222+const opened = await openBoundaryFile({
223+ absolutePath,
224+rootPath: params.packageDir,
225+boundaryLabel: "plugin package directory",
226+});
227+if (!opened.ok) {
228+return matchBoundaryFileOpenFailure(opened, {
229+path: () => ({ ok: false, error: `${params.label} not found: ${params.entry}` }),
230+io: () => ({ ok: false, error: `${params.label} unreadable: ${params.entry}` }),
231+validation: () => ({
232+ok: false,
233+error: `${params.label} failed plugin directory boundary checks: ${params.entry}`,
234+}),
235+fallback: () => ({
236+ok: false,
237+error: `${params.label} failed plugin directory boundary checks: ${params.entry}`,
238+}),
239+});
240+}
241+fsSync.closeSync(opened.fd);
242+return { ok: true, exists: true };
243+}
244+245+async function validatePackageExtensionEntries(params: {
246+packageDir: string;
247+extensions: string[];
248+manifest: PackageManifest;
249+}): Promise<{ ok: true } | { ok: false; error: string; code: PluginInstallErrorCode }> {
250+const packageMetadata = getPackageManifestMetadata(params.manifest);
251+const runtimeExtensions = Array.isArray(packageMetadata?.runtimeExtensions)
252+ ? packageMetadata.runtimeExtensions
253+.map((entry) => normalizeOptionalString(entry) ?? "")
254+.filter(Boolean)
255+ : [];
256+const useRuntimeExtensions = runtimeExtensions.length === params.extensions.length;
257+258+for (const [index, entry] of params.extensions.entries()) {
259+const sourceEntry = await validatePackageExtensionEntry({
260+packageDir: params.packageDir,
261+ entry,
262+label: "extension entry",
263+requireExisting: false,
264+});
265+if (!sourceEntry.ok) {
266+return {
267+ok: false,
268+error: sourceEntry.error,
269+code: PLUGIN_INSTALL_ERROR_CODE.INVALID_OPENCLAW_EXTENSIONS,
270+};
271+}
272+273+const runtimeEntry = useRuntimeExtensions ? runtimeExtensions[index] : undefined;
274+if (runtimeEntry) {
275+const runtimeResult = await validatePackageExtensionEntry({
276+packageDir: params.packageDir,
277+entry: runtimeEntry,
278+label: "runtime extension entry",
279+requireExisting: true,
280+});
281+if (!runtimeResult.ok) {
282+return {
283+ok: false,
284+error: runtimeResult.error,
285+code: PLUGIN_INSTALL_ERROR_CODE.INVALID_OPENCLAW_EXTENSIONS,
286+};
287+}
288+continue;
289+}
290+291+if (sourceEntry.exists) {
292+continue;
293+}
294+295+let foundBuiltEntry = false;
296+for (const builtEntry of listBuiltRuntimeEntryCandidates(entry)) {
297+const builtResult = await validatePackageExtensionEntry({
298+packageDir: params.packageDir,
299+entry: builtEntry,
300+label: "inferred runtime extension entry",
301+requireExisting: false,
302+});
303+if (!builtResult.ok) {
304+return {
305+ok: false,
306+error: builtResult.error,
307+code: PLUGIN_INSTALL_ERROR_CODE.INVALID_OPENCLAW_EXTENSIONS,
308+};
309+}
310+if (builtResult.exists) {
311+foundBuiltEntry = true;
312+break;
313+}
314+}
315+316+if (!foundBuiltEntry) {
317+return {
318+ok: false,
319+error: `extension entry not found: ${entry}`,
320+code: PLUGIN_INSTALL_ERROR_CODE.INVALID_OPENCLAW_EXTENSIONS,
321+};
322+}
323+}
324+325+return { ok: true };
326+}
327+189328function isNpmPackageNotFoundMessage(error: string): boolean {
190329const normalized = error.trim();
191330if (normalized.startsWith("Package not found on npm:")) {
@@ -766,6 +905,15 @@ async function installPluginFromPackageDir(
766905};
767906}
768907908+const extensionValidation = await validatePackageExtensionEntries({
909+packageDir: params.packageDir,
910+ extensions,
911+ manifest,
912+});
913+if (!extensionValidation.ok) {
914+return extensionValidation;
915+}
916+769917const targetResult = await resolvePreparedDirectoryInstallTarget({
770918 runtime,
771919 pluginId,
@@ -819,18 +967,6 @@ async function installPluginFromPackageDir(
819967hasDeps: Object.keys(deps).length > 0,
820968depsLogMessage: "Installing plugin dependencies…",
821969nameEncoder: encodePluginInstallDirName,
822-afterCopy: async (installedDir) => {
823-for (const entry of extensions) {
824-const resolvedEntry = path.resolve(installedDir, entry);
825-if (!runtime.isPathInside(installedDir, resolvedEntry)) {
826-logger.warn?.(`extension entry escapes plugin directory: ${entry}`);
827-continue;
828-}
829-if (!(await runtime.fileExists(resolvedEntry))) {
830-logger.warn?.(`extension entry not found: ${entry}`);
831-}
832-}
833-},
834970afterInstall: async (installedDir) => {
835971// Run the dependency-tree security scan BEFORE linking peer deps.
836972// The scan rejects any node_modules/ symlink whose target resolves
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。