From 5551d9fad4d46a5e3ed0d3fb6f66a3ca89f08c27 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 17:30:59 +0100 Subject: [PATCH] fix: discover source-only plugins in checkouts --- CHANGELOG.md | 1 + extensions/codex/package.json | 3 - extensions/diffs/package.json | 3 - extensions/discord/package.json | 3 - extensions/qqbot/package.json | 3 - scripts/generate-plugin-inventory-doc.mjs | 4 +- scripts/lib/optional-bundled-clusters.mjs | 7 -- scripts/root-dependency-ownership-audit.mjs | 3 - src/infra/package-dist-inventory.test.ts | 11 ++- src/infra/package-dist-inventory.ts | 73 ++++++---------- .../bundled-capability-metadata.test.ts | 27 +++++- .../inventory/bundled-capability-metadata.ts | 26 ++++-- .../contracts/registry.contract.test.ts | 7 +- src/plugins/discovery.test.ts | 64 ++++++++++++-- src/plugins/discovery.ts | 83 +++++++++++++++++-- .../installed-plugin-index-invalidation.ts | 2 - .../installed-plugin-index-record-builder.ts | 18 +--- src/plugins/installed-plugin-index-store.ts | 1 - src/plugins/installed-plugin-index-types.ts | 4 +- src/plugins/installed-plugin-index.test.ts | 61 -------------- .../manifest-registry-installed.test.ts | 29 ------- src/plugins/manifest-registry-installed.ts | 21 ++--- src/plugins/manifest.ts | 11 --- src/plugins/plugin-registry-contributions.ts | 7 -- src/plugins/providers.test.ts | 11 +-- src/plugins/providers.ts | 12 +-- 26 files changed, 227 insertions(+), 268 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d33a6b2f9b..0d2d2dae185 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/extensions/codex/package.json b/extensions/codex/package.json index fb49cab4992..3c04a90081a 100644 --- a/extensions/codex/package.json +++ b/extensions/codex/package.json @@ -26,9 +26,6 @@ "defaultChoice": "npm", "minHostVersion": ">=2026.5.1-beta.1" }, - "bundle": { - "includeInCore": false - }, "compat": { "pluginApi": ">=2026.5.2" }, diff --git a/extensions/diffs/package.json b/extensions/diffs/package.json index 9cbc4863ccc..4b787e3953b 100644 --- a/extensions/diffs/package.json +++ b/extensions/diffs/package.json @@ -35,9 +35,6 @@ "build": { "openclawVersion": "2026.5.2" }, - "bundle": { - "includeInCore": false - }, "release": { "publishToClawHub": true, "publishToNpm": true diff --git a/extensions/discord/package.json b/extensions/discord/package.json index 3ff11fb2b27..d9db05a22e4 100644 --- a/extensions/discord/package.json +++ b/extensions/discord/package.json @@ -69,9 +69,6 @@ "build": { "openclawVersion": "2026.5.2" }, - "bundle": { - "includeInCore": true - }, "release": { "publishToClawHub": true, "publishToNpm": true diff --git a/extensions/qqbot/package.json b/extensions/qqbot/package.json index 697b0a0e330..d79e9d4c965 100644 --- a/extensions/qqbot/package.json +++ b/extensions/qqbot/package.json @@ -55,9 +55,6 @@ "build": { "openclawVersion": "2026.5.2" }, - "bundle": { - "includeInCore": false - }, "release": { "publishToClawHub": true, "publishToNpm": true diff --git a/scripts/generate-plugin-inventory-doc.mjs b/scripts/generate-plugin-inventory-doc.mjs index 20987a19908..2b9a54e1ee9 100644 --- a/scripts/generate-plugin-inventory-doc.mjs +++ b/scripts/generate-plugin-inventory-doc.mjs @@ -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) { diff --git a/scripts/lib/optional-bundled-clusters.mjs b/scripts/lib/optional-bundled-clusters.mjs index df5a06f7cdd..fc23057f290 100644 --- a/scripts/lib/optional-bundled-clusters.mjs +++ b/scripts/lib/optional-bundled-clusters.mjs @@ -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; } diff --git a/scripts/root-dependency-ownership-audit.mjs b/scripts/root-dependency-ownership-audit.mjs index 28aa5ae12be..194bca91ec4 100644 --- a/scripts/root-dependency-ownership-audit.mjs +++ b/scripts/root-dependency-ownership-audit.mjs @@ -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) ?? []; diff --git a/src/infra/package-dist-inventory.test.ts b/src/infra/package-dist-inventory.test.ts index 57f699d1acb..1ab078f587b 100644 --- a/src/infra/package-dist-inventory.test.ts +++ b/src/infra/package-dist-inventory.test.ts @@ -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, diff --git a/src/infra/package-dist-inventory.ts b/src/infra/package-dist-inventory.ts index 432788f9e70..8d23c731e05 100644 --- a/src/infra/package-dist-inventory.ts +++ b/src/infra/package-dist-inventory.ts @@ -75,6 +75,27 @@ export function isLegacyPluginDependencyInstallStagePath(relativePath: string): ); } +function collectExcludedPackagedExtensionDirs(rootPackageJson: unknown): Set { + 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(); + 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 { - 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(); - 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( diff --git a/src/plugins/bundled-capability-metadata.test.ts b/src/plugins/bundled-capability-metadata.test.ts index ccf818bac2b..6c382f99c92 100644 --- a/src/plugins/bundled-capability-metadata.test.ts +++ b/src/plugins/bundled-capability-metadata.test.ts @@ -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 { + 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(); + 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( diff --git a/src/plugins/contracts/inventory/bundled-capability-metadata.ts b/src/plugins/contracts/inventory/bundled-capability-metadata.ts index 23d5c82c901..1a4d8ef44c5 100644 --- a/src/plugins/contracts/inventory/bundled-capability-metadata.ts +++ b/src/plugins/contracts/inventory/bundled-capability-metadata.ts @@ -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 | undefined { } } -function isExplicitlyDownloadablePlugin(packageJson: Record | undefined): boolean { - return !isPackageIncludedInCoreBundle(getPackageManifestMetadata(packageJson as PackageManifest)); +function collectExcludedPackagedExtensionDirs(): ReadonlySet { + 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(); + 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) { diff --git a/src/plugins/contracts/registry.contract.test.ts b/src/plugins/contracts/registry.contract.test.ts index 8aa753aa791..2e7cc0b92ac 100644 --- a/src/plugins/contracts/registry.contract.test.ts +++ b/src/plugins/contracts/registry.contract.test.ts @@ -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)); } diff --git a/src/plugins/discovery.test.ts b/src/plugins/discovery.test.ts index e64202578ed..195928d58c3 100644 --- a/src/plugins/discovery.test.ts +++ b/src/plugins/discovery.test.ts @@ -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 () => { diff --git a/src/plugins/discovery.ts b/src/plugins/discovery.ts index 2fe91b152ab..3f85587dae5 100644 --- a/src/plugins/discovery.ts +++ b/src/plugins/discovery.ts @@ -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 { + 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, diff --git a/src/plugins/installed-plugin-index-invalidation.ts b/src/plugins/installed-plugin-index-invalidation.ts index cc58a3041ef..c8ece3570d9 100644 --- a/src/plugins/installed-plugin-index-invalidation.ts +++ b/src/plugins/installed-plugin-index-invalidation.ts @@ -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 ) { diff --git a/src/plugins/installed-plugin-index-record-builder.ts b/src/plugins/installed-plugin-index-record-builder.ts index ae32138169a..2504c6f2698 100644 --- a/src/plugins/installed-plugin-index-record-builder.ts +++ b/src/plugins/installed-plugin-index-record-builder.ts @@ -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; } diff --git a/src/plugins/installed-plugin-index-store.ts b/src/plugins/installed-plugin-index-store.ts index 36ae559cdc7..a0c3d31619f 100644 --- a/src/plugins/installed-plugin-index-store.ts +++ b/src/plugins/installed-plugin-index-store.ts @@ -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(), diff --git a/src/plugins/installed-plugin-index-types.ts b/src/plugins/installed-plugin-index-types.ts index 0ee9e1b3202..eee98adab7e 100644 --- a/src/plugins/installed-plugin-index-types.ts +++ b/src/plugins/installed-plugin-index-types.ts @@ -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; diff --git a/src/plugins/installed-plugin-index.test.ts b/src/plugins/installed-plugin-index.test.ts index 988cf65b171..7300fbea3d1 100644 --- a/src/plugins/installed-plugin-index.test.ts +++ b/src/plugins/installed-plugin-index.test.ts @@ -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({ diff --git a/src/plugins/manifest-registry-installed.test.ts b/src/plugins/manifest-registry-installed.test.ts index 5e7d3fb2afd..41046ed402b 100644 --- a/src/plugins/manifest-registry-installed.test.ts +++ b/src/plugins/manifest-registry-installed.test.ts @@ -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(); diff --git a/src/plugins/manifest-registry-installed.ts b/src/plugins/manifest-registry-installed.ts index 52ecfca063f..cc0162124f8 100644 --- a/src/plugins/manifest-registry-installed.ts +++ b/src/plugins/manifest-registry-installed.ts @@ -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; diff --git a/src/plugins/manifest.ts b/src/plugins/manifest.ts index 37b33de66fe..d3fbef5c223 100644 --- a/src/plugins/manifest.ts +++ b/src/plugins/manifest.ts @@ -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 { diff --git a/src/plugins/plugin-registry-contributions.ts b/src/plugins/plugin-registry-contributions.ts index 78311c8b60d..909a7e3932b 100644 --- a/src/plugins/plugin-registry-contributions.ts +++ b/src/plugins/plugin-registry-contributions.ts @@ -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[] { ); } -function isCoreBundledManifestSurface(plugin: PluginManifestRecord): boolean { - return plugin.origin !== "bundled" || isPackageIncludedInCoreBundle(plugin.packageManifest); -} - function collectObjectKeys(value: Record | 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), ) diff --git a/src/plugins/providers.test.ts b/src/plugins/providers.test.ts index 8e815a53b75..d936b62ba67 100644 --- a/src/plugins/providers.test.ts +++ b/src/plugins/providers.test.ts @@ -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"], }); }); diff --git a/src/plugins/providers.ts b/src/plugins/providers.ts index 50686434a59..af25533bac2 100644 --- a/src/plugins/providers.ts +++ b/src/plugins/providers.ts @@ -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)), )