refactor: harden outbound, matrix bootstrap, and plugin entry resolution

This commit is contained in:
Peter Steinberger
2026-03-02 19:54:58 +00:00
parent a351ab2481
commit c424836fbe
14 changed files with 194 additions and 65 deletions

View File

@@ -4,7 +4,9 @@ import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
import { resolveConfigDir, resolveUserPath } from "../utils.js";
import { resolveBundledPluginsDir } from "./bundled-dir.js";
import {
DEFAULT_PLUGIN_ENTRY_CANDIDATES,
getPackageManifestMetadata,
resolvePackageExtensionEntries,
type OpenClawPackageManifest,
type PackageManifest,
} from "./manifest.js";
@@ -243,14 +245,6 @@ function readPackageManifest(dir: string): PackageManifest | null {
}
}
function resolvePackageExtensions(manifest: PackageManifest): string[] {
const raw = getPackageManifestMetadata(manifest)?.extensions;
if (!Array.isArray(raw)) {
return [];
}
return raw.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean);
}
function deriveIdHint(params: {
filePath: string;
packageName?: string;
@@ -394,7 +388,8 @@ function discoverInDirectory(params: {
}
const manifest = readPackageManifest(fullPath);
const extensions = manifest ? resolvePackageExtensions(manifest) : [];
const extensionResolution = resolvePackageExtensionEntries(manifest ?? undefined);
const extensions = extensionResolution.status === "ok" ? extensionResolution.entries : [];
if (extensions.length > 0) {
for (const extPath of extensions) {
@@ -428,8 +423,7 @@ function discoverInDirectory(params: {
continue;
}
const indexCandidates = ["index.ts", "index.js", "index.mjs", "index.cjs"];
const indexFile = indexCandidates
const indexFile = [...DEFAULT_PLUGIN_ENTRY_CANDIDATES]
.map((candidate) => path.join(fullPath, candidate))
.find((candidate) => fs.existsSync(candidate));
if (indexFile && isExtensionFile(indexFile)) {
@@ -495,7 +489,8 @@ function discoverFromPath(params: {
if (stat.isDirectory()) {
const manifest = readPackageManifest(resolved);
const extensions = manifest ? resolvePackageExtensions(manifest) : [];
const extensionResolution = resolvePackageExtensionEntries(manifest ?? undefined);
const extensions = extensionResolution.status === "ok" ? extensionResolution.entries : [];
if (extensions.length > 0) {
for (const extPath of extensions) {
@@ -529,8 +524,7 @@ function discoverFromPath(params: {
return;
}
const indexCandidates = ["index.ts", "index.js", "index.mjs", "index.cjs"];
const indexFile = indexCandidates
const indexFile = [...DEFAULT_PLUGIN_ENTRY_CANDIDATES]
.map((candidate) => path.join(resolved, candidate))
.find((candidate) => fs.existsSync(candidate));

View File

@@ -1,6 +1,5 @@
import fs from "node:fs/promises";
import path from "node:path";
import { MANIFEST_KEY } from "../compat/legacy-names.js";
import { fileExists, readJsonFile, resolveArchiveKind } from "../infra/archive.js";
import { writeFileFromPathWithinRoot } from "../infra/fs-safe.js";
import { resolveExistingInstallPath, withExtractedArchiveRoot } from "../infra/install-flow.js";
@@ -31,18 +30,20 @@ import { validateRegistryNpmSpec } from "../infra/npm-registry-spec.js";
import { extensionUsesSkippedScannerPath, isPathInside } from "../security/scan-paths.js";
import * as skillScanner from "../security/skill-scanner.js";
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
import { loadPluginManifest } from "./manifest.js";
import {
loadPluginManifest,
resolvePackageExtensionEntries,
type PackageManifest as PluginPackageManifest,
} from "./manifest.js";
type PluginInstallLogger = {
info?: (message: string) => void;
warn?: (message: string) => void;
};
type PackageManifest = {
name?: string;
version?: string;
type PackageManifest = PluginPackageManifest & {
dependencies?: Record<string, string>;
} & Partial<Record<typeof MANIFEST_KEY, { extensions?: string[] }>>;
};
const MISSING_EXTENSIONS_ERROR =
'package.json missing openclaw.extensions; update the plugin package to include openclaw.extensions (for example ["./dist/index.js"]). See https://docs.openclaw.ai/help/troubleshooting#plugin-install-fails-with-missing-openclaw-extensions';
@@ -86,15 +87,14 @@ function validatePluginId(pluginId: string): string | null {
}
function ensureOpenClawExtensions(params: { manifest: PackageManifest }): string[] {
const extensions = params.manifest[MANIFEST_KEY]?.extensions;
if (!Array.isArray(extensions)) {
const resolved = resolvePackageExtensionEntries(params.manifest);
if (resolved.status === "missing") {
throw new Error(MISSING_EXTENSIONS_ERROR);
}
const list = extensions.map((e) => (typeof e === "string" ? e.trim() : "")).filter(Boolean);
if (list.length === 0) {
if (resolved.status === "empty") {
throw new Error("package.json openclaw.extensions is empty");
}
return list;
return resolved.entries;
}
function buildFileInstallResult(pluginId: string, targetFile: string): InstallPluginResult {

View File

@@ -148,6 +148,18 @@ export type OpenClawPackageManifest = {
install?: PluginPackageInstall;
};
export const DEFAULT_PLUGIN_ENTRY_CANDIDATES = [
"index.ts",
"index.js",
"index.mjs",
"index.cjs",
] as const;
export type PackageExtensionResolution =
| { status: "ok"; entries: string[] }
| { status: "missing"; entries: [] }
| { status: "empty"; entries: [] };
export type ManifestKey = typeof MANIFEST_KEY;
export type PackageManifest = {
@@ -164,3 +176,19 @@ export function getPackageManifestMetadata(
}
return manifest[MANIFEST_KEY];
}
export function resolvePackageExtensionEntries(
manifest: PackageManifest | undefined,
): PackageExtensionResolution {
const raw = getPackageManifestMetadata(manifest)?.extensions;
if (!Array.isArray(raw)) {
return { status: "missing", entries: [] };
}
const entries = raw
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
.filter(Boolean);
if (entries.length === 0) {
return { status: "empty", entries: [] };
}
return { status: "ok", entries };
}