fix: discover source-only plugins in checkouts

This commit is contained in:
Peter Steinberger
2026-05-02 17:30:59 +01:00
parent cc8a8f1df1
commit 5551d9fad4
26 changed files with 227 additions and 268 deletions

View File

@@ -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.

View File

@@ -26,9 +26,6 @@
"defaultChoice": "npm",
"minHostVersion": ">=2026.5.1-beta.1"
},
"bundle": {
"includeInCore": false
},
"compat": {
"pluginApi": ">=2026.5.2"
},

View File

@@ -35,9 +35,6 @@
"build": {
"openclawVersion": "2026.5.2"
},
"bundle": {
"includeInCore": false
},
"release": {
"publishToClawHub": true,
"publishToNpm": true

View File

@@ -69,9 +69,6 @@
"build": {
"openclawVersion": "2026.5.2"
},
"bundle": {
"includeInCore": true
},
"release": {
"publishToClawHub": true,
"publishToNpm": true

View File

@@ -55,9 +55,6 @@
"build": {
"openclawVersion": "2026.5.2"
},
"bundle": {
"includeInCore": false
},
"release": {
"publishToClawHub": true,
"publishToNpm": true

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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) ?? [];

View File

@@ -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,

View File

@@ -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(

View File

@@ -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(

View File

@@ -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) {

View File

@@ -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));
}

View File

@@ -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 () => {

View File

@@ -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,

View File

@@ -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
) {

View File

@@ -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;
}

View File

@@ -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(),

View File

@@ -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;

View File

@@ -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({

View File

@@ -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();

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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),
)

View File

@@ -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"],
});
});

View File

@@ -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)),
)