mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix: discover source-only plugins in checkouts
This commit is contained in:
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Plugins/source checkout: discover source-only plugins such as Codex from the `extensions/*` workspace while using npm package excludes as the packaged-core boundary, removing the stale `includeInCore` metadata path.
|
||||
- Control UI: allow deployments to configure grouped chat message max-width with a validated `gateway.controlUi.chatMessageMaxWidth` setting instead of patching bundled CSS after upgrades. Fixes #67935. Thanks @xiew4589-lang.
|
||||
- Control UI/Cron: ignore malformed persisted cron rows without valid payloads before they enter UI state and guard stale cron render paths, preventing blank Control UI sections after a bad cron snapshot. Fixes #55047 and #54439; supersedes #54550 and #54552.
|
||||
- Control UI/sessions: bound the default Sessions tab query to recent activity and fewer rows, avoiding expensive full-history loads while keeping filters editable. Fixes #76050. (#76051) Thanks @Neomail2.
|
||||
|
||||
@@ -26,9 +26,6 @@
|
||||
"defaultChoice": "npm",
|
||||
"minHostVersion": ">=2026.5.1-beta.1"
|
||||
},
|
||||
"bundle": {
|
||||
"includeInCore": false
|
||||
},
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.5.2"
|
||||
},
|
||||
|
||||
@@ -35,9 +35,6 @@
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.2"
|
||||
},
|
||||
"bundle": {
|
||||
"includeInCore": false
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
"publishToNpm": true
|
||||
|
||||
@@ -69,9 +69,6 @@
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.2"
|
||||
},
|
||||
"bundle": {
|
||||
"includeInCore": true
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
"publishToNpm": true
|
||||
|
||||
@@ -55,9 +55,6 @@
|
||||
"build": {
|
||||
"openclawVersion": "2026.5.2"
|
||||
},
|
||||
"bundle": {
|
||||
"includeInCore": false
|
||||
},
|
||||
"release": {
|
||||
"publishToClawHub": true,
|
||||
"publishToNpm": true
|
||||
|
||||
@@ -316,9 +316,7 @@ function resolveStatus({ dirName, packageJson, excludedDirs }) {
|
||||
const hasInstallSpec =
|
||||
typeof packageJson.openclaw?.install?.clawhubSpec === "string" ||
|
||||
typeof packageJson.openclaw?.install?.npmSpec === "string";
|
||||
const excluded =
|
||||
excludedDirs.has(dirName) || packageJson.openclaw?.bundle?.includeInCore === false;
|
||||
if (!excluded) {
|
||||
if (!excludedDirs.has(dirName)) {
|
||||
return "core";
|
||||
}
|
||||
if (release?.publishToClawHub === true || release?.publishToNpm === true || hasInstallSpec) {
|
||||
|
||||
@@ -35,14 +35,7 @@ function hasReleasedBundledInstall(packageJson) {
|
||||
);
|
||||
}
|
||||
|
||||
function isExplicitlyDownloadablePlugin(packageJson) {
|
||||
return packageJson?.openclaw?.bundle?.includeInCore === false;
|
||||
}
|
||||
|
||||
export function shouldBuildBundledCluster(cluster, env = process.env, options = {}) {
|
||||
if (isExplicitlyDownloadablePlugin(options.packageJson)) {
|
||||
return false;
|
||||
}
|
||||
if (hasReleasedBundledInstall(options.packageJson)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -171,9 +171,6 @@ function collectInternalizedBundledExtensionRuntimeDependencies(repoRoot, rootPa
|
||||
continue;
|
||||
}
|
||||
const packageJson = readJson(packageJsonPath);
|
||||
if (packageJson?.openclaw?.bundle?.includeInCore === false) {
|
||||
continue;
|
||||
}
|
||||
for (const section of ["dependencies", "optionalDependencies"]) {
|
||||
for (const depName of Object.keys(packageJson[section] ?? {})) {
|
||||
const existing = dependencies.get(depName) ?? [];
|
||||
|
||||
@@ -215,6 +215,7 @@ describe("package dist inventory", () => {
|
||||
"bundled-chat",
|
||||
"package.json",
|
||||
);
|
||||
const rootPackageJson = path.join(packageRoot, "package.json");
|
||||
|
||||
await fs.mkdir(path.dirname(externalizedRuntime), { recursive: true });
|
||||
await fs.mkdir(path.dirname(bundledRuntime), { recursive: true });
|
||||
@@ -222,6 +223,13 @@ describe("package dist inventory", () => {
|
||||
await fs.mkdir(path.dirname(bundledPackageJson), { recursive: true });
|
||||
await fs.writeFile(externalizedRuntime, "export {};\n", "utf8");
|
||||
await fs.writeFile(bundledRuntime, "export {};\n", "utf8");
|
||||
await fs.writeFile(
|
||||
rootPackageJson,
|
||||
JSON.stringify({
|
||||
files: ["dist/", "!dist/extensions/external-chat/**"],
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
externalizedPackageJson,
|
||||
JSON.stringify({
|
||||
@@ -263,9 +271,6 @@ describe("package dist inventory", () => {
|
||||
JSON.stringify({
|
||||
name: "@openclaw/core-chat",
|
||||
openclaw: {
|
||||
bundle: {
|
||||
includeInCore: true,
|
||||
},
|
||||
release: {
|
||||
publishToClawHub: true,
|
||||
publishToNpm: true,
|
||||
|
||||
@@ -75,6 +75,27 @@ export function isLegacyPluginDependencyInstallStagePath(relativePath: string):
|
||||
);
|
||||
}
|
||||
|
||||
function collectExcludedPackagedExtensionDirs(rootPackageJson: unknown): Set<string> {
|
||||
if (!rootPackageJson || typeof rootPackageJson !== "object") {
|
||||
return new Set();
|
||||
}
|
||||
const files = (rootPackageJson as { files?: unknown }).files;
|
||||
if (!Array.isArray(files)) {
|
||||
return new Set();
|
||||
}
|
||||
const excluded = new Set<string>();
|
||||
for (const entry of files) {
|
||||
if (typeof entry !== "string") {
|
||||
continue;
|
||||
}
|
||||
const match = /^!dist\/extensions\/([^/]+)\/\*\*$/u.exec(entry);
|
||||
if (match?.[1]) {
|
||||
excluded.add(match[1]);
|
||||
}
|
||||
}
|
||||
return excluded;
|
||||
}
|
||||
|
||||
function isExternalizedBundledExtensionDistPath(
|
||||
relativePath: string,
|
||||
externalizedExtensionIds: ExternalizedBundledExtensionIds,
|
||||
@@ -92,65 +113,19 @@ function isExternalizedBundledExtensionDistPath(
|
||||
);
|
||||
}
|
||||
|
||||
function isPublishableExternalizedBundledManifest(value: unknown): boolean {
|
||||
if (!value || typeof value !== "object") {
|
||||
return false;
|
||||
}
|
||||
const openclaw = (value as { openclaw?: unknown }).openclaw;
|
||||
if (!openclaw || typeof openclaw !== "object") {
|
||||
return false;
|
||||
}
|
||||
const release = (openclaw as { release?: unknown }).release;
|
||||
if (!release || typeof release !== "object") {
|
||||
return false;
|
||||
}
|
||||
const bundle = (openclaw as { bundle?: unknown }).bundle;
|
||||
if (
|
||||
bundle &&
|
||||
typeof bundle === "object" &&
|
||||
(bundle as { includeInCore?: unknown }).includeInCore === true
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const typedRelease = release as { publishToClawHub?: unknown; publishToNpm?: unknown };
|
||||
return typedRelease.publishToNpm === true || typedRelease.publishToClawHub === true;
|
||||
}
|
||||
|
||||
async function collectExternalizedBundledExtensionIds(
|
||||
packageRoot: string,
|
||||
): Promise<ExternalizedBundledExtensionIds> {
|
||||
const extensionsDir = path.join(packageRoot, "extensions");
|
||||
let entries: import("node:fs").Dirent[];
|
||||
const packageJsonPath = path.join(packageRoot, "package.json");
|
||||
try {
|
||||
entries = await fs.readdir(extensionsDir, { withFileTypes: true });
|
||||
const parsed = JSON.parse(await fs.readFile(packageJsonPath, "utf8")) as unknown;
|
||||
return collectExcludedPackagedExtensionDirs(parsed);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return new Set();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const ids = new Set<string>();
|
||||
await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
if (!entry.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
const packageJsonPath = path.join(extensionsDir, entry.name, "package.json");
|
||||
try {
|
||||
const parsed = JSON.parse(await fs.readFile(packageJsonPath, "utf8")) as unknown;
|
||||
if (isPublishableExternalizedBundledManifest(parsed)) {
|
||||
ids.add(entry.name);
|
||||
}
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
);
|
||||
return ids;
|
||||
}
|
||||
|
||||
function isPackagedDistPath(
|
||||
|
||||
@@ -10,15 +10,37 @@ import {
|
||||
hasBundledPluginContractSnapshotCapabilities,
|
||||
} from "./contracts/inventory/bundled-capability-metadata.js";
|
||||
import { pluginTestRepoRoot as repoRoot } from "./generated-plugin-test-helpers.js";
|
||||
import { isPackageIncludedInCoreBundle, type OpenClawPackageManifest } from "./manifest.js";
|
||||
import type { OpenClawPackageManifest } from "./manifest.js";
|
||||
import type { PluginManifest } from "./manifest.js";
|
||||
|
||||
function collectExcludedPackagedExtensionDirs(): ReadonlySet<string> {
|
||||
const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf-8")) as {
|
||||
files?: unknown;
|
||||
};
|
||||
if (!Array.isArray(packageJson.files)) {
|
||||
return new Set();
|
||||
}
|
||||
const excluded = new Set<string>();
|
||||
for (const entry of packageJson.files) {
|
||||
if (typeof entry !== "string") {
|
||||
continue;
|
||||
}
|
||||
const match = /^!dist\/extensions\/([^/]+)\/\*\*$/u.exec(entry);
|
||||
if (match?.[1]) {
|
||||
excluded.add(match[1]);
|
||||
}
|
||||
}
|
||||
return excluded;
|
||||
}
|
||||
|
||||
function readManifestRecords(): PluginManifest[] {
|
||||
const extensionsDir = path.join(repoRoot, "extensions");
|
||||
const excludedDirs = collectExcludedPackagedExtensionDirs();
|
||||
return fs
|
||||
.readdirSync(extensionsDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => path.join(extensionsDir, entry.name))
|
||||
.filter((pluginDir) => !excludedDirs.has(path.basename(pluginDir)))
|
||||
.filter((pluginDir) => {
|
||||
const packagePath = path.join(pluginDir, "package.json");
|
||||
if (!fs.existsSync(packagePath)) {
|
||||
@@ -27,9 +49,6 @@ function readManifestRecords(): PluginManifest[] {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf-8")) as {
|
||||
openclaw?: OpenClawPackageManifest;
|
||||
};
|
||||
if (!isPackageIncludedInCoreBundle(packageJson.openclaw)) {
|
||||
return false;
|
||||
}
|
||||
return normalizeBundledPluginStringList(packageJson.openclaw?.extensions).length > 0;
|
||||
})
|
||||
.map(
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from "../../bundled-plugin-scan.js";
|
||||
import {
|
||||
getPackageManifestMetadata,
|
||||
isPackageIncludedInCoreBundle,
|
||||
PLUGIN_MANIFEST_FILENAME,
|
||||
type PackageManifest,
|
||||
type PluginManifest,
|
||||
@@ -70,15 +69,32 @@ function readJsonRecord(filePath: string): Record<string, unknown> | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
function isExplicitlyDownloadablePlugin(packageJson: Record<string, unknown> | undefined): boolean {
|
||||
return !isPackageIncludedInCoreBundle(getPackageManifestMetadata(packageJson as PackageManifest));
|
||||
function collectExcludedPackagedExtensionDirs(): ReadonlySet<string> {
|
||||
const packageJson = readJsonRecord(path.join(OPENCLAW_PACKAGE_ROOT, "package.json"));
|
||||
const files = packageJson?.files;
|
||||
if (!Array.isArray(files)) {
|
||||
return new Set();
|
||||
}
|
||||
const excluded = new Set<string>();
|
||||
for (const entry of files) {
|
||||
if (typeof entry !== "string") {
|
||||
continue;
|
||||
}
|
||||
const match = /^!dist\/extensions\/([^/]+)\/\*\*$/u.exec(entry);
|
||||
if (match?.[1]) {
|
||||
excluded.add(match[1]);
|
||||
}
|
||||
}
|
||||
return excluded;
|
||||
}
|
||||
|
||||
const EXCLUDED_PACKAGED_EXTENSION_DIRS = collectExcludedPackagedExtensionDirs();
|
||||
|
||||
function readBundledCapabilityManifest(pluginDir: string): BundledCapabilityManifest | undefined {
|
||||
const packageJson = readJsonRecord(path.join(pluginDir, "package.json"));
|
||||
if (isExplicitlyDownloadablePlugin(packageJson)) {
|
||||
if (EXCLUDED_PACKAGED_EXTENSION_DIRS.has(path.basename(pluginDir))) {
|
||||
return undefined;
|
||||
}
|
||||
const packageJson = readJsonRecord(path.join(pluginDir, "package.json"));
|
||||
const packageManifest = getPackageManifestMetadata(packageJson as PackageManifest);
|
||||
const extensions = normalizeBundledPluginStringList(packageManifest?.extensions);
|
||||
if (extensions.length === 0) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { uniqueSortedStrings } from "../../plugin-sdk/test-helpers/string-utils.js";
|
||||
import { loadPluginManifestRegistry, type PluginManifestRecord } from "../manifest-registry.js";
|
||||
import { isPackageIncludedInCoreBundle } from "../manifest.js";
|
||||
import { resolveManifestContractPluginIds } from "../plugin-registry.js";
|
||||
import {
|
||||
pluginRegistrationContractRegistry,
|
||||
@@ -25,11 +24,7 @@ describe("plugin contract registry", () => {
|
||||
|
||||
function resolveBundledManifestPluginIds(predicate: (plugin: PluginManifestRecord) => boolean) {
|
||||
return loadPluginManifestRegistry({})
|
||||
.plugins.filter(
|
||||
(plugin) =>
|
||||
(plugin.origin !== "bundled" || isPackageIncludedInCoreBundle(plugin.packageManifest)) &&
|
||||
predicate(plugin),
|
||||
)
|
||||
.plugins.filter((plugin) => predicate(plugin))
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
@@ -953,7 +953,7 @@ describe("discoverOpenClawPlugins", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("skips bundled package plugins that are externalized from core", () => {
|
||||
it("discovers present bundled package plugins without package metadata gates", () => {
|
||||
const stateDir = makeTempDir();
|
||||
const bundledDir = path.join(stateDir, "bundled");
|
||||
const pluginDir = path.join(bundledDir, "downloadable");
|
||||
@@ -964,9 +964,6 @@ describe("discoverOpenClawPlugins", () => {
|
||||
name: "@openclaw/downloadable",
|
||||
openclaw: {
|
||||
extensions: ["./index.ts"],
|
||||
bundle: {
|
||||
includeInCore: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
@@ -980,7 +977,64 @@ describe("discoverOpenClawPlugins", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
expectCandidateIds(candidates, { excludes: ["downloadable"] });
|
||||
expectCandidateIds(candidates, { includes: ["downloadable"] });
|
||||
});
|
||||
|
||||
it("discovers source-checkout-only bundled plugins alongside built bundled plugins", () => {
|
||||
const stateDir = makeTempDir();
|
||||
const packageRoot = path.join(stateDir, "openclaw");
|
||||
const bundledDir = path.join(packageRoot, "dist", "extensions");
|
||||
const sourceDir = path.join(packageRoot, "extensions");
|
||||
const builtPluginDir = path.join(bundledDir, "shipped");
|
||||
const sourceBuiltPluginDir = path.join(sourceDir, "shipped");
|
||||
const sourceOnlyPluginDir = path.join(sourceDir, "downloadable");
|
||||
mkdirSafe(path.join(packageRoot, "src"));
|
||||
mkdirSafe(builtPluginDir);
|
||||
mkdirSafe(sourceBuiltPluginDir);
|
||||
mkdirSafe(sourceOnlyPluginDir);
|
||||
fs.writeFileSync(path.join(packageRoot, ".git"), "gitdir: /tmp/fake.git\n", "utf-8");
|
||||
fs.writeFileSync(path.join(packageRoot, "pnpm-workspace.yaml"), "packages: []\n", "utf-8");
|
||||
|
||||
writePluginPackageManifest({
|
||||
packageDir: builtPluginDir,
|
||||
packageName: "@openclaw/shipped",
|
||||
extensions: ["./index.js"],
|
||||
});
|
||||
writePluginManifest({ pluginDir: builtPluginDir, id: "shipped" });
|
||||
writePluginEntry(path.join(builtPluginDir, "index.js"));
|
||||
writePluginPackageManifest({
|
||||
packageDir: sourceBuiltPluginDir,
|
||||
packageName: "@openclaw/shipped",
|
||||
extensions: ["./index.ts"],
|
||||
});
|
||||
writePluginManifest({ pluginDir: sourceBuiltPluginDir, id: "shipped" });
|
||||
writePluginEntry(path.join(sourceBuiltPluginDir, "index.ts"));
|
||||
fs.writeFileSync(
|
||||
path.join(sourceOnlyPluginDir, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "@openclaw/downloadable",
|
||||
openclaw: {
|
||||
extensions: ["./index.ts"],
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
writePluginManifest({ pluginDir: sourceOnlyPluginDir, id: "downloadable" });
|
||||
writePluginEntry(path.join(sourceOnlyPluginDir, "index.ts"));
|
||||
|
||||
const { candidates } = discoverOpenClawPlugins({
|
||||
env: buildDiscoveryEnvWithOverrides(stateDir, {
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: bundledDir,
|
||||
}),
|
||||
});
|
||||
|
||||
expectCandidateIds(candidates, { includes: ["shipped", "downloadable"] });
|
||||
expect(fs.realpathSync(findCandidateById(candidates, "shipped")?.source ?? "")).toBe(
|
||||
fs.realpathSync(path.join(builtPluginDir, "index.js")),
|
||||
);
|
||||
expect(fs.realpathSync(findCandidateById(candidates, "downloadable")?.source ?? "")).toBe(
|
||||
fs.realpathSync(path.join(sourceOnlyPluginDir, "index.ts")),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not discover nested node_modules copies under installed plugins", async () => {
|
||||
|
||||
@@ -9,13 +9,15 @@ import {
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { detectBundleManifestFormat, loadBundleManifest } from "./bundle-manifest.js";
|
||||
import { resolveSourceCheckoutDependencyDiagnostic } from "./bundled-dir.js";
|
||||
import { resolvePackagedBundledLoadPathAlias } from "./bundled-load-path-aliases.js";
|
||||
import {
|
||||
buildLegacyBundledRootPath,
|
||||
resolvePackagedBundledLoadPathAlias,
|
||||
} from "./bundled-load-path-aliases.js";
|
||||
import { listBundledSourceOverlayDirs } from "./bundled-source-overlays.js";
|
||||
import type { PluginBundleFormat, PluginDiagnostic, PluginFormat } from "./manifest-types.js";
|
||||
import {
|
||||
DEFAULT_PLUGIN_ENTRY_CANDIDATES,
|
||||
getPackageManifestMetadata,
|
||||
isPackageIncludedInCoreBundle,
|
||||
loadPluginManifest,
|
||||
type PluginManifest,
|
||||
resolvePackageExtensionEntries,
|
||||
@@ -627,10 +629,6 @@ function discoverInDirectory(params: {
|
||||
const rejectHardlinks = params.origin !== "bundled";
|
||||
const fullPathRealPath = safeRealpathSync(fullPath, params.realpathCache) ?? undefined;
|
||||
const manifest = readPackageManifest(fullPath, rejectHardlinks, fullPathRealPath);
|
||||
const packageManifest = getPackageManifestMetadata(manifest ?? undefined);
|
||||
if (params.origin === "bundled" && !isPackageIncludedInCoreBundle(packageManifest)) {
|
||||
continue;
|
||||
}
|
||||
const extensionResolution = resolvePackageExtensionEntries(manifest ?? undefined);
|
||||
const extensions = extensionResolution.status === "ok" ? extensionResolution.entries : [];
|
||||
const manifestId = resolveIdHintManifestId(fullPath, rejectHardlinks, fullPathRealPath);
|
||||
@@ -725,6 +723,61 @@ function discoverInDirectory(params: {
|
||||
}
|
||||
}
|
||||
|
||||
function hasDiscoverablePluginTree(pluginsDir: string): boolean {
|
||||
try {
|
||||
return fs.readdirSync(pluginsDir, { withFileTypes: true }).some((entry) => {
|
||||
if (!entry.isDirectory()) {
|
||||
return false;
|
||||
}
|
||||
const pluginDir = path.join(pluginsDir, entry.name);
|
||||
return (
|
||||
fs.existsSync(path.join(pluginDir, "package.json")) ||
|
||||
fs.existsSync(path.join(pluginDir, "openclaw.plugin.json"))
|
||||
);
|
||||
});
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isSourceCheckoutExtensionsDir(extensionsDir: string): boolean {
|
||||
const packageRoot = path.dirname(extensionsDir);
|
||||
return (
|
||||
fs.existsSync(path.join(packageRoot, ".git")) &&
|
||||
fs.existsSync(path.join(packageRoot, "pnpm-workspace.yaml")) &&
|
||||
fs.existsSync(path.join(packageRoot, "src")) &&
|
||||
fs.existsSync(extensionsDir) &&
|
||||
hasDiscoverablePluginTree(extensionsDir)
|
||||
);
|
||||
}
|
||||
|
||||
function resolveBundledSourceCheckoutExtensionsDir(bundledRoot?: string): string | undefined {
|
||||
if (!bundledRoot) {
|
||||
return undefined;
|
||||
}
|
||||
const legacyRoot = buildLegacyBundledRootPath(bundledRoot);
|
||||
if (!legacyRoot || !isSourceCheckoutExtensionsDir(legacyRoot)) {
|
||||
return undefined;
|
||||
}
|
||||
return legacyRoot;
|
||||
}
|
||||
|
||||
function readChildDirectoryNames(dir: string | undefined): Set<string> {
|
||||
if (!dir || !fs.existsSync(dir)) {
|
||||
return new Set();
|
||||
}
|
||||
try {
|
||||
return new Set(
|
||||
fs
|
||||
.readdirSync(dir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name),
|
||||
);
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
function discoverFromPath(params: {
|
||||
rawPath: string;
|
||||
origin: PluginOrigin;
|
||||
@@ -996,6 +1049,24 @@ export function discoverOpenClawPlugins(params: {
|
||||
realpathCache,
|
||||
});
|
||||
}
|
||||
const sourceCheckoutExtensionsDir = resolveBundledSourceCheckoutExtensionsDir(roots.stock);
|
||||
const sourceCheckoutMatchesBundledRoot = resolvesToSameDirectory(
|
||||
sourceCheckoutExtensionsDir,
|
||||
roots.stock,
|
||||
realpathCache,
|
||||
);
|
||||
if (sourceCheckoutExtensionsDir && !sourceCheckoutMatchesBundledRoot) {
|
||||
discoverInDirectory({
|
||||
dir: sourceCheckoutExtensionsDir,
|
||||
origin: "bundled",
|
||||
ownershipUid: params.ownershipUid,
|
||||
candidates: result.candidates,
|
||||
diagnostics: result.diagnostics,
|
||||
seen,
|
||||
realpathCache,
|
||||
skipDirectories: readChildDirectoryNames(roots.stock),
|
||||
});
|
||||
}
|
||||
for (const installedPath of collectInstalledPluginRecordPaths(params.installRecords, env)) {
|
||||
discoverFromPath({
|
||||
rawPath: installedPath,
|
||||
|
||||
@@ -51,8 +51,6 @@ export function diffInstalledPluginIndexInvalidationReasons(
|
||||
}
|
||||
if (
|
||||
previousPlugin.packageVersion !== currentPlugin.packageVersion ||
|
||||
hashJson(previousPlugin.packageBundle ?? {}) !==
|
||||
hashJson(currentPlugin.packageBundle ?? {}) ||
|
||||
previousPlugin.packageJson?.path !== currentPlugin.packageJson?.path ||
|
||||
previousPlugin.packageJson?.hash !== currentPlugin.packageJson?.hash
|
||||
) {
|
||||
|
||||
@@ -11,13 +11,12 @@ import { hasOptionalMissingPluginManifestFile } from "./installed-plugin-index-m
|
||||
import type {
|
||||
InstalledPluginIndexRecord,
|
||||
InstalledPluginInstallRecordInfo,
|
||||
InstalledPluginPackageBundleInfo,
|
||||
InstalledPluginPackageChannelInfo,
|
||||
InstalledPluginStartupInfo,
|
||||
} from "./installed-plugin-index-types.js";
|
||||
import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js";
|
||||
import type { PluginDiagnostic } from "./manifest-types.js";
|
||||
import type { OpenClawPackageBundle, PluginPackageChannel } from "./manifest.js";
|
||||
import type { PluginPackageChannel } from "./manifest.js";
|
||||
import { safeRealpathSync } from "./path-safety.js";
|
||||
import { hasKind } from "./slots.js";
|
||||
|
||||
@@ -151,17 +150,6 @@ function normalizePackageChannel(
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePackageBundle(
|
||||
bundle: OpenClawPackageBundle | undefined,
|
||||
): InstalledPluginPackageBundleInfo | undefined {
|
||||
if (typeof bundle?.includeInCore !== "boolean") {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
includeInCore: bundle.includeInCore,
|
||||
};
|
||||
}
|
||||
|
||||
function hashManifestlessBundleRecord(record: PluginManifestRecord): string {
|
||||
return hashJson({
|
||||
id: record.id,
|
||||
@@ -223,7 +211,6 @@ export function buildInstalledPluginIndexRecords(params: {
|
||||
const packageChannel = normalizePackageChannel(
|
||||
record.packageChannel ?? candidate?.packageManifest?.channel,
|
||||
);
|
||||
const packageBundle = normalizePackageBundle(candidate?.packageManifest?.bundle);
|
||||
const manifestHash = resolveManifestHash({ record, diagnostics: params.diagnostics });
|
||||
const manifestFile = hasOptionalMissingPluginManifestFile(record)
|
||||
? undefined
|
||||
@@ -283,9 +270,6 @@ export function buildInstalledPluginIndexRecords(params: {
|
||||
if (packageChannel) {
|
||||
indexRecord.packageChannel = packageChannel;
|
||||
}
|
||||
if (packageBundle) {
|
||||
indexRecord.packageBundle = packageBundle;
|
||||
}
|
||||
if (packageJson) {
|
||||
indexRecord.packageJson = packageJson;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,6 @@ const InstalledPluginIndexRecordSchema = z.object({
|
||||
installRecordHash: z.string().optional(),
|
||||
packageInstall: z.unknown().optional(),
|
||||
packageChannel: z.unknown().optional(),
|
||||
packageBundle: z.unknown().optional(),
|
||||
manifestPath: z.string(),
|
||||
manifestHash: z.string(),
|
||||
manifestFile: InstalledPluginFileSignatureSchema.optional(),
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { PluginInstallSourceInfo } from "./install-source-info.js";
|
||||
import type { InstalledPluginFileSignature } from "./installed-plugin-index-hash.js";
|
||||
import type { PluginManifestRecord } from "./manifest-registry.js";
|
||||
import type { PluginDiagnostic } from "./manifest-types.js";
|
||||
import type { OpenClawPackageBundle, PluginPackageChannel } from "./manifest.js";
|
||||
import type { PluginPackageChannel } from "./manifest.js";
|
||||
|
||||
export const INSTALLED_PLUGIN_INDEX_VERSION = 1;
|
||||
export const INSTALLED_PLUGIN_INDEX_MIGRATION_VERSION = 1;
|
||||
@@ -62,7 +62,6 @@ export type InstalledPluginInstallRecordInfo = Pick<
|
||||
>;
|
||||
|
||||
export type InstalledPluginPackageChannelInfo = PluginPackageChannel;
|
||||
export type InstalledPluginPackageBundleInfo = OpenClawPackageBundle;
|
||||
|
||||
export type InstalledPluginIndexRecord = {
|
||||
pluginId: string;
|
||||
@@ -81,7 +80,6 @@ export type InstalledPluginIndexRecord = {
|
||||
*/
|
||||
packageInstall?: PluginInstallSourceInfo;
|
||||
packageChannel?: InstalledPluginPackageChannelInfo;
|
||||
packageBundle?: InstalledPluginPackageBundleInfo;
|
||||
manifestPath: string;
|
||||
manifestHash: string;
|
||||
manifestFile?: InstalledPluginFileSignature;
|
||||
|
||||
@@ -486,37 +486,6 @@ describe("installed plugin index", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps package bundle metadata needed for core inclusion decisions", () => {
|
||||
const rootDir = makeTempDir();
|
||||
writeRuntimeEntry(rootDir);
|
||||
writePluginManifest(rootDir, {
|
||||
id: "downloadable-bundled-provider",
|
||||
providers: ["downloadable-bundled-provider"],
|
||||
configSchema: { type: "object" },
|
||||
});
|
||||
|
||||
const index = loadInstalledPluginIndex({
|
||||
candidates: [
|
||||
createPluginCandidate({
|
||||
rootDir,
|
||||
packageManifest: {
|
||||
bundle: {
|
||||
includeInCore: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
env: hermeticEnv(),
|
||||
});
|
||||
|
||||
expect(index.plugins[0]).toMatchObject({
|
||||
pluginId: "downloadable-bundled-provider",
|
||||
packageBundle: {
|
||||
includeInCore: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps packageJson paths root-relative when packageDir is reached through a symlink", () => {
|
||||
const fixture = createRichPluginFixture();
|
||||
const linkParent = makeTempDir();
|
||||
@@ -958,36 +927,6 @@ describe("installed plugin index", () => {
|
||||
expect(diffInstalledPluginIndexInvalidationReasons(previous, current)).toEqual([]);
|
||||
});
|
||||
|
||||
it("treats package bundle metadata changes as stale package metadata", () => {
|
||||
const rootDir = makeTempDir();
|
||||
writeRuntimeEntry(rootDir);
|
||||
writePluginManifest(rootDir, {
|
||||
id: "bundle-policy-demo",
|
||||
configSchema: { type: "object" },
|
||||
});
|
||||
const previous = loadInstalledPluginIndex({
|
||||
candidates: [createPluginCandidate({ rootDir })],
|
||||
env: hermeticEnv(),
|
||||
});
|
||||
const current = loadInstalledPluginIndex({
|
||||
candidates: [
|
||||
createPluginCandidate({
|
||||
rootDir,
|
||||
packageManifest: {
|
||||
bundle: {
|
||||
includeInCore: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
env: hermeticEnv(),
|
||||
});
|
||||
|
||||
expect(diffInstalledPluginIndexInvalidationReasons(previous, current)).toEqual([
|
||||
"stale-package",
|
||||
]);
|
||||
});
|
||||
|
||||
it("treats plugin index changes as source invalidation", () => {
|
||||
const fixture = createRichPluginFixture();
|
||||
const previous = loadInstalledPluginIndex({
|
||||
|
||||
@@ -224,35 +224,6 @@ describe("loadPluginManifestRegistryForInstalledIndex", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("hydrates package bundle metadata from the installed index", () => {
|
||||
const rootDir = makeTempDir();
|
||||
writePlugin(rootDir, "installed", "installed-");
|
||||
|
||||
const index = createIndex(rootDir);
|
||||
const registry = loadPluginManifestRegistryForInstalledIndex({
|
||||
index: {
|
||||
...index,
|
||||
plugins: [
|
||||
{
|
||||
...index.plugins[0],
|
||||
packageBundle: {
|
||||
includeInCore: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
env: {
|
||||
OPENCLAW_VERSION: "2026.4.25",
|
||||
VITEST: "true",
|
||||
},
|
||||
includeDisabled: true,
|
||||
});
|
||||
|
||||
expect(registry.plugins[0]?.packageManifest?.bundle).toEqual({
|
||||
includeInCore: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("round-trips bundle metadata through the persisted index before reconstruction", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const rootDir = makeTempDir();
|
||||
|
||||
@@ -59,7 +59,6 @@ function buildInstalledManifestRegistryIndexKey(index: InstalledPluginIndex) {
|
||||
installRecordHash: record.installRecordHash,
|
||||
packageInstall: record.packageInstall,
|
||||
packageChannel: record.packageChannel,
|
||||
packageBundle: record.packageBundle,
|
||||
manifestPath: record.manifestPath,
|
||||
manifestHash: record.manifestHash,
|
||||
manifestFile: safeFileSignature(record.manifestPath),
|
||||
@@ -105,13 +104,11 @@ function resolveFallbackPluginSource(record: InstalledPluginIndexRecord): string
|
||||
function resolveInstalledPackageManifest(
|
||||
record: InstalledPluginIndexRecord,
|
||||
): OpenClawPackageManifest | undefined {
|
||||
const fallbackPackageManifest =
|
||||
record.packageChannel || record.packageBundle
|
||||
? {
|
||||
...(record.packageChannel ? { channel: record.packageChannel } : {}),
|
||||
...(record.packageBundle ? { bundle: record.packageBundle } : {}),
|
||||
}
|
||||
: undefined;
|
||||
const fallbackPackageManifest = record.packageChannel
|
||||
? {
|
||||
channel: record.packageChannel,
|
||||
}
|
||||
: undefined;
|
||||
const rootDir = resolveInstalledPluginRootDir(record);
|
||||
const packageJsonPath = record.packageJson?.path
|
||||
? path.resolve(rootDir, record.packageJson.path)
|
||||
@@ -136,17 +133,9 @@ function resolveInstalledPackageManifest(
|
||||
...packageManifest.channel,
|
||||
}
|
||||
: undefined;
|
||||
const bundle =
|
||||
record.packageBundle || packageManifest.bundle
|
||||
? {
|
||||
...record.packageBundle,
|
||||
...packageManifest.bundle,
|
||||
}
|
||||
: undefined;
|
||||
return {
|
||||
...packageManifest,
|
||||
...(channel ? { channel } : {}),
|
||||
...(bundle ? { bundle } : {}),
|
||||
};
|
||||
} catch {
|
||||
return fallbackPackageManifest;
|
||||
|
||||
@@ -1708,10 +1708,6 @@ export type OpenClawPackageSetupFeatures = {
|
||||
legacySessionSurfaces?: boolean;
|
||||
};
|
||||
|
||||
export type OpenClawPackageBundle = {
|
||||
includeInCore?: boolean;
|
||||
};
|
||||
|
||||
export type OpenClawPackageManifest = {
|
||||
extensions?: string[];
|
||||
runtimeExtensions?: string[];
|
||||
@@ -1720,7 +1716,6 @@ export type OpenClawPackageManifest = {
|
||||
setupFeatures?: OpenClawPackageSetupFeatures;
|
||||
channel?: PluginPackageChannel;
|
||||
install?: PluginPackageInstall;
|
||||
bundle?: OpenClawPackageBundle;
|
||||
startup?: OpenClawPackageStartup;
|
||||
};
|
||||
|
||||
@@ -1753,12 +1748,6 @@ export function getPackageManifestMetadata(
|
||||
return manifest[MANIFEST_KEY];
|
||||
}
|
||||
|
||||
export function isPackageIncludedInCoreBundle(
|
||||
manifest: OpenClawPackageManifest | undefined,
|
||||
): boolean {
|
||||
return manifest?.bundle?.includeInCore !== false;
|
||||
}
|
||||
|
||||
export function resolvePackageExtensionEntries(
|
||||
manifest: PackageManifest | undefined,
|
||||
): PackageExtensionResolution {
|
||||
|
||||
@@ -12,7 +12,6 @@ import type {
|
||||
PluginManifestRecord,
|
||||
PluginManifestRegistry,
|
||||
} from "./manifest-registry.js";
|
||||
import { isPackageIncludedInCoreBundle } from "./manifest.js";
|
||||
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.types.js";
|
||||
import type { PluginOrigin } from "./plugin-origin.types.js";
|
||||
import {
|
||||
@@ -109,10 +108,6 @@ function sortUnique(values: Iterable<string>): string[] {
|
||||
);
|
||||
}
|
||||
|
||||
function isCoreBundledManifestSurface(plugin: PluginManifestRecord): boolean {
|
||||
return plugin.origin !== "bundled" || isPackageIncludedInCoreBundle(plugin.packageManifest);
|
||||
}
|
||||
|
||||
function collectObjectKeys(value: Record<string, unknown> | undefined): readonly string[] {
|
||||
return value ? Object.keys(value) : [];
|
||||
}
|
||||
@@ -409,7 +404,6 @@ export function resolveManifestContractPluginIds(
|
||||
.plugins.filter(
|
||||
(plugin) =>
|
||||
(!params.origin || plugin.origin === params.origin) &&
|
||||
isCoreBundledManifestSurface(plugin) &&
|
||||
listManifestContractValues(plugin, params.contract).length > 0,
|
||||
)
|
||||
.map((plugin) => plugin.id)
|
||||
@@ -427,7 +421,6 @@ export function resolveManifestContractPluginIdsByCompatibilityRuntimePath(
|
||||
.plugins.filter(
|
||||
(plugin) =>
|
||||
(!params.origin || plugin.origin === params.origin) &&
|
||||
isCoreBundledManifestSurface(plugin) &&
|
||||
listManifestContractValues(plugin, params.contract).length > 0 &&
|
||||
(plugin.configContracts?.compatibilityRuntimePaths ?? []).includes(normalizedPath),
|
||||
)
|
||||
|
||||
@@ -744,7 +744,7 @@ describe("resolvePluginProviders", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps externalized bundled providers out of core bundled compat expansion", () => {
|
||||
it("includes present bundled providers in bundled compat expansion", () => {
|
||||
setManifestPlugins([
|
||||
createManifestProviderPlugin({
|
||||
id: "google",
|
||||
@@ -753,10 +753,6 @@ describe("resolvePluginProviders", () => {
|
||||
createManifestProviderPlugin({
|
||||
id: "codex",
|
||||
providerIds: ["codex"],
|
||||
packageManifest: {
|
||||
extensions: ["./index.ts"],
|
||||
bundle: { includeInCore: false },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -770,11 +766,10 @@ describe("resolvePluginProviders", () => {
|
||||
});
|
||||
|
||||
expectResolvedAllowlistState({
|
||||
expectedAllow: ["openrouter", "google"],
|
||||
unexpectedAllow: ["codex"],
|
||||
expectedAllow: ["openrouter", "google", "codex"],
|
||||
});
|
||||
expectLastRuntimeRegistryLoad({
|
||||
onlyPluginIds: ["google"],
|
||||
onlyPluginIds: ["codex", "google"],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@ import {
|
||||
passesManifestOwnerBasePolicy,
|
||||
} from "./manifest-owner-policy.js";
|
||||
import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js";
|
||||
import { type PluginManifestRecord, type PluginManifestRegistry } from "./manifest-registry.js";
|
||||
import { isPackageIncludedInCoreBundle } from "./manifest.js";
|
||||
import type { PluginManifestRegistry } from "./manifest-registry.js";
|
||||
import {
|
||||
loadPluginRegistrySnapshot,
|
||||
normalizePluginsConfigWithRegistry,
|
||||
@@ -72,16 +71,10 @@ function resolveProviderSurfacePluginIdSet(
|
||||
resolveManifestRegistry({
|
||||
...params,
|
||||
includeDisabled: true,
|
||||
}).plugins.flatMap((plugin) =>
|
||||
plugin.providers.length > 0 && isCoreBundledManifestSurface(plugin) ? [plugin.id] : [],
|
||||
),
|
||||
}).plugins.flatMap((plugin) => (plugin.providers.length > 0 ? [plugin.id] : [])),
|
||||
);
|
||||
}
|
||||
|
||||
function isCoreBundledManifestSurface(plugin: PluginManifestRecord): boolean {
|
||||
return plugin.origin !== "bundled" || isPackageIncludedInCoreBundle(plugin.packageManifest);
|
||||
}
|
||||
|
||||
function resolveProviderOwnerPluginIds(
|
||||
params: ProviderRegistryLoadParams & {
|
||||
pluginIds: readonly string[];
|
||||
@@ -146,7 +139,6 @@ export function resolveBundledProviderCompatPluginIds(params: {
|
||||
.filter(
|
||||
(plugin) =>
|
||||
plugin.origin === "bundled" &&
|
||||
isCoreBundledManifestSurface(plugin) &&
|
||||
plugin.providers.length > 0 &&
|
||||
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user